본문 바로가기

Database

InnoDB 스토리지 엔진 아키텍처

728x90
반응형

MySQL 스토리지 엔진 중에 가장 많이 사용되는 InnoDB는 거의 유일하게 레코드 기반의 잠금을 제공하며, 그 때문에 높은 동시성 처리가 가능하고 안정적이며 성능이 뛰어나다. 이번 글에서는 InnoDB 스토리지 엔진의 핵심 아키텍처를 자세히 살펴보고자 한다.

출처 : Real MySQL 1권 4장. 아키텍처

프라이머리 키에 의한 클러스터링

InnoDB의 모든 테이블은 기본적으로 프라이머리 키를 기준으로 클러스터링되어 저장된다. 이는 프라이머리 키 값의 순서대로 디스크에 저장된다는 의미이고, 세컨더리 인덱스는 레코드의 주소 대신 프라이머리 키의 값을 논리적 주소로 사용한다.

클러스터링과 넌클러스터링의 차이

클러스터링 인덱스는 데이터가 인덱스 키 순서대로 물리적으로 정렬되어 저장되는 구조다. 반면 넌클러스터링 인덱스는 데이터의 실제 저장 순서와 무관하게 별도의 인덱스 구조를 가지며, 인덱스에는 데이터의 위치(포인터)만 저장된다. 책의 맨 뒤 색인처럼 특정 단어가 어느 페이지에 있는지 알려주는 구조라고 생각하면 된다. InnoDB와 달리 MyISAM 스토리지 엔진에서는 클러스터링 키를 지원하지 않는다. MyISAM은 모든 인덱스가 넌클러스터링 방식으로 동작한다.

외래키 지원

InnoDB 스토리지 엔진 레벨에서 지원하는 기능으로 MyISAM이나 MEMORY 테이블에서는 사용할 수 없다. 외래키는 서버 운영의 불편함 때문에 서비스용 데이터베이스에서는 생성하지 않는 경우가 종종 있는데, 데이터 무결성을 보장하는 좋은 가이드 역할을 할 수 있다.

foreign_key_checks 시스템 변수로 외래키 체크를 활성화/비활성화할 수 있다. 다만 외래키를 사용할 때 주의할 점은 잠금이 여러 테이블로 전파되고, 그로 인해 데드락이 발생할 때가 많다는 것이다. 따라서 개발할 때도 외래키는 신중하게 사용할 필요가 있다.

MVCC (Multi-Version Concurrency Control)

일반적으로 레코드 레벨의 트랜잭션을 지원하는 DBMS가 제공하는 기능이며, MVCC의 가장 큰 목적은 잠금을 사용하지 않는 일관된 읽기를 제공하기 위해서다. InnoDB는 언두 로그를 이용해 이 기능을 구현한다. 멀티 버전이라 함은 하나의 레코드에 대해 여러 개의 버전이 동시에 관리된다는 의미다.테이블에 한 건의 레코드를 INSERT한 다음 UPDATE해서 발생하는 변경 작업 및 절차를 확인해보자.

출처 : Real MySQL 1권 4장. 아키텍처
CREATE TABLE member (
    m_id INT NOT NULL,
    m_name VARCHAR(20) NOT NULL, 
    m_area VARCHAR(100) NOT NULL,
    PRIMARY KEY(m_id),
    INDEX ix_area(m_area)
);

INSERT INTO member (m_id, m_name, m_area) VALUES (12, 'dong', 'seoul');
COMMIT;

UPDATE member SET m_area = 'incheon' WHERE m_id = 12;

UPDATE 문장이 실행되면 커밋 실행 여부와 관계없이 InnoDB 버퍼 풀은 새로운 값인 'incheon'으로 업데이트된다. 그리고 디스크의 데이터 파일에는 체크포인트나 InnoDB Write 스레드에 의해 새로운 값으로 업데이트되어 있을 수도 있고 아닐 수도 있다.

InnoDB가 ACID를 보장하기 때문에 일반적으로는 InnoDB의 버퍼 풀과 데이터 파일은 동일한 상태라고 봐도 무방하다. 아직 COMMIT이나 ROLLBACK이 되지 않은 상태에서 다른 사용자가 다음과 같은 쿼리로 작업 중인 레코드를 조회하면 어디에 있는 데이터를 조회할까?

