콘텐츠로 이동

MySQL 안에서 InnoDB가 맡는 일

InnoDB를 외울 구성 요소의 목록으로 보지 않고, MySQL이 데이터를 안전하게 저장하기 위해 선택한 기본 실행 장치로 이해합니다.

예상 읽기 시간 8분 · 핵심 질문: MySQL과 InnoDB는 무엇이 다르고, 문제는 어느 층에서 생기는가

MySQL을 사용한다고 말할 때 실제로는 두 층이 함께 일한다. 바깥의 MySQL 서버는 애플리케이션의 연결을 받고 SQL을 해석하며 실행 계획을 세운다. 그 안의 InnoDB는 표의 행과 인덱스를 페이지에 배치하고, 여러 요청이 동시에 데이터를 바꿀 때 충돌을 조정하며, 커밋한 내용을 장애 뒤 복구한다.

그래서 둘은 경쟁 제품이 아니다. MySQL은 데이터베이스 서버 전체이고, InnoDB는 그 서버 안에서 저장과 트랜잭션을 담당하는 기본 저장 엔진이다. 별도로 설치해 조합하는 데이터베이스가 아니라, 일반적인 MySQL 표를 만들 때 자동으로 사용되는 핵심 구성 요소에 가깝다.

이 관계를 먼저 알아야 느린 SQL과 저장소 문제를 구분할 수 있다. 실행 계획이 잘못되어 불필요한 행을 많이 찾는다면 주로 MySQL 최적화기 쪽을 본다. 계획은 적절한데 디스크 조회, 잠금 대기, 긴 트랜잭션이나 복구 로그 때문에 느리다면 InnoDB 쪽을 본다.

저장 엔진이라는 층은 왜 생겼을까

초기 MySQL은 SQL을 처리하는 공통 서버데이터를 보관하는 방식을 분리했다. 서버는 표준화된 통로를 통해 저장 엔진에 “이 조건에 맞는 행을 찾아 달라”, “이 행을 업데이트해 달라”, “트랜잭션을 커밋해 달라”고 요청한다. 저장 엔진은 그 요청을 자기 방식의 파일, 인덱스, 잠금과 로그로 처리한다.

이 구조 덕분에 표를 만들 때 목적이 다른 엔진을 선택할 수 있었다. 같은 SELECTUPDATE를 사용해도 엔진에 따라 트랜잭션 지원, 잠금 범위, 장애 복구와 저장 방식이 달라진다. 현재 어떤 엔진을 쓸 수 있는지는 SHOW ENGINES로, 이미 만든 표의 엔진은 SHOW CREATE TABLE로 확인할 수 있다.

역사적으로 입문서에서 가장 자주 비교한 엔진은 MyISAMInnoDB다. MyISAM은 구조가 단순하고 과거의 조회 중심 환경에서 널리 쓰였지만, 트랜잭션을 제공하지 않고 업데이트할 때 표 단위 잠금의 영향을 크게 받으며 비정상 종료 뒤 복구 보장도 제한적이다. 여러 사용자가 주문·재고·결제 상태를 동시에 바꾸는 서버의 기본값으로 삼기 어렵다.

InnoDB는 트랜잭션, 행 수준 잠금, 다중 버전 동시성 제어와 장애 복구를 제공한다. MySQL 5.5부터 기본 저장 엔진이 되었고, 현재 MySQL 8.4에서도 별도로 지정하지 않은 일반 표는 InnoDB로 생성된다. 따라서 “MySQL에는 보통 MyISAM과 InnoDB 두 개가 있다”는 문장은 과거 배경을 이해하는 데는 유용하지만 현재 선택 상황을 그대로 묘사하지는 않는다. 오늘날의 출발점은 InnoDB이고, 다른 엔진은 특별한 요구가 있을 때 검토한다.

예를 들어 MEMORY 엔진은 휘발되어도 되는 임시성 데이터, NDB는 MySQL Cluster의 분산 구성이라는 특수한 목적이 있다. MyRocks처럼 쓰기량과 저장 공간을 줄이기 위해 LSM 트리를 사용하는 외부 계열도 있지만 기본 MySQL의 단순한 교체 옵션은 아니다. 엔진 이름을 많이 외우기보다, 트랜잭션이 필요한 일반 업무 데이터라면 왜 InnoDB가 기본인지를 먼저 이해하는 편이 낫다.

