앞선 포스트에서 MySQL의 락 메커니즘을 살펴봤습니다. 락은 쓰기 충돌을 막는 수단이지만, 동시에 실행 중인 트랜잭션들이 서로의 변경 내용을 얼마나 볼 수 있는지는 별도의 개념인 격리 수준(Isolation Level)으로 제어합니다.
이번 포스트에서는 트랜잭션 격리 수준 4가지와, InnoDB가 이를 구현하는 핵심 메커니즘인 MVCC를 살펴봅니다.
1. 이상 현상 (Anomaly)
격리 수준은 동시성과 일관성 사이의 트레이드오프입니다. 격리를 느슨하게 할수록 동시성은 높아지지만, 다음과 같은 이상 현상이 발생할 수 있습니다.
더티 리드 (Dirty Read)
아직 커밋되지 않은 다른 트랜잭션의 변경 내용을 읽는 현상입니다.
트랜잭션 A: salary를 5000 → 8000으로 UPDATE (미커밋)
트랜잭션 B: salary 조회 → 8000을 읽음
트랜잭션 A: ROLLBACK
트랜잭션 B: 존재한 적 없는 값을 읽은 셈
반복 불가능 읽기 (Non-Repeatable Read)
같은 트랜잭션 내에서 같은 행을 두 번 읽었을 때 값이 달라지는 현상입니다.
트랜잭션 A: salary 조회 → 5000
트랜잭션 B: salary를 5000 → 8000으로 UPDATE하고 COMMIT
트랜잭션 A: salary 재조회 → 8000 (값이 바뀜)
팬텀 리드 (Phantom Read)
같은 트랜잭션 내에서 같은 조건으로 범위 쿼리를 두 번 실행했을 때 결과 행 수가 달라지는 현상입니다.
트랜잭션 A: salary > 5000인 직원 조회 → 3명
트랜잭션 B: salary = 7000인 직원 INSERT하고 COMMIT
트랜잭션 A: 같은 조건 재조회 → 4명 (새로운 행이 나타남)

2. 격리 수준
SQL 표준(ANSI/ISO)은 격리 수준을 4단계로 정의하고, 각 단계에서 허용되는 이상 현상을 명시합니다. MySQL InnoDB의 기본값은 REPEATABLE READ입니다.
| 격리 수준 | 더티 리드 | 반복 불가능 읽기 | 팬텀 리드 |
| READ UNCOMMITTED | 발생 | 발생 | 발생 |
| READ COMMITTED | 방지 | 발생 | 발생 |
| REPEATABLE READ | 방지 | 방지 | 발생 (InnoDB는 방지) |
| SERIALIZABLE | 방지 | 방지 | 방지 |
READ UNCOMMITTED
커밋 여부와 무관하게 다른 트랜잭션의 변경 내용을 즉시 읽습니다. 더티 리드가 발생하므로 실무에서는 사용하지 않습니다.
READ COMMITTED
커밋된 데이터만 읽습니다. 더티 리드는 방지되지만, SQL 문이 실행될 때마다 최신 커밋 기준으로 읽기 때문에 반복 불가능 읽기가 발생합니다. Oracle, PostgreSQL의 기본 격리 수준입니다.
REPEATABLE READ
MySQL InnoDB의 기본 격리 수준입니다. 트랜잭션이 시작될 때 스냅샷을 찍고, 이후에는 항상 그 스냅샷 기준으로 읽습니다. 다른 트랜잭션이 커밋을 해도 현재 트랜잭션에서는 보이지 않습니다.
ANSI 표준상 REPEATABLE READ는 팬텀 리드를 허용하지만, InnoDB는 넥스트 키 락을 통해 팬텀 리드까지 방지합니다. 자세한 내용은 아래에서 다룹니다.
SERIALIZABLE
가장 엄격한 격리 수준입니다. 모든 SELECT에 자동으로 FOR SHARE가 추가되어, 읽기 자체가 다른 트랜잭션의 쓰기를 차단합니다. 완전한 직렬화가 보장되지만 동시성이 크게 떨어져 실무에서는 거의 사용하지 않습니다.
3. MVCC — 잠금 없는 읽기의 비결
MVCC(Multi-Version Concurrency Control, 다중 버전 동시성 제어)는 InnoDB가 격리 수준을 구현하는 핵심 메커니즘으로, 데이터의 여러 버전을 동시에 유지하는 방식으로 동작합니다.
쓰기 트랜잭션이 행을 변경할 때 기존 버전을 Undo 로그에 보관합니다. 읽기 트랜잭션은 락을 걸지 않고 자신의 스냅샷 시점에 맞는 버전을 Undo 로그에서 찾아 읽습니다. 덕분에 읽기와 쓰기가 서로를 차단하지 않습니다. 이것이 InnoDB가 높은 동시성을 유지할 수 있는 이유입니다.