출처 : Real MySQL 1권 4장. 아키텍처
SELECT * FROM member WHERE m_id = 12;

이 질문의 답은 MySQL 서버의 시스템 변수(transaction_isolation)에 설정된 격리 수준에 따라 다르다. 격리 수준이 READ_UNCOMMITTED인 경우에는 InnoDB 버퍼 풀이 현재 가지고 있는 변경된 데이터를 읽어서 반환한다. 데이터가 커밋됐든 아니든 변경된 상태의 데이터를 반환하는 것이다.

그렇지 않고 READ_COMMITTED나 그 이상의 격리 수준(REPEATABLE_READ, SERIALIZABLE)인 경우에는 아직 커밋되지 않았기 때문에 InnoDB 버퍼 풀이나 데이터 파일에 있는 내용 대신 변경되기 이전의 내용을 보관하고 있는 언두 로그 영역의 데이터를 반환한다. 이러한 과정을 DBMS에서는 MVCC라고 표현한다.

잠금 없는 일관된 읽기 (Non-Locking Consistent Read)

출처 : Real MySQL 1권 4장. 아키텍처

InnoDB 스토리지 엔진은 MVCC 기술을 이용해 잠금을 걸지 않고 읽기 작업을 수행한다. 격리 수준이 SERIALIZABLE이 아닌 READ_UNCOMMITTED나 READ_COMMITTED, REPEATABLE_READ 수준인 경우, INSERT와 연결되지 않은 순수한 읽기(SELECT) 작업은 다른 트랜잭션의 변경 작업과 관계없이 항상 잠금을 대기하지 않고 바로 실행한다.

특정 사용자가 레코드를 변경하고 아직 커밋을 수행하지 않았다 하더라도 이 변경 트랜잭션이 다른 사용자의 SELECT 작업을 방해하지 않는다. 이를 '잠금 없는 일관된 읽기'라고 표현하며, InnoDB에서는 변경되기 전의 데이터를 읽기 위해 언두 로그를 사용한다.

자동 데드락 감지

InnoDB 스토리지 엔진은 내부적으로 잠금이 교착 상태에 빠지지 않았는지 체크하기 위해 잠금 대기 목록을 그래프(Wait-for List) 형태로 관리한다. InnoDB는 데드락 감지 스레드를 가지고 있어서 주기적으로 잠금 대기 그래프를 검사해 교착 상태에 빠진 트랜잭션들을 찾아낸다.

innodb_table_locks 시스템 변수를 활성화하면 InnoDB 스토리지 엔진 내부의 레코드 잠금뿐만 아니라 테이블 레벨의 잠금까지 감지할 수 있다. 이 기능의 장점은 더 광범위한 데드락을 감지할 수 있다는 것이고, 단점은 감지 작업 자체의 오버헤드가 증가한다는 점이다. 데드락이 감지되면 InnoDB는 트랜잭션 중 하나를 강제로 롤백시켜 데드락을 해소한다. 일반적으로 언두 로그의 양이 적은(롤백 비용이 낮은) 트랜잭션을 희생양으로 선택한다.

자동화된 장애 복구

InnoDB에는 손실이나 장애로부터 데이터를 보호하기 위한 여러 가지 메커니즘이 탑재되어 있다. InnoDB 데이터 파일은 기본적으로 MySQL 서버가 시작될 때 항상 자동 복구를 수행한다.

자동 복구는 innodb_force_recovery 시스템 변수로 제어할 수 있으며, 레벨 1부터 6까지 설정할 수 있다. 레벨이 높을수록 복구 작업이 덜 엄격해지며, 더 많은 손상을 허용하면서 서버를 시작한다.

InnoDB 버퍼 풀

InnoDB 스토리지 엔진에서 가장 핵심적인 부분으로, 디스크의 데이터 파일이나 인덱스 정보를 메모리에 캐시해 두는 저장 공간이다. 쓰기 작업을 지연시켜 일괄 작업으로 처리할 수 있게 해주는 버퍼 역할도 같이 한다.

