카테고리 : MySQL/기술노트

InnoDB를 기본 엔진으로 선택하는 이유: ACID, MVCC, crash recovery

MySQL에서 InnoDB가 기본 스토리지 엔진이 된 이유를 ACID, MVCC, crash recovery와 운영 관점에서 정리한다.

저자: MySQL 기술 노트 작성: 2026.05.23 약 16분 9,590자
다운로드

1. 왜 InnoDB가 기본 엔진이어야 하는가

MySQL은 역사적으로 여러 스토리지 엔진을 지원해 왔다. MyISAM, MEMORY, ARCHIVE, CSV, NDB, InnoDB처럼 각 엔진은 서로 다른 저장 구조와 잠금 방식, 복구 방식을 가진다. 이 구조는 MySQL의 유연성을 높였지만, 운영 관점에서는 “어떤 엔진을 기본으로 둘 것인가”라는 중요한 선택을 요구한다.

현대적인 온라인 서비스의 기본 요구사항은 단순히 데이터를 빠르게 읽고 쓰는 것에 그치지 않는다. 장애가 발생해도 커밋된 데이터가 보존되어야 하고, 동시에 많은 트랜잭션이 실행되어도 서로의 중간 상태를 오염시키지 않아야 하며, 운영자는 백업·복구·복제·장애 조치 시 일관된 기준으로 시스템을 판단할 수 있어야 한다. 이 조건을 만족하기 위해 MySQL은 InnoDB를 기본 스토리지 엔진으로 삼는다.

InnoDB를 기본 엔진으로 선택하는 핵심 이유는 다음 세 가지로 요약할 수 있다.

  • ACID 트랜잭션을 제공하여 데이터 변경의 원자성, 일관성, 격리성, 지속성을 보장한다.
  • MVCC를 통해 읽기와 쓰기의 충돌을 줄이고, 동시성 높은 OLTP 워크로드를 처리한다.
  • crash recovery 구조를 통해 프로세스 종료, OS 장애, 전원 장애 이후에도 커밋된 변경을 복구하고 미완료 변경을 정리한다.

이 글은 InnoDB의 장점을 기능 목록으로 나열하기보다, 운영자가 실제로 장애와 성능 문제를 해석할 때 필요한 내부 동작 중심으로 설명한다.

2. MySQL 스토리지 엔진 계층과 InnoDB의 위치

MySQL 서버는 크게 SQL 계층과 스토리지 엔진 계층으로 나눌 수 있다. SQL 계층은 접속, 인증, 파서, 옵티마이저, 실행 계획, replication SQL 처리, binlog 기록 같은 공통 기능을 담당한다. 스토리지 엔진 계층은 실제 테이블과 인덱스 데이터를 저장하고, 레코드를 읽고 쓰며, 잠금과 트랜잭션을 구현한다.

간단히 보면 다음과 같다.

Client
  -> MySQL Server SQL Layer
       - parser / optimizer / executor
       - privilege / connection / binlog
  -> Storage Engine API
       - InnoDB
       - MyISAM
       - MEMORY
       - ...

SQL 계층이 UPDATE accounts SET balance = balance - 100 WHERE id = 1이라는 명령을 해석하더라도, 실제 레코드 잠금, undo 기록, redo 기록, 버퍼 풀 변경, 디스크 flush 정책은 InnoDB가 담당한다. 따라서 MySQL을 운영할 때 “MySQL이 느리다” 또는 “MySQL이 장애 후 복구 중이다”라는 표현은 실제로는 상당 부분 “InnoDB 내부에서 무엇이 일어나고 있는가”를 의미한다.

현재 일반적인 OLTP 시스템에서 InnoDB가 기본값인 이유는 단순히 최신 엔진이기 때문이 아니다. MyISAM처럼 테이블 단위 잠금과 비트랜잭션 구조를 가진 엔진은 일부 읽기 위주 또는 특수 목적에는 쓸 수 있지만, 장애 후 일관성과 동시 쓰기 처리 면에서 현대 서비스의 기본값으로 삼기 어렵다. 반면 InnoDB는 트랜잭션, row-level locking, clustered index, buffer pool, redo/undo log, foreign key, crash recovery를 통합적으로 제공한다.

현재 서버의 기본 엔진은 다음 SQL로 확인할 수 있다.

SHOW VARIABLES LIKE 'default_storage_engine';