스냅샷 읽기 vs 현재 읽기
MVCC가 적용되는 방식은 읽기 종류에 따라 달라집니다.
스냅샷 읽기 (Snapshot Read) — 일반 SELECT입니다. 락을 걸지 않고 Undo 로그에서 스냅샷 시점의 버전을 읽습니다.
SELECT * FROM employees WHERE id = 1;
현재 읽기 (Current Read) — FOR UPDATE, FOR SHARE, 그리고 INSERT/UPDATE/DELETE입니다. Undo 로그가 아닌 최신 커밋된 데이터를 읽으며, 동시에 레코드에 락을 겁니다.
SELECT * FROM employees WHERE id = 1 FOR UPDATE; -- X-Lock
SELECT * FROM employees WHERE id = 1 FOR SHARE; -- S-Lock
UPDATE employees SET salary = 8000 WHERE id = 1; -- X-Lock

MVCC와 격리 수준의 관계
같은 MVCC 메커니즘이지만, 스냅샷을 언제 찍느냐에 따라 격리 수준이 달라집니다.
| 격리 수준 | 스냅샷 기준 시점 |
| READ COMMITTED | SQL 문이 실행될 때마다 새 스냅샷 |
| REPEATABLE READ | 트랜잭션 시작 시 스냅샷, 이후 고정 |
4. REPEATABLE READ와 넥스트 키 락
스냅샷 읽기만으로는 팬텀 리드를 완전히 막을 수 없습니다. 일반 SELECT는 스냅샷 기준으로 읽지만, 현재 읽기는 최신 데이터를 직접 읽기 때문입니다.
-- [1] 트랜잭션 A
SELECT * FROM employees WHERE salary > 5000;
-- 결과: 3건 (스냅샷 읽기)
-- [2] 트랜잭션 B
INSERT INTO employees (name, salary) VALUES ('Eve', 7000);
COMMIT;
-- [3] 트랜잭션 A — 같은 조건이지만 현재 읽기
SELECT * FROM employees WHERE salary > 5000 FOR UPDATE;
-- 결과: 4건 → 팬텀 리드 발생
InnoDB는 이 문제를 넥스트 키 락으로 해결합니다. FOR UPDATE나 UPDATE/DELETE처럼 현재 읽기가 발생하면, 조건에 해당하는 인덱스 범위 전체에 넥스트 키 락을 겁니다. 레코드와 레코드 사이의 갭까지 잠그기 때문에 다른 트랜잭션이 그 구간에 INSERT하는 것 자체를 차단합니다.
salary > 5000 조건이고 인덱스에 5000, 7000, 9000이 있다면, 잠기는 구간은 다음과 같습니다:
(5000, 7000] → 7000 레코드 + 앞 갭 잠금
(7000, 9000] → 9000 레코드 + 앞 갭 잠금
(9000, +∞) → 테이블 끝까지 갭 잠금
이 상태에서 다른 트랜잭션이 salary = 6000을 INSERT하려 하면 (5000, 7000] 구간의 갭 락에 의해 블로킹됩니다.

결과적으로 InnoDB의 REPEATABLE READ는 두 가지를 조합해 팬텀 리드를 방지합니다.
- 스냅샷 읽기: 일반 SELECT에서 다른 트랜잭션의 커밋이 보이지 않도록 합니다.
- 넥스트 키 락: 현재 읽기(
FOR UPDATE등)에서 새로운 행이 끼어드는 것을 차단합니다.
5. 왜 MySQL 기본값이 REPEATABLE READ인가?
MySQL 리플리케이션의 역사적 배경과 관련이 있습니다.
MySQL이 바이너리 로그를 STATEMENT 형식으로 기록하던 시절, READ COMMITTED에서는 문제가 있었습니다. 같은 SQL이라도 실행 시점의 최신 커밋 기준으로 읽기 때문에, 소스와 레플리카에서 동일한 SQL을 실행해도 서로 다른 행에 적용될 수 있었습니다. REPEATABLE READ는 스냅샷을 트랜잭션 시작 시점에 고정하기 때문에 이 문제를 우회할 수 있었고, 그래서 기본값으로 채택되었습니다.
현재는 ROW 기반 바이너리 로그가 일반화되었지만, 기본값은 그대로 유지되고 있습니다.
정리
InnoDB가 높은 동시성과 일관성을 동시에 달성하는 방식을 한 문장으로 표현하면 이렇습니다. 읽기는 MVCC로, 쓰기는 락으로 보호한다.
MVCC는 데이터의 여러 버전을 Undo 로그에 유지해 읽기가 쓰기를 차단하지 않도록 합니다. 격리 수준은 스냅샷을 언제 찍느냐로 구현되고, 넥스트 키 락은 스냅샷 읽기만으로 막을 수 없는 현재 읽기에서의 팬텀 리드까지 보완합니다.
'Data > MySQL' 카테고리의 다른 글
| [MySQL] B-Tree 인덱스 완전 해부 — 구조부터 가용성까지 (1) | 2026.06.07 |
|---|---|
| [MySQL] 트랜잭션과 잠금 1편 — 락 메커니즘 (0) | 2026.06.07 |
| [MySQL] 아키텍처 2편 — InnoDB 스토리지 엔진 (0) | 2026.06.07 |
| [MySQL] 아키텍처 1편 — 엔진 아키텍처 (0) | 2026.06.07 |