일반적으로 애플리케이션에서는 INSERT, UPDATE, DELETE처럼 데이터를 변경하는 쿼리는 데이터 파일의 이곳저곳에 위치한 레코드를 변경하기 때문에 랜덤한 디스크 작업을 발생시킨다. 하지만 버퍼 풀이 이러한 변경된 데이터를 모아서 처리하면 랜덤한 디스크 작업의 횟수를 줄일 수 있다. 버퍼 풀 크기 설정은 운영체제와 각 클라이언트 스레드가 사용할 메모리도 충분히 고려해서 설정해야 한다. 

버퍼 풀의 구조

InnoDB 스토리지 엔진은 버퍼 풀이라는 거대한 메모리 공간을 페이지 크기(innodb_page_size 시스템 변수에 설정된, 기본값은 16KB)의 조각으로 쪼개어 InnoDB 스토리지 엔진이 데이터를 필요로 할 때 해당 데이터 페이지를 읽어서 각 조각에 저장한다. 버퍼 풀은 크게 세 가지 리스트로 관리된다.

  • LRU(Least Recently Used) 리스트: 디스크로부터 한 번 읽어온 페이지를 최대한 오랫동안 버퍼 풀의 메모리에 유지해서 디스크 읽기를 최소화하기 위한 리스트다.
  • 플러시(Flush) 리스트: 디스크로 동기화되지 않은 데이터를 가진 더티 페이지(Dirty Page)의 변경 시점 기준 페이지 목록을 관리한다.
  • 프리(Free) 리스트: 비어 있는 페이지들의 목록으로, 새로운 데이터를 읽어올 때 사용 가능한 페이지를 관리한다.

LRU와 MRU의 결합

엄밀하게 말하면 InnoDB의 LRU 리스트는 LRU와 MRU(Most Recently Used)가 결합된 형태다. 전통적인 LRU는 가장 최근에 사용된 페이지를 리스트의 맨 앞(MRU)에 두고, 오랫동안 사용되지 않은 페이지를 맨 뒤(LRU)에 두는 방식이다. InnoDB는 이를 개선해 LRU 리스트를 New 서브리스트(MRU 영역)와 Old 서브리스트(LRU 영역)로 구분한다. 새로운 페이지는 Old 영역의 맨 앞에 추가되고, 이 페이지가 실제로 읽히면 New 영역으로 승격된다. 이를 통해 한 번만 읽히고 다시 사용되지 않는 페이지(예: Full Table Scan)가 버퍼 풀을 오염시키는 것을 방지한다.

LRU 리스트 동작 방식

InnoDB 버퍼 풀의 LRU 리스트는 다음과 같이 동작한다.

  1. 필요한 레코드가 저장된 데이터 페이지가 버퍼 풀에 있는지 검사
    • InnoDB 어댑티브 해시 인덱스를 이용해 페이지를 검색한다. 어댑티브 해시 인덱스는 자주 읽히는 데이터에 대해 InnoDB가 자동으로 생성하는 해시 인덱스로, B-Tree를 거치지 않고 바로 데이터 페이지에 접근할 수 있게 해준다.
    • 해당 테이블의 인덱스(B-Tree)를 이용해 버퍼 풀에서 페이지를 검색한다.
    • 버퍼 풀에 이미 데이터 페이지가 있었다면 해당 페이지의 포인터를 MRU 방향으로 승급한다.
  2. 디스크에서 필요한 데이터 페이지를 버퍼 풀에 적재하고, 적재된 페이지에 대한 포인터를 LRU 헤더 부분(Old 영역의 맨 앞)에 추가한다.
  3. 버퍼 풀의 LRU 헤더 부분에 적재된 데이터 페이지가 실제로 읽히면 MRU 헤더 부분으로 이동한다. Read Ahead와 같이 대량 읽기의 경우 디스크의 데이터 페이지가 버퍼 풀로 적재는 되지만 실제 쿼리에서 사용되지는 않을 수도 있으며, 이런 경우에는 MRU로 이동되지 않는다.
  4. 버퍼 풀에 상주하는 데이터 페이지는 사용자 쿼리가 얼마나 최근에 접근했는지에 따라 나이(Age)가 부여되며, 버퍼 풀에 상주하는 동안 쿼리에서 오랫동안 사용되지 않으면 데이터 페이지에 부여된 나이가 오래되고 결국 해당 페이지는 버퍼 풀에서 제거된다. 버퍼 풀의 데이터 페이지가 쿼리에 의해 사용되면 나이가 초기화되어 다시 젊어지고 MRU 헤더 부분으로 옮겨진다.
  5. 필요한 데이터가 자주 접근됐다면 해당 페이지의 인덱스 키를 어댑티브 해시 인덱스에 추가한다.