InnoDB가 해결하려는 세 가지 문제

첫째, 여러 변경을 하나의 약속으로 묶어야 한다. 주문을 만들었는데 재고 차감이 실패했다면 두 변경을 모두 취소해야 한다. InnoDB의 트랜잭션은 중간 상태를 외부에 완성된 결과처럼 보이지 않게 하고, 커밋하거나 롤백하는 경계를 제공한다.

둘째, 여러 요청이 같은 데이터를 동시에 조회하고 업데이트해야 한다. 모든 작업을 한 줄로 세우면 안전하지만 처리량이 크게 떨어진다. InnoDB는 행과 인덱스 범위에 잠금을 걸고, 이전 버전을 필요한 동안 남기는 MVCC(Multi-Version Concurrency Control, 다중 버전 동시성 제어)를 함께 사용한다. 조회는 가능한 한 일관된 과거 모습을 보고, 충돌하는 업데이트는 기다리거나 한쪽을 중단한다.

셋째, 커밋 직후 서버가 꺼져도 성공했다고 답한 변경을 되살려야 한다. 매 커밋마다 큰 데이터 페이지 전체를 원하는 위치에 바로 쓰면 느리다. InnoDB는 복구에 필요한 변경 기록을 먼저 순차적으로 남기고, 실제 데이터 페이지는 나중에 기록한다. 이 역할 분리가 빠른 커밋과 장애 복구를 함께 가능하게 한다.

애플리케이션 요청을 MySQL 서버 계층이 해석하고 InnoDB의 버퍼 풀, 실행 취소 로그, 재실행 로그와 디스크 페이지로 전달하는 구조
MySQL 서버가 SQL의 실행 방법을 정하면 InnoDB가 메모리의 페이지를 조회·업데이트하고, 로그와 디스크를 사용해 트랜잭션과 복구를 책임집니다.

메모리와 디스크가 함께 움직이는 구조

버퍼 풀(Buffer Pool)은 InnoDB가 데이터와 인덱스 페이지를 보관하는 큰 메모리 영역이다. 필요한 페이지가 이미 버퍼 풀에 있으면 디스크를 거치지 않고 처리한다. 없으면 디스크에서 페이지를 가져오며, 새 공간이 필요하면 덜 사용한 페이지를 내보낸다. 조회 성능을 볼 때 단순히 “인덱스를 탔는가”뿐 아니라 몇 페이지를 요구했고 그 페이지가 메모리에 있었는지를 함께 보는 이유다.

업데이트된 페이지는 즉시 원래 디스크 위치에 기록되지 않을 수 있다. 메모리에서 바뀌었지만 아직 데이터 파일에 반영되지 않은 페이지를 더티 페이지(Dirty Page)라고 한다. 백그라운드 작업이 이를 나중에 내려 쓰기 때문에, 짧은 시간에 업데이트가 몰리면 더티 페이지와 기록 대역폭이 함께 늘어난다.

리두 로그(Redo Log, 재실행 로그)는 비정상 종료 뒤 다시 적용할 변경 기록이다. 데이터 페이지보다 리두 로그를 먼저 안전하게 남기는 규칙을 WAL(Write-Ahead Logging, 선행 기록 로그)이라고 한다. 커밋 뒤 데이터 페이지가 아직 디스크에 없더라도 리두 로그가 남아 있으면 재시작 과정에서 변경을 복원할 수 있다. 리두 로그는 복제에 쓰는 바이너리 로그와 목적이 다르다. 리두는 InnoDB의 장애 복구, 바이너리 로그는 서버 계층의 복제와 시점 복구에 중심이 있다.

언두 로그(Undo Log, 실행 취소 로그)는 업데이트 전 값을 추적한다. 트랜잭션을 롤백할 때 이전 상태로 되돌리고, 다른 트랜잭션이 조금 전 시점의 일관된 데이터를 조회할 때 과거 버전을 재구성한다. 리두와 언두는 같은 로그를 두 번 저장하는 것이 아니다. 리두는 “성공한 변경을 다시 적용하기 위해”, 언두는 “변경을 되돌리거나 이전 버전을 보여 주기 위해” 존재한다.