SELECT ENGINE, SUPPORT, TRANSACTIONS, XA, SAVEPOINTS
FROM information_schema.ENGINES
WHERE ENGINE IN ('InnoDB', 'MyISAM', 'MEMORY');

실행 결과(MySQL 8.0.46):

+------------------------+--------+
| Variable_name          | Value  |
+------------------------+--------+
| default_storage_engine | InnoDB |
+------------------------+--------+
+--------+---------+--------------+------+------------+
| ENGINE | SUPPORT | TRANSACTIONS | XA   | SAVEPOINTS |
+--------+---------+--------------+------+------------+
| MEMORY | YES     | NO           | NO   | NO         |
| InnoDB | DEFAULT | YES          | YES  | YES        |
| MyISAM | YES     | NO           | NO   | NO         |
+--------+---------+--------------+------+------------+

새 테이블의 엔진은 명시하지 않으면 default_storage_engine 값을 따른다.

CREATE TABLE orders (
  id BIGINT PRIMARY KEY,
  customer_id BIGINT NOT NULL,
  amount DECIMAL(12,2) NOT NULL,
  status VARCHAR(30) NOT NULL,
  created_at DATETIME NOT NULL
) ENGINE=InnoDB;

SELECT table_name, engine
FROM information_schema.tables
WHERE table_schema = DATABASE()
  AND table_name = 'orders';

실행 결과(MySQL 8.0.46):

+------------+--------+
| TABLE_NAME | ENGINE |
+------------+--------+
| orders     | InnoDB |
+------------+--------+

운영 환경에서는 특별한 이유가 없는 한 테이블 생성 시 ENGINE=InnoDB를 명시하거나, 적어도 서버 기본값이 InnoDB인지 표준 점검 항목에 포함하는 편이 안전하다.

3. ACID: InnoDB 트랜잭션의 운영상 의미

ACID는 트랜잭션 시스템을 설명하는 고전적인 약어다. 그러나 운영자에게 중요한 것은 정의를 외우는 것이 아니라, 각 속성이 장애와 동시성 상황에서 어떤 판단 기준을 제공하는지 이해하는 것이다.

3.1 Atomicity: 일부만 반영된 변경을 남기지 않는다

원자성은 트랜잭션 안의 변경이 모두 반영되거나 모두 취소되어야 한다는 의미다. 예를 들어 계좌 이체는 출금과 입금이 함께 성공해야 한다.

CREATE TABLE accounts (
  account_id BIGINT PRIMARY KEY,
  balance DECIMAL(15,2) NOT NULL
) ENGINE=InnoDB;

INSERT INTO accounts VALUES
  (101, 50000.00),
  (202, 30000.00);

START TRANSACTION;

UPDATE accounts
SET balance = balance - 10000
WHERE account_id = 101;

UPDATE accounts
SET balance = balance + 10000
WHERE account_id = 202;

COMMIT;

SELECT account_id, balance
FROM accounts
ORDER BY account_id;

DROP TABLE accounts;

실행 결과(MySQL 8.0.46):

+------------+----------+
| account_id | balance  |
+------------+----------+
|        101 | 40000.00 |
|        202 | 40000.00 |
+------------+----------+

두 번째 UPDATE가 실패했는데 첫 번째 UPDATE만 남는다면 회계 데이터는 즉시 깨진다. InnoDB는 변경 전 이미지를 undo log에 기록하여 트랜잭션이 rollback될 때 이전 상태로 되돌릴 수 있게 한다. 이 구조 때문에 애플리케이션이 오류를 감지하고 ROLLBACK을 실행하면 미완료 변경은 논리적으로 취소된다.

원자성은 애플리케이션 코드의 책임과도 연결된다. InnoDB가 원자성을 제공하더라도 애플리케이션이 autocommit 상태에서 관련 SQL을 여러 개 따로 실행하면 하나의 업무 단위가 여러 트랜잭션으로 쪼개진다. 운영자는 장애 분석 시 “업무상 한 작업”과 “DB 트랜잭션 한 개”가 일치하는지 확인해야 한다.

3.2 Consistency: 제약 조건과 애플리케이션 규칙을 경계로 삼는다

일관성은 트랜잭션 전후에 데이터가 정의된 규칙을 만족해야 한다는 뜻이다. InnoDB는 primary key, unique key, foreign key, not null, check constraint 같은 데이터베이스 제약을 통해 일관성 일부를 강제한다.