버퍼 풀과 리두 로그

출처 : Real MySQL 1권 4장. 아키텍처

InnoDB 버퍼 풀과 리두 로그는 매우 밀접한 관계를 맺고 있다. InnoDB 버퍼 풀은 서버의 메모리가 허용하는 만큼 크게 설정하면 할수록 쿼리 성능이 빨라진다. InnoDB 버퍼 풀은 데이터베이스 서버의 성능 향상을 위해 데이터 캐시쓰기 버퍼링이라는 두 가지 용도가 있는데, 버퍼 풀의 메모리 공간만 단순히 늘리는 것은 데이터 캐시 기능만 향상시키는 것이다.

 

클린 페이지와 더티 페이지

버퍼 풀의 페이지는 크게 두 가지로 구분된다. 디스크와 내용이 동일한 클린 페이지(Clean Page)와 변경되었지만 아직 디스크에 기록되지 않은 더티 페이지(Dirty Page)다. 더티 페이지는 언젠가는 디스크로 기록되어야 하며, 이 작업을 플러시(Flush)라고 한다.

 

리두 로그와의 관계

리두 로그는 1개 이상의 고정 크기 파일을 연결해서 순환 고리(Circular Buffer)처럼 사용한다. 데이터 변경이 계속 발생하면 리두 로그 파일에 기록됐던 로그 엔트리는 어느 순간 다시 새로운 로그 엔트리로 덮어 쓰인다. 그러나 재사용 불가능한 공간을 활성 리두 로그(Active Redo Log)라고 하는데, 이는 아직 디스크로 기록되지 않은 더티 페이지와 관련된 로그다.

리두 로그 파일의 공간은 계속 순환되어 재사용되지만, 전체 리두 로그 파일에서 재사용 가능한 공간과 재사용 불가능한 공간은 LSN(Log Sequence Number)으로 구분한다.

 

버퍼 풀과 리두 로그 크기의 균형

다음 두 가지 경우를 생각해보자.

  1. InnoDB 버퍼 풀은 100GB이며 리두 로그 파일의 전체 크기는 100MB인 경우
  2. InnoDB 버퍼 풀은 100MB이며 리두 로그 파일의 전체 크기는 100GB인 경우

둘 다 좋은 설정은 아니다. 첫 번째 경우는 버퍼 풀에 더티 페이지가 많이 쌓여도 리두 로그 공간이 작아 빠르게 순환되므로, 더티 페이지를 자주 디스크로 플러시해야 한다. 이는 쓰기 성능 저하로 이어진다. 두 번째 경우는 버퍼 풀이 작아 캐시 효율이 떨어지고, 리두 로그는 쓸데없이 크다.

일반적으로 리두 로그는 버퍼 풀 크기를 고려해 설정하는데, 기본적으로 1~2GB 정도가 적절하지만, 쓰기가 많은 대용량 시스템에서는 수십 GB까지 설정하기도 한다. AWS RDS나 오라클 같은 상용 환경에서는 리두 로그 공간을 수백 GB로 설정한 사례도 있다. InnoDB 스토리지 엔진이 매우 많은 더티 페이지를 한 번에 기록해야 하는 상황이 오지 않도록, 버퍼 풀의 크기와 쓰기 부하를 고려해 최적값을 선택하는 것이 좋다.

버퍼 풀 플러시 (Buffer Pool Flush)

InnoDB는 더티 페이지를 디스크로 기록하는 작업을 플러시라고 하며, 주로 두 가지 방식으로 동작한다.

 

플러시 리스트 플러시

