백엔드 면접 용어 사전
데이터베이스

Phantom Read

같은 조건의 범위 조회 결과에 없던 행이 나타나는 이상현상. 범위 락 또는 SERIALIZABLE로 막는다.

같은 조건으로 범위 조회를 두 번 했을 때, 처음엔 없던 행이 나타나는 이상현상. "유령처럼 나타난다"는 뜻이다.

어떻게 일어나는가

T1: SELECT COUNT(*) FROM orders WHERE amount > 1000;   → 5건
T2: INSERT INTO orders (amount) VALUES (5000); COMMIT;
T1: SELECT COUNT(*) FROM orders WHERE amount > 1000;   → 6건   ← 유령 행

같은 트랜잭션 안에서 같은 질문을 했는데 답이 달라졌다.

왜 행 락으로 못 막나 — 핵심

Non-Repeatable Read는 이미 읽은 행을 잠그면 막을 수 있다. 그런데 팬텀은 아직 존재하지 않는 행이 문제다.

없는 행을 어떻게 미리 잠그는가?

불가능하다. 그래서 범위 자체를 잠가야 한다.

두 가지 해법

① 갭 락 / 넥스트 키 락 (InnoDB)

인덱스 레코드 사이의 빈 공간(gap) 을 잠근다.

인덱스: … [900] ──갭── [1200] ──갭── [1500] …
                  ↑
        amount > 1000 조건이면 이 갭도 잠근다
        → amount=1100 INSERT 시도가 대기한다

MySQL의 REPEATABLE READ가 팬텀을 실질적으로 막는 이유다. 대가는 데드락 증가 — 넓은 범위를 잠그니 충돌이 잦아진다.

② SERIALIZABLE

읽기에도 공유 락을 걸어 사실상 직렬 실행처럼 만든다. 확실하지만 동시성이 크게 떨어진다.

PostgreSQL은 다르게 푼다

PostgreSQL은 갭 락이 없다. 대신 SERIALIZABLE에서 SSI(Serializable Snapshot Isolation) 를 쓴다 — 일단 낙관적으로 진행하고, 커밋 시점에 직렬 가능성이 깨졌다고 판단되면 트랜잭션을 실패시킨다.

그래서 PostgreSQL에서 SERIALIZABLE을 쓰면 재시도 로직이 필수다.

실무에서는

격리수준을 올리기보다 문제가 되는 지점만 다르게 다룬다.

-- 예약 중복을 막고 싶다면 격리수준 대신 유니크 제약
ALTER TABLE reservation ADD UNIQUE (room_id, date);
-- 두 번째 INSERT 는 무조건 실패한다. 격리수준과 무관하게 확실하다

DB의 제약조건은 어떤 격리수준에서도 뚫리지 않는다. 가장 튼튼한 방어선이다.

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — 데이터베이스 — 인덱스·트랜잭션·격리수준·N+1