CREATE TABLE order_items (
  order_id BIGINT NOT NULL,
  line_no INT NOT NULL,
  product_id BIGINT NOT NULL,
  quantity INT NOT NULL,
  PRIMARY KEY (order_id, line_no),
  CONSTRAINT chk_order_items_quantity CHECK (quantity > 0)
) ENGINE=InnoDB;

다만 모든 업무 규칙을 InnoDB가 알 수 있는 것은 아니다. “주문 상태가 PAID로 바뀔 때 결제 승인 레코드가 반드시 존재해야 한다” 같은 규칙은 외래키와 트랜잭션 경계, 애플리케이션 검증을 함께 설계해야 한다. 즉 InnoDB의 일관성 보장은 데이터베이스가 알 수 있는 규칙을 강하게 지켜 주는 것이며, 업무 규칙까지 자동으로 설계해 주는 것은 아니다.

3.3 Isolation: 동시에 실행되는 트랜잭션의 간섭을 통제한다

격리성은 여러 트랜잭션이 동시에 실행될 때 각 트랜잭션이 어느 정도 독립된 세계를 보는지 정의한다. MySQL InnoDB의 기본 격리 수준은 일반적으로 REPEATABLE READ다.

SELECT @@transaction_isolation;

실행 결과(MySQL 8.0.46):

+-------------------------+
| @@transaction_isolation |
+-------------------------+
| REPEATABLE-READ         |
+-------------------------+

InnoDB는 row-level locking과 MVCC를 조합하여 격리성을 구현한다. 단순 조회는 대개 잠금을 잡지 않고 일관된 snapshot을 읽고, 변경 작업은 필요한 레코드나 인덱스 범위에 잠금을 잡는다. 이 조합 덕분에 읽기와 쓰기를 모두 테이블 단위로 막지 않고도 일관성을 유지할 수 있다.

격리성은 성능 문제와 직접 연결된다. 잠금 대기, deadlock, gap lock, next-key lock 같은 현상은 대부분 “정확한 격리 수준을 유지하기 위해 InnoDB가 어디에 어떤 잠금을 잡았는가”의 문제다. 따라서 InnoDB를 이해하지 못한 상태에서 단순히 innodb_lock_wait_timeout을 늘리는 것은 근본 해결이 되기 어렵다.

3.4 Durability: 커밋된 변경은 장애 후에도 남아야 한다

지속성은 COMMIT이 성공한 변경이 장애 후에도 보존되어야 한다는 의미다. InnoDB는 redo log와 flush 정책을 통해 지속성을 구현한다.

대표 설정은 다음과 같다.

SHOW VARIABLES LIKE 'innodb_flush_log_at_trx_commit';
SHOW VARIABLES LIKE 'sync_binlog';

실행 결과(MySQL 8.0.46):

+--------------------------------+-------+
| Variable_name                  | Value |
+--------------------------------+-------+
| innodb_flush_log_at_trx_commit | 1     |
+--------------------------------+-------+
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| sync_binlog   | 1     |
+---------------+-------+

innodb_flush_log_at_trx_commit=1이면 트랜잭션 커밋 시 redo log를 디스크에 flush하는 가장 강한 지속성 설정이다. 복제를 함께 고려하면 sync_binlog=1도 중요한 설정이다. 성능을 위해 이 값을 완화할 수는 있지만, 장애 시 최근 트랜잭션 손실 가능성을 받아들이는 선택이라는 점을 명확히 해야 한다.

Aurora MySQL에서는 스토리지 계층이 일반 MySQL과 다르다. Aurora는 redo log 성격의 변경 기록을 분산 스토리지 계층에 전송하고, 6-way 복제 구조와 quorum 기반 쓰기 확인을 사용한다. 그래서 로컬 디스크 flush의 의미가 Community MySQL과 완전히 같지는 않다. 그러나 애플리케이션 관점에서 COMMIT 이후의 지속성을 기대한다는 사실은 동일하며, 파라미터 변경이 장애 허용 범위에 어떤 의미를 갖는지는 Aurora 문서와 운영 정책에 맞추어 별도로 검토해야 한다.

4. MVCC: 읽기와 쓰기의 충돌을 줄이는 핵심 구조

MVCC는 Multi-Version Concurrency Control의 약자다. 하나의 레코드에 대해 여러 버전의 논리적 상태를 관리하여, 읽기 트랜잭션이 쓰기 트랜잭션과 불필요하게 충돌하지 않도록 하는 방식이다.