플러시 리스트에 있는 오래된 더티 페이지를 주기적으로 디스크로 기록한다. 이는 리두 로그 공간을 재사용하기 위해 필요하며, InnoDB의 백그라운드 스레드가 담당한다. 클리너 스레드(Cleaner Thread)가 innodb_io_capacity와 innodb_io_capacity_max 설정값에 따라 플러시 속도를 조절한다.

 

LRU 리스트 플러시

LRU 리스트에서 사용 빈도가 낮은 데이터 페이지를 제거해 새로운 페이지를 적재할 공간을 확보한다. 이때 제거 대상 페이지가 더티 페이지라면 디스크로 먼저 플러시한 후 제거한다.

버퍼 풀 상태 백업 및 복구

MySQL 서버를 재시작하면 버퍼 풀의 내용이 사라지고, 다시 디스크에서 데이터를 읽어와야 하므로 워밍업(Warm-up) 시간이 필요하다. InnoDB는 서버 종료 시 버퍼 풀의 상태를 디스크에 백업하고, 재시작 시 이를 복구하는 기능을 제공한다. innodb_buffer_pool_dump_at_shutdown과 innodb_buffer_pool_load_at_startup 변수로 이 기능을 활성화할 수 있다. 실제 데이터가 아닌 메타데이터(어떤 페이지가 버퍼 풀에 있었는지)만 저장하므로 백업 파일 크기는 작다.

버퍼 풀의 적재 내용 확인

information_schema.innodb_buffer_page 테이블을 조회하면 현재 버퍼 풀에 어떤 테이블의 어떤 페이지들이 적재되어 있는지 확인할 수 있다. 이를 통해 자주 사용되는 테이블이나 인덱스가 메모리에 잘 캐싱되어 있는지 모니터링할 수 있다.

Double Write Buffer

더티 페이지를 디스크로 플러시할 때 부분 쓰기(Partial Write) 문제가 발생할 수 있다. 예를 들어 16KB 페이지를 쓰는 도중 시스템이 중단되면 일부만 기록될 수 있다. InnoDB는 이를 방지하기 위해 더블 라이트 버퍼를 사용한다. 더티 페이지를 실제 데이터 파일에 쓰기 전에 더블 라이트 버퍼 영역에 먼저 순차적으로 기록하고, 그 후 각 페이지를 실제 위치에 기록한다. 만약 쓰기 도중 문제가 발생하면 더블 라이트 버퍼의 내용을 이용해 복구할 수 있다.

언두 로그

InnoDB 스토리지 엔진은 트랜잭션과 격리 수준을 보장하기 위해 DML(INSERT, UPDATE, DELETE)로 변경되기 이전 버전의 데이터를 별도로 백업한다. 이렇게 백업된 데이터를 언두 로그라고 한다.

언두 로그는 두 가지 핵심 역할을 수행한다.

  • 트랜잭션 보장: 트랜잭션이 롤백되면 트랜잭션 도중 변경된 데이터를 변경 전 데이터로 복구해야 하는데, 이때 언두 로그에 백업해둔 이전 버전의 데이터를 이용해 복구한다.
  • 격리 수준 보장: 특정 커넥션에서 데이터를 변경하는 도중에 다른 커넥션이 데이터를 조회하면 트랜잭션 격리 수준에 맞게 변경 중인 레코드를 읽지 않고 언두 로그에 백업해둔 데이터를 읽어서 반환하기도 한다.

언두 로그는 스토리지 엔진에서 매우 중요한 역할을 담당하지만 관리 비용도 많이 필요하다.

언두 로그 레코드 모니터링

언두 로그는 트랜잭션이 길어질수록 계속 쌓이게 된다. 특히 대용량 배치 작업이나 오래 실행되는 트랜잭션은 언두 로그를 급격히 증가시킬 수 있다.

언두 로그가 과도하게 쌓이면 몇 가지 문제가 발생한다. 첫째, 디스크 공간을 많이 차지한다. 둘째, InnoDB가 오래된 언두 로그를 검색해야 하므로 성능이 저하된다. 셋째, 언두 로그를 저장하는 언두 테이블스페이스가 비대해진다.

