DataBase Isolation Levels
Atomicity / Consistency / Isolation / Durability
트랜잭션이 안전하게 동작하기 위해 지켜야 할 네 가지 성질을 묶어 ACID라고 부른다. 이 글에서 다루는 격리 수준(Isolation Level)은 이 중 Isolation을 얼마나 엄격하게 보장할지 선택하는 옵션이다.
- Atomicity(원자성): 트랜잭션 안의 여러 작업은 전부 반영되거나 전부 반영되지 않거나 둘 중 하나로만 끝난다. 중간에 실패하면 이미 실행된 작업까지 모두 롤백되어, 계좌 이체에서 출금만 성공하고 입금은 실패하는 것 같은 상황이 생기지 않는다.
- Consistency(일관성): 트랜잭션 전후로 데이터베이스는 항상 정의된 제약조건(PK, FK, unique, check 제약 등)을 만족하는 상태로 남는다. 트랜잭션이 이 제약을 어기면 커밋되지 못하고 롤백된다. Atomicity/Isolation/Durability가 지켜지는 한 자연히 따라오는 결과에 가까워, 나머지 셋과 달리 DBMS가 별도의 메커니즘으로 직접 구현하는 성질은 아니다.
- Isolation(격리성): 동시에 실행되는 여러 트랜잭션이 서로의 중간(커밋 전) 상태에 영향을 주지 않고, 마치 순서대로 하나씩 실행된 것처럼 결과가 보장된다. 아래에서 다룰 Read Uncommitted ~ Serializable 네 단계가 이 Isolation을 얼마나 강하게 보장할지를 정하는 옵션이며, 강할수록 Dirty/Non-Repeatable/Phantom Read 같은 이상현상은 줄어들지만 그만큼 동시성은 떨어진다.
- Durability(지속성): 커밋이 완료된 트랜잭션의 결과는 이후 시스템이 크래시나 전원 차단을 겪어도 사라지지 않는다. 보통 커밋 시점에 변경 내용을 WAL(Write-Ahead Log)로 디스크에 먼저 기록해두고, 장애 복구 시 이 로그를 재생(replay)해 데이터를 복구하는 방식으로 구현한다.
여러 트랜잭션이 동시에 같은 데이터를 건드리면 격리 수준(Isolation Level)에 따라 서로 다른 이상현상(anomaly)을 겪을 수 있다. SQL 표준은 이 이상현상을 막는 정도에 따라 네 단계의 격리 수준을 정의하고 있고, 각 수준은 Lock(락을 걸어 접근 자체를 막는 방식)이나 MVCC(Multi-Version Concurrency Control, 데이터의 여러 버전을 두고 스냅샷을 읽는 방식) 중 하나로 구현된다. 같은 격리 수준이라도 어떤 방식으로 구현하느냐에 따라 동시성과 성능이 크게 달라진다.
Dirty Read / Non Repeatable / Phantom Read
Dirty Read
다른 트랜잭션이 아직 커밋하지 않은 값을 읽어버리는 현상이다. 그 트랜잭션이 롤백되면 존재한 적 없는 데이터를 읽은 셈이 된다.
1
2
3
4
5
6
7
8
9
10
T1 T2
------------------------------------------------------------------
UPDATE accounts
SET balance = 0
WHERE id = 1; -- 커밋 전, balance는 아직 1000 -> 0으로 변경만 된 상태
SELECT balance FROM accounts
WHERE id = 1;
-- 0을 읽음 (아직 커밋되지 않은 값!)
ROLLBACK; -- balance는 다시 1000으로 원복
-- 하지만 T2는 이미 존재한 적 없는 값 0을 사용해버림
Non-Repeatable Read
같은 트랜잭션 안에서 같은 로우를 두 번 조회했는데, 그 사이 다른 트랜잭션이 값을 바꾸고 커밋해서 결과가 달라지는 현상이다.
1
2
3
4
5
6
7
8
9
10
T1 T2
------------------------------------------------------------------
SELECT balance FROM accounts
WHERE id = 1; -- 1000
UPDATE accounts
SET balance = 500
WHERE id = 1;
COMMIT;
SELECT balance FROM accounts
WHERE id = 1; -- 500 (같은 로우인데 값이 바뀜)
Phantom Read
같은 트랜잭션 안에서 같은 조건으로 두 번 조회했는데, 그 사이 다른 트랜잭션이 조건에 맞는 로우를 삽입/삭제하고 커밋해서 집합(row 개수) 이 달라지는 현상이다.
1
2
3
4
5
6
7
8
9
T1 T2
------------------------------------------------------------------
SELECT * FROM accounts
WHERE balance > 100; -- 5 rows
INSERT INTO accounts(id, balance)
VALUES (99, 200);
COMMIT;
SELECT * FROM accounts
WHERE balance > 100; -- 6 rows (조건에 맞는 로우가 유령처럼 나타남)
Non-Repeatable Read는 이미 읽은 로우의 값이 바뀌는 문제이고, Phantom Read는 조회 결과의 범위(row 집합) 자체가 바뀌는 문제라는 점이 다르다.
Read Uncommitted
- 읽기에 아무런 락도 걸지 않고, 커밋 여부와 상관없이 최신 값을 그대로 읽는다.
- 세 가지 이상현상을 전혀 막지 못하는 가장 낮은 격리 수준이라 실무에서는 거의 쓰이지 않는다.
Read Committed
Dirty Read만 방지하고, Non-Repeatable Read와 Phantom Read는 허용하는 수준이다.
- Lock 기반: 읽을 때 shared lock을 걸지만, 그 문장(statement)이 끝나면 바로 락을 해제한다. 커밋된 값만 읽을 수는 있지만, 락을 오래 들고 있지 않으므로 다음 조회 시점에는 다른 트랜잭션이 이미 값을 바꿔놓을 수 있다.
- MVCC 기반 (PostgreSQL, Oracle, MySQL InnoDB도 옵션으로 지원): 락 없이 매 문장마다 새로운 스냅샷(read view) 을 떠서 그 시점에 커밋되어 있는 데이터만 읽는다. 읽기가 쓰기를 막지 않고 쓰기가 읽기를 막지 않으므로(readers don’t block writers, writers don’t block readers) Lock 기반보다 동시성이 훨씬 좋다.
Repeatable Read
Non-Repeatable Read까지 방지하는 수준이다. 표준상 Phantom Read는 허용 대상이지만, 실제 구현체마다 차이가 크다.
- Lock 기반: 트랜잭션이 읽은 로우에 건 shared lock을 문장이 끝나도 풀지 않고 트랜잭션이 끝날 때까지 유지한다. 같은 로우를 다시 읽어도 다른 트랜잭션이 그 로우를 바꾸지 못했으므로 항상 같은 값이 보장되지만, 락을 오래 들고 있는 만큼 동시성은 떨어진다.
- MVCC 기반 (MySQL InnoDB 기본 격리 수준): 문장마다가 아니라 트랜잭션이 시작되는 시점에 스냅샷을 한 번만 떠서, 트랜잭션이 끝날 때까지 그 스냅샷만 바라본다. 락 없이도 반복 읽기 일관성이 보장된다.
- InnoDB는 여기에 더해 next-key lock / gap lock 을 함께 사용하기 때문에, 표준 정의상 RR은 Phantom Read를 허용해도 되지만 InnoDB의 RR은 실질적으로 대부분의 Phantom Read까지 막아준다는 점이 유명한 특이 케이스다.
Serializable
Phantom Read까지 완전히 방지해, 모든 트랜잭션이 마치 순서대로 하나씩 실행된 것처럼 동작하는 가장 엄격한 수준이다.
- Lock 기반: 조회 조건에 해당하는 범위 자체를 잠그는 next-key lock / gap lock / key-range lock 을 사용해, 인덱스 상의 특정 로우뿐 아니라 로우와 로우 사이의 빈 구간까지 잠가서 아직 존재하지 않는 “미래에 삽입될 로우”까지 막는다. 동시성은 가장 낮다.
- MVCC 기반: PostgreSQL의 SSI(Serializable Snapshot Isolation) 처럼 락으로 미리 막는 대신, 트랜잭션들을 우선 동시에 진행시키고 직렬 실행으로는 나올 수 없는 충돌이 감지되면 커밋 시점에 한쪽을 abort 시키는 방식이다. 락 기반이 비관적(pessimistic)이라면 이쪽은 낙관적(optimistic)에 가까운 접근이다.
정리
| 격리 수준 | Dirty Read | Non-Repeatable Read | Phantom Read | 대표 구현 방식 |
|---|---|---|---|---|
| Read Uncommitted | 발생 | 발생 | 발생 | 락 없음 |
| Read Committed | 방지 | 발생 | 발생 | Lock(문장 단위 shared lock) 또는 MVCC(문장마다 스냅샷) |
| Repeatable Read | 방지 | 방지 | 발생(표준), InnoDB는 대부분 방지 | Lock(트랜잭션 내내 shared lock 유지) 또는 MVCC(트랜잭션 시작 시 스냅샷 1회) |
| Serializable | 방지 | 방지 | 방지 | Lock(next-key/gap/key-range lock) 또는 MVCC(SSI, 커밋 시점 충돌 감지) |
결국 격리 수준이 올라갈수록 막을 수 있는 이상현상은 늘어나지만, Lock 기반은 그만큼 락을 더 오래·더 넓게 들고 있어야 하고, MVCC 기반은 스냅샷을 얼마나 자주 새로 뜨는지(문장마다 vs 트랜잭션 시작 시 1회)로 같은 효과를 락 없이 달성한다는 차이가 있다.