아무도 과거 버전을 필요로 하지 않게 되면 퍼지(Purge) 작업이 오래된 언두 기록과 삭제 표시된 레코드를 정리한다. 오래 열린 트랜잭션이 있으면 그 트랜잭션이 볼 수 있는 과거 상태를 지울 수 없어 정리가 밀린다. 이때 언두 이력이 길어지고 저장 공간과 과거 버전 탐색 비용이 증가한다.

한 번의 조회와 업데이트는 어떻게 흐를까

조회는 MySQL 서버가 SQL을 해석하는 것으로 시작한다. 최적화기는 통계와 비용을 이용해 어떤 인덱스를 사용할지, 여러 표를 어떤 순서로 결합할지 정한다. 그 뒤 InnoDB가 선택된 인덱스 페이지를 버퍼 풀에서 찾고, 없으면 디스크에서 가져온다. MVCC 규칙으로 현재 트랜잭션에 보이는 행인지 확인한 다음 결과를 서버 계층으로 돌려보낸다.

업데이트도 처음에는 같은 경로로 대상 행을 찾는다. 이후 InnoDB가 필요한 잠금을 얻고, 되돌릴 수 있도록 이전 값을 언두 로그에 연결하며, 버퍼 풀의 페이지를 바꾼다. 복구할 변경은 리두 로그 버퍼에 쌓인다. 커밋 시점에는 설정된 내구성 계약에 따라 리두 로그를 안전한 저장 장치까지 밀어낸다. 데이터 페이지는 백그라운드에서 나중에 기록할 수 있다.

이 순서에서 중요한 점은 커밋이 곧 데이터 파일 쓰기 완료를 뜻하지 않는다는 것이다. 커밋의 핵심은 약속한 수준까지 복구 로그를 안전하게 만드는 데 있다. innodb_flush_log_at_trx_commit 같은 설정을 성능 옵션으로만 보면 안 되는 이유다. 값을 바꾸면 장애 순간 잃을 수 있는 데이터 범위도 바뀐다.

기본 키가 저장 위치까지 결정한다

InnoDB 표는 기본 키로 정렬된 클러스터형 인덱스(Clustered Index) 안에 행 전체를 둔다. 기본 키 인덱스가 행을 가리키는 별도 목차인 것이 아니라, 가장 아래 리프 페이지가 곧 행 본문이다.

이메일처럼 다른 열에 만든 보조 인덱스는 물리 주소 대신 기본 키를 함께 저장한다. 이메일로 사용자를 찾을 때 보조 인덱스에서 기본 키를 얻고, 필요한 열이 보조 인덱스에 없다면 기본 키 트리를 다시 찾아 행 전체를 가져온다. 그래서 기본 키가 길면 모든 보조 인덱스가 함께 커진다. 무작위 값이 넓게 흩어지는 기본 키는 여러 페이지에 삽입을 분산시키고 페이지 분할과 캐시 압박을 늘릴 수 있다.

기본 키가 없으면 InnoDB는 모든 열이 NOT NULL인 첫 고유 키를 찾고, 그것도 없으면 내부 행 식별자를 만든다. 내부 식별자는 애플리케이션이 사용할 수 없으므로, 업무 식별과 저장 비용을 고려해 명시적인 기본 키를 설계하는 편이 낫다.

MVCC가 있어도 잠금은 사라지지 않는다

일반 조회는 조회 뷰(Read View)를 기준으로 보이는 버전을 선택할 수 있다. 하지만 같은 행을 업데이트하거나 SELECT ... FOR UPDATE처럼 최신 행을 잠그는 조회는 충돌을 조정해야 한다. InnoDB는 인덱스 레코드 잠금뿐 아니라 레코드 사이 범위를 막는 갭 잠금과 둘을 합친 넥스트 키 잠금을 사용한다.