InnoDB의 각 레코드에는 내부적으로 트랜잭션 ID와 rollback pointer 같은 버전 추적 정보가 연결된다. 변경 전 이미지는 undo log에 보관된다. 어떤 트랜잭션이 일관된 읽기 consistent read를 수행하면, InnoDB는 해당 트랜잭션의 read view를 기준으로 “이 트랜잭션이 볼 수 있는 버전”을 선택한다.

예를 들어 세션 A가 트랜잭션을 시작한 뒤 주문 금액을 조회했다고 하자.

-- Session A
START TRANSACTION;
SELECT amount FROM orders WHERE id = 1001;

동시에 세션 B가 같은 주문의 금액을 변경하고 커밋한다.

-- Session B
START TRANSACTION;
UPDATE orders SET amount = amount + 5000 WHERE id = 1001;
COMMIT;

세션 A가 REPEATABLE READ에서 같은 행을 다시 조회하면, 일반적인 consistent read에서는 트랜잭션 시작 시점의 snapshot을 계속 볼 수 있다.

-- Session A
SELECT amount FROM orders WHERE id = 1001;
COMMIT;

이 동작은 보고서성 조회와 OLTP 변경이 동시에 존재하는 시스템에서 매우 중요하다. 읽기 쿼리가 모든 쓰기를 막고, 쓰기 쿼리가 모든 읽기를 막는 구조라면 동시 사용자가 많은 서비스는 쉽게 병목에 걸린다. InnoDB는 MVCC를 통해 많은 단순 조회를 잠금 없는 일관 읽기로 처리한다.

다만 MVCC가 모든 잠금을 없애는 것은 아니다. 다음 쿼리들은 읽기처럼 보이지만 잠금을 동반한다.

SELECT * FROM orders WHERE id = 1001 FOR UPDATE;
SELECT * FROM orders WHERE status = 'READY' ORDER BY id LIMIT 10 FOR UPDATE;
SELECT * FROM orders WHERE id = 1001 LOCK IN SHARE MODE;

또한 UPDATE, DELETE, INSERT는 당연히 변경 대상에 대한 잠금을 필요로 한다. 특히 인덱스가 부적절하면 InnoDB는 훨씬 넓은 범위를 스캔하고 잠글 수 있다. 운영자가 잠금 문제를 볼 때 실행 계획과 인덱스를 함께 확인해야 하는 이유가 여기에 있다.

5. Undo log, purge, history list length

MVCC는 과거 버전을 유지해야 하므로 undo log와 purge 동작을 이해해야 한다. 긴 트랜잭션이 오래 열려 있으면, InnoDB는 그 트랜잭션이 필요로 할 수 있는 오래된 레코드 버전을 쉽게 삭제하지 못한다. 이때 undo log가 쌓이고 purge가 지연되며, history list length가 증가할 수 있다.

운영 환경에서 다음과 같은 쿼리와 명령을 통해 긴 트랜잭션과 InnoDB 상태를 확인한다.

SELECT trx_id,
       trx_state,
       trx_started,
       TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS trx_age_seconds,
       trx_mysql_thread_id,
       trx_query
FROM information_schema.INNODB_TRX
ORDER BY trx_started;
SHOW ENGINE INNODB STATUS\G

SHOW ENGINE INNODB STATUS 출력의 TRANSACTIONS 섹션에는 history list length, 활성 트랜잭션, 잠금 대기 정보가 나타난다. history list length가 계속 증가하고 purge가 따라가지 못하면 다음 문제가 발생할 수 있다.

  • undo tablespace 또는 시스템 테이블스페이스 사용량 증가
  • consistent read가 오래된 버전을 따라가며 조회 비용 증가
  • purge 부하 증가로 인한 백그라운드 I/O 압박
  • DDL 또는 스키마 변경 작업과의 간접 충돌

이 문제의 원인은 종종 DB 자체가 아니라 애플리케이션의 커넥션 관리다. 예를 들어 트랜잭션을 시작한 뒤 결과 집합을 모두 읽지 않고 커넥션을 방치하거나, connection pool에서 autocommit을 원래 상태로 되돌리지 않거나, 배치 작업이 큰 범위를 하나의 트랜잭션으로 처리하는 경우가 있다.

운영 기준은 단순하다. 짧은 OLTP 트랜잭션은 빠르게 커밋하고, 긴 분석 쿼리는 replica나 별도 분석 계층으로 분리하며, 대량 변경은 작은 배치로 나누어 처리하는 것이 안전하다.

