1. 개요
트랜잭션 격리 수준은 “여러 트랜잭션이 동시에 돌 때, 한 트랜잭션이 다른 트랜잭션의 작업을 얼마나 볼 수 있게 허용하느냐” 를 정하는 규칙이다. 이름이 어렵게 들리지만 막아주는 문제 세 가지(Dirty Read, Non-repeatable Read, Phantom Read)를 타임라인으로 보면 어렵지 않다.
2. 핵심 내용
2-1. Dirty Read — 커밋 안 된 값을 읽는다
| 시각 | Tx1 | Tx2 |
|---|---|---|
| t1 | UPDATE balance SET amount = 0 WHERE id = 1 (아직 커밋 안 함) | |
| t2 | SELECT amount FROM balance WHERE id = 1 → 0 을 읽음 | |
| t3 | ROLLBACK |
Tx1 이 롤백하면 amount 는 원래 값으로 되돌아가지만, Tx2 는 이미 존재한 적 없는 값(0)을 읽고 그걸 기준으로 다른 처리를 했을 수 있다. 이게 Dirty Read 다.
2-2. Non-repeatable Read — 같은 행을 다시 읽었더니 값이 바뀌었다
| 시각 | Tx1 | Tx2 |
|---|---|---|
| t1 | SELECT amount FROM balance WHERE id = 1 → 1000 | |
| t2 | UPDATE balance SET amount = 500 WHERE id = 1; COMMIT; | |
| t3 | SELECT amount FROM balance WHERE id = 1 → 500 |
Tx1 은 같은 트랜잭션 안에서 같은 행을 두 번 읽었는데 값이 다르다. 다른 트랜잭션이 그 사이 커밋했기 때문이다.
2-3. Phantom Read — 같은 조건인데 행 개수가 달라졌다
| 시각 | Tx1 | Tx2 |
|---|---|---|
| t1 | SELECT COUNT(*) FROM orders WHERE status = 'PENDING' → 5건 | |
| t2 | INSERT INTO orders (status) VALUES ('PENDING'); COMMIT; | |
| t3 | SELECT COUNT(*) FROM orders WHERE status = 'PENDING' → 6건 |
Non-repeatable Read 와 비슷해 보이지만, 대상은 “이미 있던 행의 값” 이 아니라 “조건에 맞는 행의 집합” 이다. 새로 나타난(또는 사라진) 행을 유령(Phantom)에 비유한다.
2-4. 4단계가 각각 뭘 막아주나
| 격리 수준 | Dirty Read | Non-repeatable Read | Phantom Read |
|---|---|---|---|
| Read Uncommitted | 발생 | 발생 | 발생 |
| Read Committed | 방지 | 발생 | 발생 |
| Repeatable Read | 방지 | 방지 | 발생(DB 마다 다름) |
| Serializable | 방지 | 방지 | 방지 |
- Read Uncommitted: 커밋 여부와 상관없이 다 보인다. 실무에서 거의 안 쓴다.
- Read Committed: 커밋된 값만 본다. PostgreSQL, Oracle, SQL Server 의 기본값이다. 같은 트랜잭션 안에서도 매번 조회 시점의 최신 커밋 값을 읽는다.
- Repeatable Read: 트랜잭션이 시작한 시점의 스냅샷을 트랜잭션 내내 유지한다. MySQL InnoDB 의 기본값이다. 다만 InnoDB 는 갭 락(Gap Lock) 덕분에 표준에서 정의한 Phantom Read 까지 상당 부분 막아준다.
- Serializable: 트랜잭션들을 마치 순서대로 하나씩 실행한 것과 같은 결과를 보장한다. 가장 안전하지만 동시성이 가장 낮다.
2-5. 락과 격리 수준은 다른 얘기다
여기서 자주 섞이는 게 있다. 격리 수준은 “무엇이 보이느냐” 를 정하는 규칙이고, 락은 “누가 먼저 자원을 잡고 남을 기다리게 하느냐” 를 정하는 메커니즘이다. 둘은 서로 다른 층위의 문제다.
예를 들어 재고 차감처럼 충돌이 뻔한 상황이라면 격리 수준을 올리는 것만으로는 부족해서 명시적으로 SELECT ... FOR UPDATE 로 행을 잠그는 Pessimistic Lock (비관적 락) 을 함께 쓴다. 그리고 여러 트랜잭션이 서로 다른 순서로 락을 잡으면 Deadlock (교착 상태) 이 발생할 수 있는데, 이건 격리 수준을 아무리 올려도 피할 수 없고 락을 잡는 순서를 설계로 맞춰야 풀리는 문제다. 격리 수준이 “읽기 일관성” 얘기라면, 락은 “쓰기 충돌” 얘기다.
3. 마무리
요약
- Dirty Read: 커밋 안 된 값을 읽는다.
- Non-repeatable Read: 같은 행을 두 번 읽었는데 값이 바뀐다.
- Phantom Read: 같은 조건인데 행의 개수가 달라진다.
- Read Uncommitted < Read Committed < Repeatable Read < Serializable 순으로 더 많은 이상 현상을 막는다.
- PostgreSQL 은 Read Committed, MySQL InnoDB 는 Repeatable Read 가 기본값이다.
- 격리 수준은 읽기 일관성, 락은 쓰기 충돌 제어라 서로 다른 문제다.
다음은 JOIN 정리 이다.