11장. 표준이 말하지 않은 것 — 스냅샷 격리

만화로 보는 요약 — 먼저 읽어보세요
표준 이름표와 실제 DB 구현은 다를 수 있다 → 스냅샷 격리는 각자 시작 시점의 사진을 보여줘 읽기와 쓰기를 덜 막는다 → 하지만 write skew는 진짜 직렬성이나 명시적 잠금이 필요하다.
면접 실전 질문: ① 스냅샷 격리가 읽기와 쓰기를 덜 막는 이유 ② write skew가 표준 세 유령과 다른 점 ③ SERIALIZABLE이라는 이름을 그대로 믿으면 위험한 이유
배경 — 이름표는 같은데, 문 뒤가 다르다
10장의 조합표는 아름다웠죠. 네 문, 세 유령, 깔끔한 격자. 그런데 그 표 아래에 제가 계속 경고를 달았습니다. “이건 표준의 정의일 뿐, 실제 DB는 다르다.” 이 장은 그 경고의 정체를 밝힙니다.
무슨 일이 벌어지냐면 — 실제 DB들은 REPEATABLE READ라는 같은 이름표를 달고도, 문 뒤에서
완전히 다른 방식으로 격리를 구현합니다. 그래서 표준 표만 믿고 코드를 짜면, 이름은
맞는데 동작이 딴판이라 발이 쑥 빠지는 거예요.
스토리 — 잠그지 않고 격리하는 법
10장에선 격리의 대가로 주로 잠금을 떠올렸죠. 그런데 잠그면 느립니다. 읽는 사람도 쓰는 사람을 기다려야 하니까. 여기서 누군가 기막힌 발상을 합니다.
“잠그지 말고, 각자에게 그 시점의 ‘사진(snapshot)‘을 보여주면 어떨까?”
트랜잭션이 시작할 때, DB가 그 순간의 데이터를 사진 한 장으로 찍어줍니다. 그 트랜잭션은 자기 사진만 봐요. 그동안 남이 값을 바꾸고 커밋해도, 내 사진은 안 변합니다. 이게 스냅샷 격리(Snapshot Isolation, SI) 예요. 읽는 사람은 자기 사진을 보므로 아무도 안 기다립니다. 쓰는 사람도 읽는 사람을 안 막고요. (이 “여러 버전의 사진”을 어떻게 유지하는지는 13장 MVCC에서 파헤칩니다.)
놀라운 건 성능만이 아니에요. 스냅샷을 쓰면 9장의 세 유령이 거의 다 사라집니다.
내 사진은 안 변하니 반복불가도 없고, 팬텀도 사실상 없어요. 그래서 PostgreSQL의
REPEATABLE READ는 사실 스냅샷 격리이고, 표준 정의보다 훨씬 강합니다(팬텀까지 막아요).
MySQL InnoDB의 REPEATABLE READ도 스냅샷 읽기에 잠금을 더해 팬텀을 상당 부분 막습니다.
“표준상 RR은 팬텀을 허용”인데 현실은 안 그런 거죠. 이름표는 같은데 문 뒤가 다르다는
게 이겁니다.
핵심 — 공짜 점심은 없다: Write Skew
여기까지 들으면 스냅샷 격리가 만능 같죠. 빠르고, 유령도 막고. 그런데 스냅샷 격리만의 고유한 함정이 하나 있어요. 표준 세 유령 목록엔 없던 새 유령, write skew(쓰기 편향) 입니다.
병원 응급실 규칙을 예로 들게요. “당직 의사는 항상 최소 1명” 이어야 합니다. 지금
앨리스와 밥, 두 명이 당직이에요. 둘 다 동시에 “퇴근할까?” 합니다. 두 세션(스냅샷 격리,
예: PostgreSQL REPEATABLE READ)에서 직접 재현해 볼게요.
-- [세션 A = 앨리스]
BEGIN;
SELECT COUNT(*) FROM doctor WHERE on_call = true; -- 2명 (나 말고 밥도 있네)-- [세션 B = 밥]
BEGIN;
SELECT COUNT(*) FROM doctor WHERE on_call = true; -- 2명 (나 말고 앨리스도 있네)-- [세션 A] 한 명 더 있으니 나는 퇴근
UPDATE doctor SET on_call = false WHERE id = 'alice'; COMMIT;
-- [세션 B] 나도 한 명 더 있으니 퇴근
UPDATE doctor SET on_call = false WHERE id = 'bob'; COMMIT;
-- 이제 확인하면?
SELECT COUNT(*) FROM doctor WHERE on_call = true; -- 0명! 규칙이 깨졌다각자 자기 사진에서는 “다른 한 명이 남아 있다”를 봤어요. 둘 다 규칙을 지켰다고 믿고 퇴근합니다. 결과는? 당직 0명.
포인트는 이겁니다. 앨리스와 밥은 서로 다른 행을 수정했어요(각자 자기 당직 상태만). 그래서 “같은 행을 동시에 쓰는” 충돌이 아니라 DB가 못 잡습니다. 하지만 두 트랜잭션이 함께 지켜야 할 규칙(당직 ≥ 1)을 각자 자기 사진만 보고 깨버린 거죠. 이게 write skew고, 스냅샷 격리로는 못 막습니다.
그럼 어떻게 막나요? 진짜 직렬성(= SERIALIZABLE 격리 수준)이 필요합니다. 그런데
여기도 함정이 있어요. Oracle의 SERIALIZABLE은 사실 스냅샷 격리라서 write skew를
못 막습니다. 반면 PostgreSQL은 9.1부터 진짜 직렬성(직렬성 스냅샷 격리, Serializable
Snapshot Isolation, SSI)을 구현해 write skew까지 잡아요. 표준을 믿은 대가죠.
실무 해법은 보통 이렇습니다. write skew가 걱정되는 로직이라면, 읽을 때부터 당직 행들을 명시적으로 잠급니다.
-- 집계가 아니라 '행'을 잠근다 (COUNT ... FOR UPDATE는 PostgreSQL에서 에러)
SELECT id FROM doctor WHERE on_call = true FOR UPDATE;
-- 잠근 뒤 인원수를 판단 → 남이 그 행을 못 바꾸니 직렬화된다(뒤 트랜잭션은 막히거나 재시도)단, FOR UPDATE는 이미 존재하는 행만 잠급니다. 그래서 이 예제(기존 당직 행이
바뀌는 경우)는 막지만, 새 행을 INSERT해서 규칙이 깨지는 종류는 못 막아요. 그럴 땐
격리 수준을 진짜 직렬성으로 올리는 게 답입니다. 정리하면 — 국소적 방어는 FOR UPDATE, 로직 전체가 얽히면 직렬성.
정리
- 실제 DB는 표준 조합표대로 안 움직인다. 같은 이름표, 다른 구현이 핵심.
- 스냅샷 격리(SI) = 트랜잭션 시작 시점의 “사진”만 본다. 읽기-쓰기가 서로 안 막아
빠르고, 반복불가·팬텀이 거의 사라진다(→ 13장 MVCC).
- PostgreSQL
REPEATABLE READ= SI(팬텀까지 막음). MySQL InnoDB RR도 팬텀 상당 부분 차단.
- PostgreSQL
- 대신 SI만의 새 유령 write skew: 두 트랜잭션이 서로 다른 행을 고쳐 공동 규칙을 깨는 것. 같은 행 충돌이 아니라 DB가 못 잡는다.
- write skew를 막으려면 진짜 직렬성(PostgreSQL SSI) 또는 명시적 잠금(
FOR UPDATE). 단 OracleSERIALIZABLE은 실은 SI라 write skew를 못 막는다 — 이름표를 믿지 말 것.
생각해볼 질문: 우리는 이제 격리가 무엇을 보장하는지(제3부)는 다 봤습니다. 그런데 정작 어떻게 그걸 해내는지는 안 봤어요. “잠근다”, “사진을 찍는다”고 했는데, 그 자물쇠와 사진은 실제로 어떻게 생겼을까요? 제4부, 질서를 지키는 장치들의 내부로 들어갑니다.