6. Crash recovery: 장애 후 InnoDB가 스스로 정리하는 과정

InnoDB의 crash recovery는 장애 후 데이터 파일과 로그를 일관된 상태로 되돌리는 과정이다. MySQL 프로세스가 비정상 종료되거나 OS가 재부팅되면, 디스크의 데이터 페이지 일부는 최신 커밋 상태보다 뒤처져 있을 수 있고, 반대로 커밋되지 않은 변경의 흔적이 일부 페이지에 남아 있을 수도 있다.

InnoDB는 이를 크게 두 단계로 정리한다.

  1. redo 적용: 커밋되었거나 디스크 페이지에 완전히 반영되지 않은 변경을 redo log를 이용해 다시 적용한다.
  2. undo rollback: 커밋되지 않은 트랜잭션의 변경을 undo log를 이용해 되돌린다.

이 구조를 이해하려면 buffer pool과 redo log의 관계를 봐야 한다. InnoDB는 데이터 페이지를 매번 즉시 디스크에 쓰지 않는다. 변경은 먼저 buffer pool의 페이지에 적용되고, 해당 변경을 재현할 수 있는 redo record가 redo log에 기록된다. 이후 checkpoint와 flush 정책에 따라 dirty page가 데이터 파일에 기록된다.

장애 시점에는 다음 상태가 섞여 있을 수 있다.

  • redo log에는 있지만 데이터 파일에는 아직 반영되지 않은 커밋 변경
  • 데이터 파일 일부에는 반영되었지만 트랜잭션은 커밋되지 않은 변경
  • 커밋 여부와 페이지 flush 순서가 서로 다르게 보이는 중간 상태

redo log와 undo log는 이 중간 상태를 정리하기 위한 안전장치다. 그래서 InnoDB는 장애 후 시작할 때 로그를 스캔하고 필요한 복구 작업을 수행한다. 운영자는 MySQL이 장애 후 재시작되는 동안 error log에서 InnoDB recovery 관련 메시지를 확인해야 한다.

예시는 다음과 같다.

# systemd 기반 서버 예시
journalctl -u mysqld --since "30 minutes ago" | grep -i innodb

# 일반 로그 파일 예시
sudo grep -i "InnoDB" /var/log/mysqld.log | tail -n 100

복구 시간이 길어지는 대표 원인은 dirty page 양, redo log 적용량, 미완료 대형 트랜잭션 rollback, 스토리지 I/O 성능 부족이다. 따라서 장애 복구 시간을 예측하려면 평상시 checkpoint age, redo log 사용량, dirty page 비율, 대량 트랜잭션 패턴을 함께 관찰해야 한다.

7. InnoDB가 MyISAM보다 운영 기본값에 적합한 이유

MyISAM은 구조가 단순하고 일부 읽기 위주 워크로드에서 빠르게 보일 수 있다. 그러나 기본 엔진으로는 치명적인 한계가 있다.

구분 InnoDB MyISAM
트랜잭션 지원 미지원
잠금 주로 row-level locking table-level locking
crash recovery redo/undo 기반 복구 제한적, 테이블 손상 복구 필요 가능
외래키 지원 미지원
MVCC 지원 미지원
운영 기본값 적합성 OLTP 기본값 특수 목적에 제한적으로 검토

테이블 단위 잠금은 쓰기 동시성이 높을수록 문제가 된다. 한 세션이 테이블을 갱신하는 동안 다른 세션의 쓰기 또는 일부 읽기가 대기할 수 있으며, 긴 작업 하나가 전체 테이블 접근을 지연시키는 상황이 발생한다. 반면 InnoDB도 잠금 경합이 없지는 않지만, 인덱스와 조건이 적절하면 필요한 레코드 또는 범위 중심으로 잠금을 좁힐 수 있다.

또한 비트랜잭션 엔진에서는 애플리케이션이 여러 SQL을 묶어 원자적으로 처리하기 어렵다. 장애 후 일부 변경만 남는 상태를 애플리케이션 보정 작업으로 복구해야 할 수 있다. 이는 데이터 신뢰성을 애플리케이션과 운영 절차에 과도하게 떠넘기는 설계다.

기존 시스템에서 MyISAM 테이블이 남아 있는지 확인하려면 다음 쿼리를 사용할 수 있다.

SELECT table_schema,
       table_name,
       engine,
       table_rows,
       ROUND((data_length + index_length) / 1024 / 1024, 2) AS size_mb
