MySQL 트랜잭션 격리 수준: InnoDB는 이상 현상을 어떻게 막을까

격리 수준(Isolation Level)은 동시에 실행되는 트랜잭션이 서로에게 어디까지 영향을 줄 수 있는지 정하는 설정입니다. 격리 수준을 높이면 일관성이 좋아지는 대신 동시성이 떨어지므로, 격리 수준을 정하는 일은 곧 일관성과 동시성 사이의 트레이드오프를 정하는 일입니다.
이 글은 MySQL의 기본 스토리지 엔진인 InnoDB를 기준으로 설명합니다.
먼저 알아둘 이상 현상
격리 수준은 “어떤 이상 현상을 막는가”로 구분하므로, 이상 현상부터 정리합니다.
| 이상 현상 | 설명 | 원인이 되는 작업 |
|---|---|---|
| Dirty Read | 다른 트랜잭션이 아직 커밋하지 않은 값을 읽습니다 | 커밋되지 않은 UPDATE |
| Non-Repeatable Read | 같은 row를 다시 읽었더니 값이 바뀌어 있습니다 | 커밋된 UPDATE / DELETE |
| Phantom Read | 같은 범위를 다시 조회했더니 없던 row가 나타납니다 | 커밋된 INSERT |
READ UNCOMMITTED
트랜잭션이 커밋했는지와 관계없이, 읽는 시점의 row 값을 그대로 읽습니다.
- Dirty Read가 발생합니다. 커밋되지 않은 값을 읽었는데 그 트랜잭션이 롤백되면, 한 번도 존재한 적 없는 값을 읽은 셈이 됩니다.
- Non-Repeatable Read와 Phantom Read도 모두 발생합니다.
- 쓰기는 락으로 서로 막기 때문에, 같은 row를 두 트랜잭션이 동시에 수정할 수는 없습니다.
정합성을 보장하지 못하는 가장 낮은 격리 수준이라 실무에서는 거의 사용하지 않습니다.
READ COMMITTED
커밋된 데이터만 읽습니다. InnoDB는 SELECT를 실행할 때마다 새 스냅샷을 만듭니다.
- Dirty Read를 막습니다. 항상 가장 최근에 커밋된 데이터를 읽습니다.
- Non-Repeatable Read가 발생합니다. 스냅샷이 매번 새로 만들어지므로, 두 번의 조회 사이에 다른 트랜잭션이 UPDATE하고 커밋하면 바뀐 값이 보입니다.
- Phantom Read가 발생합니다. 다른 트랜잭션이 INSERT하고 커밋하면 새로운 row가 보일 수 있습니다.
- Gap Lock을 (외래 키와 중복 키 검사를 제외하면) 사용하지 않기 때문에 쓰기 동시성이 좋습니다.
한 트랜잭션 안에서 같은 데이터를 여러 번 읽을 때 결과가 계속 달라질 수 있다는 점을 염두에 둬야 합니다.
REPEATABLE READ
MySQL의 기본 격리 수준입니다. 한 트랜잭션 안에서 같은 row를 여러 번 읽어도 같은 값이 나오도록 보장합니다.
- 트랜잭션에서 처음 SELECT를 실행할 때 스냅샷을 만들고, 이후 SELECT는 계속 그 스냅샷을 읽습니다.
- 스냅샷을 만든 뒤에 다른 트랜잭션이 커밋한 내용은 보이지 않습니다.
- 일반 SELECT는 락을 걸지 않으므로, 읽기와 쓰기가 서로를 막지 않습니다.
- 반면 UPDATE, DELETE와 잠금 읽기(
FOR SHARE,FOR UPDATE)는 스냅샷이 아니라 가장 최근에 커밋된 데이터를 기준으로 락을 걸고 동작합니다.
읽기에 락을 걸지 않으면서도 트랜잭션 안의 일관성을 보장하기 때문에, 일관성과 동시성 사이의 균형이 좋은 편입니다.
InnoDB의 REPEATABLE READ에서 Phantom Read는 발생할까?
SQL 표준에서는 REPEATABLE READ에서 Phantom Read가 발생할 수 있다고 정의합니다. 하지만 InnoDB에서는 대부분의 경우 발생하지 않습니다.
- 일반 SELECT는 같은 스냅샷을 계속 읽으므로, 다른 트랜잭션이 INSERT한 row가 보이지 않습니다.
- 잠금 읽기는 Next-Key Lock(레코드 락 + Gap Lock)으로 범위 사이의 INSERT 자체를 막습니다.
다만 스냅샷 읽기와 최신 데이터 기준의 쓰기를 섞으면 Phantom Read가 발생할 수 있습니다.
- 트랜잭션 A가
SELECT ... WHERE age > 20을 실행해 3건을 읽습니다. - 트랜잭션 B가
age = 25인 row를 INSERT하고 커밋합니다. - 트랜잭션 A가
UPDATE ... WHERE age > 20을 실행하면, 스냅샷에는 없던 B의 row까지 포함해 4건이 수정됩니다. - 이후 트랜잭션 A가 다시 SELECT하면, 자신이 수정한 row이므로 4건이 보입니다.
SERIALIZABLE
가장 엄격한 격리 수준입니다.
- 명시적인 트랜잭션 안의 SELECT는 자동으로
FOR SHARE(S Lock)가 붙습니다.- S Lock과 X Lock은 서로를 막으므로, 읽고 있는 row는 다른 트랜잭션이 수정할 수 없고 수정 중인 row는 읽을 수 없습니다.
- S Lock끼리는 호환되므로 여러 트랜잭션이 같은 row를 함께 읽을 수는 있습니다.
- autocommit 상태에서 트랜잭션 밖에서 실행하는 단건 SELECT는 락 없이 스냅샷을 읽습니다.
- Dirty Read, Non-Repeatable Read, Phantom Read를 모두 막아 일관성이 가장 높습니다.
- 읽기에도 락을 걸기 때문에 동시성이 떨어지고 데드락이 늘어납니다.
격리 수준별 이상 현상 정리
| 격리 수준 | Dirty Read | Non-Repeatable Read | Phantom Read |
|---|---|---|---|
| READ UNCOMMITTED | 발생 | 발생 | 발생 |
| READ COMMITTED | 방지 | 발생 | 발생 |
| REPEATABLE READ | 방지 | 방지 | 발생 (InnoDB는 대부분 방지) |
| SERIALIZABLE | 방지 | 방지 | 방지 |