잠금 범위는 SQL 문장만으로 정해지지 않고 실제로 탐색한 인덱스 범위의 영향을 받는다. 적절한 인덱스가 없으면 예상보다 많은 레코드와 범위를 확인하며 잠금 대기가 넓어질 수 있다. 서로 다른 순서로 자원을 잠근 두 트랜잭션이 원형으로 기다리면 교착 상태가 생기고, InnoDB는 한쪽을 중단해 순환을 끊는다. 따라서 교착 상태는 “절대 생기지 않게” 만드는 것보다 트랜잭션을 짧게 유지하고, 잠금 순서를 맞추며, 중단된 요청을 안전하게 재시도하도록 다루는 문제다.

적응형 해시 인덱스는 기본 설계 수단이 아니다

InnoDB는 반복해서 접근하는 B-Tree 페이지 패턴을 보고 메모리에 적응형 해시 인덱스(Adaptive Hash Index)를 만들 수 있다. 특정 지점 조회를 줄일 수 있지만 작업 형태와 경합에 따라 이득이 없을 수도 있다. 애플리케이션이 직접 만드는 인덱스와 달리 내부 최적화이므로, 이것을 전제로 스키마를 설계하기보다 측정 지표를 보고 활성화 상태를 판단해야 한다.

PostgreSQL과 비교하면 차이가 더 선명해진다

PostgreSQL의 일반 표는 힙에 행을 두고 인덱스가 행의 물리 위치를 가리킨다. 업데이트하면 힙에 새 행 버전을 만들고 VACUUM이 오래된 버전을 정리한다. InnoDB는 기본 키 B-Tree의 리프에 행을 두고, 이전 버전은 주로 언두 연결을 따라 재구성하며 퍼지가 정리한다.

둘 다 B-Tree, 트랜잭션과 MVCC를 제공하지만 비용이 퍼지는 위치가 다르다. InnoDB에서는 기본 키의 길이와 삽입 순서가 모든 보조 인덱스에 영향을 준다. PostgreSQL에서는 새 행 버전과 VACUUM 상태가 업데이트 비용과 표 팽창에 큰 영향을 준다. 제품 이름만 비교하지 말고, 대표 조회와 업데이트가 각 저장 구조에서 몇 페이지와 몇 버전을 거치는지 봐야 한다.

운영 중에는 이 신호부터 본다

버퍼 풀 적중률만으로 상태를 결론 내리면 안 된다. 한 질의가 너무 많은 페이지를 요구하면 적중률이 높아도 느릴 수 있다. 실행 계획의 예상 행 수와 실제 행 수, 질의당 페이지 수, 저장 장치 지연을 함께 본다.

잠금 대기가 늘면 어떤 SQL이 오래 실행되는지뿐 아니라 트랜잭션이 열린 채 애플리케이션 작업을 기다리는지 확인한다. 언두 이력이 계속 증가하면 업데이트 속도만 볼 것이 아니라 오래 유지되는 조회 뷰와 복제본의 긴 질의도 찾는다. 커밋 지연이 튀면 리두 로그 기록량, fsync 지연, 체크포인트와 더티 페이지 기록 압력을 함께 본다.

마지막으로 기본 키와 보조 인덱스의 폭을 확인한다. 쿼리에 기본 키가 직접 나오지 않더라도 보조 인덱스가 기본 키를 포함한다. 긴 기본 키와 과도한 보조 인덱스는 저장 공간뿐 아니라 버퍼 풀에 담을 수 있는 유효 데이터 양과 모든 업데이트 비용을 바꾼다.

이 글을 한 문장으로 설명한다면

MySQL은 SQL 요청을 받아 실행 방법을 정하는 데이터베이스 서버이고, InnoDB는 그 안에서 실제 행과 인덱스를 페이지에 저장하고 트랜잭션·잠금·과거 버전·복구 로그를 관리하는 기본 저장 엔진이다. 과거에는 MyISAM과 자주 비교했지만, 현재 일반적인 업무 데이터의 출발점은 InnoDB다.

InnoDB를 알아야 하는 이유는 내부 구성 요소를 외우기 위해서가 아니다. 기본 키 하나가 모든 보조 인덱스의 크기를 바꾸는 이유, 긴 트랜잭션이 오래된 버전 정리를 막는 이유, 커밋과 데이터 파일 기록 시점이 다른 이유를 연결해 성능과 장애를 올바른 층에서 찾기 위해서다.

참고 자료

공식 문서

입문 설명