FROM information_schema.tables
WHERE table_schema NOT IN ('mysql', 'performance_schema', 'information_schema', 'sys')
  AND engine <> 'InnoDB'
ORDER BY size_mb DESC;

전환은 단순히 ALTER TABLE ... ENGINE=InnoDB로 끝나지 않을 수 있다. primary key 부재, 전문 검색 인덱스, 애플리케이션 잠금 가정, 디스크 공간, 복제 지연, DDL 잠금 영향을 함께 검토해야 한다.

CREATE TABLE legacy_table (
  id BIGINT PRIMARY KEY,
  note VARCHAR(100)
) ENGINE=MyISAM;

SELECT table_name, engine
FROM information_schema.tables
WHERE table_schema = DATABASE()
  AND table_name = 'legacy_table';

ALTER TABLE legacy_table ENGINE=InnoDB;

SELECT table_name, engine
FROM information_schema.tables
WHERE table_schema = DATABASE()
  AND table_name = 'legacy_table';

DROP TABLE legacy_table;

실행 결과(MySQL 8.0.46):

+--------------+--------+
| TABLE_NAME   | ENGINE |
+--------------+--------+
| legacy_table | MyISAM |
+--------------+--------+
+--------------+--------+
| TABLE_NAME   | ENGINE |
+--------------+--------+
| legacy_table | InnoDB |
+--------------+--------+

운영 중 대형 테이블을 변환할 때는 MySQL 버전의 online DDL 지원 범위, replica에서의 사전 검증, 백업, 롤백 전략, 변경 시간대를 반드시 확인해야 한다.

8. 운영자가 자주 오해하는 지점

8.1 InnoDB를 쓰면 모든 데이터 손실이 불가능하다는 오해

InnoDB는 강한 복구 구조를 제공하지만, 모든 상황에서 데이터 손실을 절대적으로 막는다는 뜻은 아니다. 지속성은 설정과 스토리지의 정직성에 의존한다. innodb_flush_log_at_trx_commit, sync_binlog, 파일 시스템, 디스크 캐시, 가상화 계층, 클라우드 스토리지의 flush 보장 수준이 모두 영향을 준다.

운영자가 “COMMIT 성공 후 어느 정도까지 손실을 허용할 것인가”를 정의하지 않고 성능만 보고 flush 관련 설정을 완화하면, 장애 후 최근 몇 초의 트랜잭션 손실 가능성을 뒤늦게 발견할 수 있다.

8.2 MVCC 때문에 SELECT는 절대 장애를 만들지 않는다는 오해

일반 consistent read는 잠금을 덜 만들지만, 긴 SELECT 트랜잭션은 undo purge를 지연시킬 수 있다. 또한 SELECT ... FOR UPDATE는 명백히 잠금을 잡는다. reporting query가 primary에서 장시간 실행되면 buffer pool을 오염시키고 I/O를 증가시키며 purge 지연을 만들 수 있다.

8.3 Row-level locking이 항상 한 행만 잠근다는 오해

InnoDB의 잠금은 인덱스 레코드에 기반한다. 적절한 인덱스가 없으면 더 많은 레코드나 범위가 스캔되고 잠금 대상이 넓어질 수 있다. REPEATABLE READ에서는 phantom 방지를 위해 next-key lock과 gap lock이 관여할 수 있다. 따라서 잠금 문제를 해결할 때는 항상 실행 계획을 확인해야 한다.

EXPLAIN FORMAT=TREE
UPDATE orders
SET status = 'PROCESSING'
WHERE status = 'READY'
ORDER BY created_at
LIMIT 100;

이 쿼리에 (status, created_at) 인덱스가 없다면 생각보다 넓은 범위를 읽고 잠글 수 있다.

8.4 Crash recovery를 백업 대체 수단으로 보는 오해

Crash recovery는 비정상 종료 직전의 데이터 파일을 일관된 상태로 복구하는 기능이다. 사용자가 실수로 데이터를 삭제했거나, 애플리케이션 버그가 잘못된 값을 커밋했거나, 랜섬웨어나 스토리지 손상이 발생한 경우를 해결하는 백업이 아니다. InnoDB의 crash recovery와 백업·PITR은 서로 다른 계층의 안전장치다.

9. Aurora MySQL에서의 해석 차이