따라서 information_schema.innodb_trx 테이블을 모니터링해 오래 실행되는 트랜잭션이 있는지 확인하고, show engine innodb status 명령으로 언두 로그의 이력 목록 길이(History List Length)를 주기적으로 체크해야 한다. 이 값이 수백만 이상으로 증가하면 성능 문제의 신호일 수 있다.

언두 테이블스페이스 관리

언두 로그가 저장되는 공간을 언두 테이블스페이스(Undo Tablespace)라고 한다. MySQL 8.0부터는 언두 테이블스페이스를 동적으로 관리할 수 있게 개선되었다.

언두 테이블스페이스는 여러 개로 분할될 수 있으며, 각 테이블스페이스는 트랜잭션이 종료되면 자동으로 정리(Truncate)될 수 있다. innodb_undo_tablespaces 변수로 언두 테이블스페이스의 개수를 설정하고, innodb_undo_log_truncate 변수로 자동 정리 기능을 활성화할 수 있다.

언두 테이블스페이스가 여러 개로 분할되면, 하나의 테이블스페이스를 정리하는 동안 다른 테이블스페이스가 새로운 트랜잭션을 처리할 수 있어 서비스 중단 없이 공간을 회수할 수 있다. 이를 통해 언두 로그가 무한정 증가하는 것을 방지하고 디스크 공간을 효율적으로 관리할 수 있다.

체인지 버퍼

체인지 버퍼(Change Buffer)는 INSERT, UPDATE, DELETE로 인한 세컨더리 인덱스 변경 작업을 즉시 처리하지 않고 버퍼링하는 기능이다.

세컨더리 인덱스는 데이터의 순서와 무관하게 저장되므로, 인덱스를 업데이트하려면 해당 인덱스 페이지를 디스크에서 읽어와야 한다. 체인지 버퍼는 이러한 랜덤 I/O를 줄이기 위해 변경 사항을 메모리에 모아두었다가 나중에 일괄 처리한다.

유니크 인덱스의 경우 중복 체크가 필요하므로 체인지 버퍼를 사용할 수 없지만, 일반 세컨더리 인덱스는 체인지 버퍼를 통해 쓰기 성능을 크게 향상시킬 수 있다. innodb_change_buffering 변수로 어떤 작업에 체인지 버퍼를 사용할지 설정할 수 있다.

리두 로그 및 로그 버퍼

리두 로그(Redo Log)는 트랜잭션 4가지 요소인 ACID 중 D(Durability, 영속성)에 해당하는 영속성과 가장 밀접한 연관이 있다. 하드웨어나 소프트웨어 등 여러 문제점으로 인해 MySQL 서버가 비정상 종료되었을 때 데이터를 잃지 않게 해주는 안전장치다.

 

영속성 보장 메커니즘

모든 DBMS는 데이터 파일뿐만 아니라 트랜잭션의 변경 내용을 로그로 기록한다. 서버가 비정상 종료되더라도 리두 로그에 기록된 내용을 이용해 데이터를 복구할 수 있다. 리두 로그는 순차적으로 기록되므로 쓰기 속도가 빠르고, 변경 내용만 기록하므로 용량도 작다.

트랜잭션이 커밋되면 변경 내용이 리두 로그에 기록되고, 리두 로그가 디스크에 플러시(fsync)되면 트랜잭션이 영속성을 갖게 된다. 실제 데이터 파일은 나중에 버퍼 풀에서 디스크로 기록되어도 되며, 서버가 재시작되면 리두 로그를 재생(Redo)해서 데이터를 복구한다.

 

리두 로그 아카이빙

MySQL 8.0부터는 리두 로그 아카이빙 기능이 추가되었다. 이는 백업 도구가 백업하는 동안 생성되는 리두 로그를 별도로 아카이빙해서 백업의 일관성을 보장하는 기능이다.

 

innodb_redo_log_archive_dirs 변수로 아카이브 디렉토리를 설정하고, 필요할 때 활성화/비활성화할 수 있다. 활성화하면 백업 중에 생성된 모든 리두 로그가 보존되므로, 백업 시점부터 복구 시점까지의 모든 변경 내역을 적용할 수 있다. 비활성화하면 리두 로그가 순환되어 오래된 로그는 덮어씌워질 수 있다. 주로 핫 백업(Hot Backup) 시나리오에서 사용되며, 백업 완료 후에는 비활성화하는 것이 일반적이다.

