다른 트랜잭션이 아직 커밋하지 않은 변경을 읽어 버리는 현상.
무엇이 문제인가
T1: UPDATE balance = 0 (아직 커밋 안 함)
T2: SELECT balance → 0 ← 커밋 안 된 값을 읽었다
T1: ROLLBACK ← 사실 0이 아니었다
T2 는 '존재한 적 없는 값'을 보고 판단을 내린 셈이다
이 판단으로 메일을 보냈거나 다른 테이블을 갱신했다면 되돌릴 방법이 없다.
형제 현상 — Non-repeatable Read
T1: SELECT balance FROM account WHERE id='A' → 10000
T2: UPDATE account SET balance=5000 WHERE id='A'; COMMIT;
T1: SELECT balance FROM account WHERE id='A' → 5000 ← 같은 행인데 값이 달라졌다
같은 행을 두 번 읽었는데 값이 다른 현상이다. Dirty Read와 달리 커밋된 값을 읽었는데도 문제가 된다 — 한 트랜잭션 안에서 같은 질문에 다른 답이 나오면 합계와 상세가 어긋나는 식의 모순이 생긴다.
어디까지 막히나
- READ UNCOMMITTED — Dirty Read 가능
- READ COMMITTED — Dirty Read 막음 · Non-repeatable Read 가능
- REPEATABLE READ — 둘 다 막음
PostgreSQL은 READ UNCOMMITTED를 요청해도 READ COMMITTED로 동작한다 — MVCC 구조상 커밋 안 된 버전을 보여줄 방법 자체가 없기 때문이다. 즉 PostgreSQL에서 Dirty Read는 애초에 일어나지 않는다.
왜 MVCC가 이걸 공짜로 푸나
각 트랜잭션은 '자기 시점의 스냅샷'을 본다
- 커밋되지 않은 버전은 애초에 스냅샷에 포함되지 않는다
- 읽기가 쓰기를 기다릴 필요도 없다
락 기반(2PL)에서는 읽기가 쓰기 락을 기다려야 막을 수 있었던 문제를, MVCC는 기다림 없이 해결한다.
면접 함정
- ❌ "Dirty Read와 Non-repeatable Read는 같은 말" → 전자는 커밋 안 된 값, 후자는 커밋된 값이 바뀐 것이다.
- ❌ "READ UNCOMMITTED가 제일 빠르니 조회에 쓰자" → PostgreSQL에서는 효과가 없고, 다른 엔진에서도 얻는 성능 대비 위험이 크다.
PostgreSQL에서는 왜 아예 없나
BEGIN ISOLATION LEVEL READ UNCOMMITTED;
SHOW transaction_isolation; -- read committed ← 요청해도 승격된다
MVCC 구조상 커밋되지 않은 버전은 어떤 스냅샷에도 포함되지 않는다. 보여줄 방법 자체가 없어서 READ UNCOMMITTED를 구현할 필요가 없었다. "PostgreSQL은 dirty read가 없다"는 관용구가 여기서 나온다.
MySQL에서 재현해 보기
-- [세션 1]
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
BEGIN;
SELECT balance FROM account WHERE id='A'; -- 세션 2가 커밋 안 한 값이 보인다
-- [세션 2]
BEGIN;
UPDATE account SET balance = 0 WHERE id='A'; -- 커밋하지 않는다
Non-repeatable Read가 실제로 아픈 곳
-- 리포트 하나가 여러 번 조회할 때
BEGIN;
SELECT sum(amount) FROM orders WHERE status='PAID'; -- 합계 1,000,000
-- ... 그 사이 누가 주문을 취소하고 커밋 ...
SELECT count(*) FROM orders WHERE status='PAID'; -- 건수는 새 값
COMMIT;
-- 합계와 건수가 서로 다른 시점의 데이터 → 리포트가 앞뒤가 안 맞는다
합계와 상세가 어긋나는 리포트가 이 현상의 전형적 피해다. 한 트랜잭션 안에서 여러 번 조회한다면 REPEATABLE READ가 필요하다는 신호다.