Aurora MySQL은 MySQL 호환 SQL 계층을 제공하지만, 스토리지 아키텍처는 Community MySQL과 다르다. 일반 MySQL은 인스턴스 로컬 스토리지의 데이터 파일과 redo log를 중심으로 생각하는 반면, Aurora는 분산 스토리지 계층이 redo 중심으로 데이터를 관리하고 여러 AZ에 복제한다.

운영 관점의 차이는 다음과 같다.

  • 인스턴스 장애와 스토리지 장애의 경계가 일반 MySQL보다 분리되어 있다.
  • failover 시 새 writer가 기존 분산 스토리지 볼륨을 사용하므로, 전통적인 파일 복사 기반 복구와 다르게 동작한다.
  • 백업은 클러스터 볼륨의 지속적 백업과 point-in-time restore 기능을 중심으로 설계된다.
  • Performance Insights, CloudWatch 지표, Enhanced Monitoring을 함께 보아야 InnoDB 내부 대기와 클라우드 계층 병목을 구분할 수 있다.

그러나 InnoDB 이해가 불필요해지는 것은 아니다. Aurora MySQL에서도 트랜잭션 격리, row lock, undo, purge, deadlock, buffer pool, 실행 계획 문제는 여전히 운영자가 해석해야 한다. Aurora가 스토리지 내구성과 failover의 많은 부분을 관리해 주더라도, 애플리케이션 트랜잭션 설계와 쿼리 패턴이 잘못되면 동일하게 잠금 대기와 성능 저하가 발생한다.

Aurora에서 확인할 수 있는 일반적인 진단 축은 다음과 같다.

-- 긴 트랜잭션과 잠금 대기 확인
SELECT *
FROM information_schema.INNODB_TRX
ORDER BY trx_started;

-- 현재 대기 이벤트 관찰이 가능한 경우 Performance Schema 사용
SELECT event_name,
       COUNT_STAR,
       SUM_TIMER_WAIT
FROM performance_schema.events_waits_summary_global_by_event_name
WHERE event_name LIKE 'wait/io/%'
   OR event_name LIKE 'wait/lock/%'
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 20;

Aurora에서는 여기에 CloudWatch의 CPU, FreeableMemory, DatabaseConnections, Deadlocks, DMLLatency, CommitLatency, VolumeReadIOPs, VolumeWriteIOPs 같은 지표를 함께 본다. 일반 MySQL에서 로컬 디스크 I/O를 보던 습관을 그대로 적용하기보다, 분산 스토리지와 writer/reader 역할을 함께 해석해야 한다.

10. 실무 진단 절차

InnoDB를 기본 엔진으로 운영할 때 다음 절차는 장애와 성능 문제의 첫 진단에 유용하다.

10.1 엔진과 핵심 설정 확인

SHOW VARIABLES WHERE Variable_name IN (
  'default_storage_engine',
  'transaction_isolation',
  'innodb_flush_log_at_trx_commit',
  'sync_binlog',
  'innodb_buffer_pool_size',
  'innodb_redo_log_capacity'
);

실행 결과(MySQL 8.0.46):

+--------------------------------+-----------------+
| Variable_name                  | Value           |
+--------------------------------+-----------------+
| default_storage_engine         | InnoDB          |
| innodb_buffer_pool_size        | 67108864        |
| innodb_flush_log_at_trx_commit | 1               |
| innodb_redo_log_capacity       | 104857600       |
| sync_binlog                    | 1               |
| transaction_isolation          | REPEATABLE-READ |
+--------------------------------+-----------------+

MySQL 8.0.30 이전 버전에서는 redo log 크기 관련 변수가 innodb_log_file_size, innodb_log_files_in_group일 수 있다.

10.2 비-InnoDB 테이블 점검

SELECT table_schema, table_name, engine
FROM information_schema.tables
WHERE table_schema NOT IN ('mysql', 'performance_schema', 'information_schema', 'sys')
  AND table_type = 'BASE TABLE'
  AND engine <> 'InnoDB'
ORDER BY table_schema, table_name;

10.3 긴 트랜잭션과 잠금 대기 확인

MySQL 5.7 이하에서는 information_schema.INNODB_LOCK_WAITS를 사용하는 예전 진단 쿼리가 쓰였지만, 이 객체는 MySQL 8.0에서 제거되었다. MySQL 8.0 이상에서는 다음처럼 Performance Schema의 data_locks, data_lock_waits를 기준으로 잠금 대기를 확인한다.