어댑티브 해시 인덱스

일반적으로 인덱스라고 하면 이는 테이블에 사용자가 생성해둔 B-Tree 인덱스를 의미한다. 어댑티브 해시 인덱스(Adaptive Hash Index)는 B-Tree 검색 시간을 줄여주기 위해 InnoDB가 도입한 기능이다.

 

InnoDB는 자주 읽히는 데이터 페이지의 키 값을 이용해 내부적으로 해시 인덱스를 자동으로 생성한다. 해시 인덱스는 O(1)의 시간 복잡도로 매우 빠르게 데이터에 접근할 수 있다. B-Tree를 거치지 않고 바로 데이터 페이지의 주소로 이동할 수 있어 CPU 부하를 줄이고 쿼리 성능을 높인다.

 

다만 모든 경우에 유리한 것은 아니다. 동시 처리가 많고 쿼리 패턴이 다양한 환경에서는 해시 인덱스를 관리하는 오버헤드가 오히려 성능을 저하시킬 수 있다. innodb_adaptive_hash_index 변수로 이 기능을 활성화/비활성화할 수 있으며, 워크로드 특성에 따라 적절히 선택해야 한다. 주로 단순하고 반복적인 쿼리가 많은 OLTP 환경에서 효과적이다.

InnoDB와 다른 스토리지 엔진 비교

MySQL 8.0으로 업그레이드되면서 MyISAM과 MEMORY 스토리지 엔진은 거의 쓸 일이 없어졌다. 과거 MyISAM의 주요 장점이었던 전문 검색(Full-Text Search)이나 공간 좌표 검색(Spatial Index) 기능이 모두 InnoDB에서 지원되기 시작했기 때문이다.

InnoDB vs MyISAM

  • 트랜잭션: InnoDB는 트랜잭션을 지원하지만 MyISAM은 지원하지 않는다.
  • 잠금: InnoDB는 레코드 레벨 잠금을 사용해 동시성이 높지만, MyISAM은 테이블 레벨 잠금만 지원해 동시성이 낮다.
  • 외래키: InnoDB만 외래키를 지원한다.
  • 크래시 복구: InnoDB는 자동 복구가 강력하지만, MyISAM은 복구가 어렵고 데이터 손실 가능성이 높다.

InnoDB vs MEMORY

MEMORY 엔진은 모든 데이터를 메모리에만 저장하므로 매우 빠르지만, 서버가 재시작되면 데이터가 모두 사라진다. 임시 테이블 용도로만 제한적으로 사용된다. MySQL 8.0부터는 InnoDB의 버퍼 풀이 충분히 크다면 비슷한 성능을 낼 수 있어 MEMORY 엔진의 필요성이 줄어들었다.

 

PostgreSQL과의 비교

전문 검색과 공간 좌표 검색 측면에서 PostgreSQL과 비교하면, PostgreSQL의 PostGIS와 전문 검색 기능이 여전히 더 강력하고 성숙도가 높다. InnoDB도 이러한 기능을 지원하지만, 복잡한 공간 쿼리나 고급 텍스트 검색 기능에서는 PostgreSQL이 우위에 있다.

그러나 InnoDB는 일반적인 OLTP 워크로드에서 우수한 성능과 안정성을 제공하며, 특히 높은 동시성 환경에서 잘 동작한다. 결국 요구사항에 따라 적절한 데이터베이스를 선택하는 것이 중요하다.


이번 글에서는 InnoDB 스토리지 엔진의 핵심 아키텍처를 살펴봤다. 클러스터링 인덱스, MVCC, 버퍼 풀, 언두 로그, 리두 로그 등 InnoDB의 주요 컴포넌트들이 어떻게 동작하고 서로 상호작용하는지 이해하는 것은 MySQL 서버를 효율적으로 운영하고 성능을 최적화하는 데 필수적이다. 다음 글에서는 트랜잭션과 잠금에 대해 더 깊이 있게 다뤄보고자 한다.

728x90
반응형