트랜잭션 격리 수준이 올라감에 따라 동시성 문제를 해결하는 방법
격리 수준이 올라갈수록 MVCC 스냅샷의 유지 범위가 넓어지고, 락의 범위와 강도가 강화됨으로써 더 많은 이상 현상을 방지합니다.
전체 구조
READ UNCOMMITTED → READ COMMITTED → REPEATABLE READ → SERIALIZABLE
(제한 없음) (MVCC 추가) (스냅샷 고정) (락 강화)
격리 수준이 올라갈수록:
1. MVCC 스냅샷 유지 기간이 길어지고
2. 락의 범위가 넓어지고
3. 락의 종류가 추가됨
단계별 방지 메커니즘 (InnoDB 기준)
1단계: READ UNCOMMITTED → READ COMMITTED
추가되는 메커니즘: MVCC 스냅샷 도입
[READ UNCOMMITTED — 아무런 보호 없음]
트랜잭션A: UPDATE price = 50000 (미커밋)
트랜잭션B: SELECT price → 50000 읽음 ← Dirty Read X
→ 현재 데이터를 그대로 읽음
→ 커밋 여부와 관계없이 최신 값을 보여줌
[READ COMMITTED — MVCC 스냅샷 도입]
트랜잭션A: UPDATE price = 50000 (미커밋)
트랜잭션B: SELECT price → ?
InnoDB 내부:
테이블: price = 50000 (트랜잭션A가 수정, 미커밋)
Undo Log: price = 45000 (이전 버전)
트랜잭션B: "A는 아직 커밋 안 됐으니 Undo Log에서 읽자"
→ price = 45000 O Dirty Read 방지
→ 매 SELECT마다 "현재 커밋된 데이터"의 스냅샷을 생성
→ 커밋되지 않은 변경은 Undo Log의 이전 버전으로 우회
[방지 원리]
SELECT 실행 시:
1. 해당 행을 수정한 트랜잭션이 있는지 확인
2. 수정한 트랜잭션이 커밋되지 않았으면
3. Undo Log에서 커밋된 버전을 찾아서 반환
→ Dirty Read 방지 O
→ 단, 매번 새 스냅샷이므로 Non-Repeatable Read는 여전히 발생
[Non-Repeatable Read가 발생하는 이유]
트랜잭션B: SELECT price → 스냅샷① → 45000
트랜잭션A: UPDATE price = 39000 + COMMIT
트랜잭션B: SELECT price → 스냅샷② → 39000 ← 새 스냅샷에서 A의 커밋 반영
→ 스냅샷을 매번 새로 생성하므로
→ 커밋된 변경이 다음 SELECT에 반영됨
2단계: READ COMMITTED → REPEATABLE READ
추가되는 메커니즘: 스냅샷 고정 + 갭 락
[READ COMMITTED — 매번 새 스냅샷]
트랜잭션B 시작
SELECT → 스냅샷① 생성 → 45000
(트랜잭션A: UPDATE 39000 + COMMIT)
SELECT → 스냅샷② 생성 → 39000 ← Non-Repeatable Read X
[REPEATABLE READ — 스냅샷 고정]
트랜잭션B 시작
SELECT → 스냅샷① 생성 → 45000
(트랜잭션A: UPDATE 39000 + COMMIT)
SELECT → 스냅샷① 재사용 → 45000 O Non-Repeatable Read 방지
→ 첫 SELECT 시 스냅샷을 생성하고
→ 트랜잭션이 끝날 때까지 같은 스냅샷을 유지
→ 다른 트랜잭션의 커밋이 보이지 않음
[스냅샷 비교]
READ COMMITTED:
SELECT① SELECT② SELECT③
↓ ↓ ↓
스냅샷① 스냅샷② 스냅샷③ ← 매번 새로 생성
REPEATABLE READ:
SELECT① SELECT② SELECT③
↓ ↓ ↓
스냅샷① 스냅샷① 스냅샷① ← 계속 재사용
갭 락 추가 (InnoDB 고유)
[잠금 읽기에서의 팬텀 방지]
REPEATABLE READ에서 SELECT ... FOR UPDATE 실행 시:
price 인덱스: [5000] ─(gap)─ [10000] ─(gap)─ [20000] ─(gap)─ [30000]
SELECT * FROM orders WHERE price > 10000 FOR UPDATE;
READ COMMITTED:
→ 10000, 20000, 30000 레코드 락만
→ (10000, 20000) 사이에 INSERT 가능 → 팬텀 발생
REPEATABLE READ (InnoDB):
→ 레코드 락 + 갭 락 = 넥스트 키 락
→ (10000, +∞) 전체에 갭 락
→ 이 범위에 INSERT 불가 → 팬텀 방지 O
READ COMMITTED의 락 범위:
[5000] [10000] [20000] [30000]
← 레코드만 잠금
REPEATABLE READ의 락 범위 (InnoDB):
[5000] [10000] [20000] [30000] [+∞)
││ ← 레코드 + 갭 전체 잠금
3단계: REPEATABLE READ → SERIALIZABLE
추가되는 메커니즘: 모든 SELECT에 자동으로 공유 락
[REPEATABLE READ]
일반 SELECT: MVCC 스냅샷 (락 없음)
FOR UPDATE: 넥스트 키 락
→ 일반 SELECT는 락이 없으므로 쓰기와 동시 실행 가능
[SERIALIZABLE]
일반 SELECT: 자동으로 SELECT ... LOCK IN SHARE MODE로 변환
→ 공유 락(S-Lock) 획득
FOR UPDATE: 넥스트 키 락
→ 모든 읽기에 공유 락이 걸림
→ 읽기 중에 다른 트랜잭션이 수정/삽입 불가
[SERIALIZABLE의 동작]
트랜잭션A: SELECT * FROM orders WHERE price > 10000
→ 자동으로 공유 락 획득 (읽기 락)
트랜잭션B: INSERT INTO orders (price) VALUES (15000)
→ 대기 ⏳ (A의 공유 락과 충돌)
트랜잭션B: UPDATE orders SET price = 50000 WHERE id = 1
→ 대기 ⏳ (A의 공유 락과 충돌)
트랜잭션A: COMMIT (공유 락 해제)
트랜잭션B: 이제 실행
→ 읽기와 쓰기가 동시에 불가능
→ 사실상 순차 실행과 같은 결과
[REPEATABLE READ vs SERIALIZABLE]
REPEATABLE READ:
읽기 (일반 SELECT) ──→ 락 없음 (MVCC)
읽기 (FOR UPDATE) ──→ 배타 락 + 갭 락
쓰기 ──→ 배타 락 + 갭 락
→ 일반 SELECT와 쓰기가 동시에 가능 O
SERIALIZABLE:
읽기 (모든 SELECT) ──→ 공유 락 + 갭 락 (자동)
쓰기 ──→ 배타 락 + 갭 락
→ 읽기와 쓰기가 동시에 불가능 X
→ 공유 락 vs 배타 락 충돌로 대기 발생
전체 비교 요약
| 격리 수준 | 추가되는 메커니즘 | 방지하는 현상 |
|---|---|---|
| READ UNCOMMITTED | 없음 | 없음 |
| READ COMMITTED | + MVCC 스냅샷 (매 SELECT) | + Dirty Read |
| REPEATABLE READ | + 스냅샷 고정 + 갭 락 (InnoDB) | + Non-Repeatable Read (+ Phantom) |
| SERIALIZABLE | + 모든 SELECT에 공유 락 | + Phantom Read (완전 보장) |
락 범위 시각화
[READ UNCOMMITTED]
아무 락 없음, 스냅샷 없음
→ 모든 이상 현상 발생 가능
[READ COMMITTED]
MVCC: 매 SELECT마다 새 스냅샷
락: 쓰기 시 레코드 락만
[5000] [10000] [20000] [30000]
← 수정하는 행만 잠금
[REPEATABLE READ — InnoDB]
MVCC: 트랜잭션 첫 SELECT 시 스냅샷 고정
락: 레코드 락 + 갭 락 (잠금 읽기/쓰기 시)
[5000] [10000] [20000] [30000] [+∞)
││ ← 레코드 + 갭 잠금
[SERIALIZABLE]
MVCC: 스냅샷 고정
락: 모든 SELECT에 공유 락 + 갭 락 자동 적용
[5000] [10000] [20000] [30000] [+∞)
││ ← 읽기만 해도 전체 잠금
성능 영향
READ UNCOMMITTED:
읽기: 락 X 스냅샷 X → 가장 빠름
쓰기: 제한 없음
READ COMMITTED:
읽기: 락 X 스냅샷 O (매번 생성) → 빠름
쓰기: 레코드 락만 → 동시성 높음
REPEATABLE READ:
읽기: 락 X 스냅샷 O (고정) → 빠름
쓰기: 레코드 락 + 갭 락 → 동시성 중간
Undo Log: 트랜잭션 끝까지 유지 → 메모리 소비
SERIALIZABLE:
읽기: 공유 락 O → 느림 (쓰기와 충돌)
쓰기: 배타 락 + 갭 락 → 동시성 최저
→ 사실상 순차 실행
면접 포인트
- 격리 수준이 올라갈수록 MVCC 스냅샷의 유지 기간이 길어지고 + 락의 범위와 종류가 추가되는 방식으로 이상 현상을 방지합니다.
- READ COMMITTED → REPEATABLE READ의 핵심 차이는 스냅샷을 매번 새로 생성하느냐 vs 고정하느냐이며, REPEATABLE READ → SERIALIZABLE의 핵심 차이는 일반 SELECT에도 공유 락을 자동 적용하느냐입니다.
- InnoDB의 REPEATABLE READ가 특별한 이유는 스냅샷 고정에 더해 갭 락(넥스트 키 락)까지 사용하여, SQL 표준에서 SERIALIZABLE에서나 방지하는 팬텀 리드까지 방지하기 때문입니다.