SELECT r.ENGINE_TRANSACTION_ID AS waiting_trx,
       r.OBJECT_SCHEMA,
       r.OBJECT_NAME,
       r.INDEX_NAME,
       r.LOCK_TYPE,
       r.LOCK_MODE AS waiting_lock_mode,
       b.ENGINE_TRANSACTION_ID AS blocking_trx,
       b.LOCK_MODE AS blocking_lock_mode
FROM performance_schema.data_lock_waits w
JOIN performance_schema.data_locks r
  ON w.REQUESTING_ENGINE_LOCK_ID = r.ENGINE_LOCK_ID
JOIN performance_schema.data_locks b
  ON w.BLOCKING_ENGINE_LOCK_ID = b.ENGINE_LOCK_ID;

10.4 InnoDB 상태 스냅샷 확보

SHOW ENGINE INNODB STATUS\G

장애 시점에는 이 출력이 매우 중요하다. 다만 개인정보나 실제 쿼리 파라미터가 포함될 수 있으므로 외부 공유 전에는 반드시 마스킹해야 한다.

10.5 에러 로그와 재시작 시점 확인

# 예시: 최근 1시간 MySQL 로그에서 InnoDB와 recovery 관련 메시지 확인
journalctl -u mysqld --since "1 hour ago" | egrep -i 'innodb|recovery|crash|redo|undo'

컨테이너 환경에서는 다음과 같이 확인할 수 있다.

docker logs --since 1h mysql-container 2>&1 | egrep -i 'innodb|recovery|crash|redo|undo'

명령 예시는 운영 환경에 맞게 서비스명과 컨테이너명을 바꾸어 사용해야 한다.

11. 설정과 설계 판단 기준

InnoDB를 기본 엔진으로 쓸 때 모든 설정을 무조건 가장 강하게 두는 것이 항상 정답은 아니다. 중요한 것은 업무 요구사항과 장애 허용 범위를 명확히 하고, 그에 맞게 일관된 선택을 하는 것이다.

판단 항목 보수적 선택 완화 선택 시 고려사항
커밋 지속성 innodb_flush_log_at_trx_commit=1 장애 시 최근 트랜잭션 손실 가능성
binlog 지속성 sync_binlog=1 복제/PITR 일관성 영향
트랜잭션 길이 짧은 OLTP 트랜잭션 긴 트랜잭션은 undo/purge 지연 가능
격리 수준 기본 REPEATABLE READ 또는 요구에 따른 READ COMMITTED gap lock, phantom, 재현 가능한 읽기 요구사항 검토
대량 변경 작은 배치와 커밋 분할 undo/redo 폭증, replication lag, lock wait
보고서 조회 replica/분석계 분리 primary 부하, purge 지연, buffer pool 오염

특히 READ COMMITTED로 격리 수준을 낮추면 일부 gap lock 경합이 줄어드는 경우가 있지만, 애플리케이션이 REPEATABLE READ의 snapshot 일관성을 전제로 작성되어 있다면 결과가 달라질 수 있다. 격리 수준 변경은 성능 튜닝이 아니라 동시성 의미 변경으로 다루어야 한다.

12. 운영 체크리스트

InnoDB 기본 운영을 점검할 때는 다음 항목을 표준 체크리스트로 삼을 수 있다.

  • default_storage_engineInnoDB
  • 주요 변경 쿼리의 WHERE
  • innodb_flush_log_at_trx_commitsync_binlog
  • SHOW ENGINE INNODB STATUS

13. 결론

InnoDB가 MySQL의 기본 스토리지 엔진인 이유는 단순한 성능 우위가 아니다. InnoDB는 ACID 트랜잭션, MVCC, redo/undo 기반 crash recovery를 통해 현대 OLTP 시스템이 요구하는 데이터 신뢰성과 동시성을 제공한다. 운영자는 이 구조를 이해해야 잠금 대기, 긴 트랜잭션, 장애 후 복구 시간, 지속성 설정, Aurora MySQL의 차이를 올바르게 해석할 수 있다.

InnoDB를 기본으로 선택한다는 것은 데이터 변경을 “파일에 쓰는 작업”이 아니라 “트랜잭션 로그, 버전 관리, 잠금, 복구 절차가 결합된 일관성 시스템”으로 다루겠다는 뜻이다. 다음 단계에서는 InnoDB의 핵심 구성 요소인 buffer pool, redo log, undo log, checkpoint, purge를 더 세밀하게 나누어 살펴보면 장애와 성능 문제를 훨씬 정확하게 진단할 수 있다.