Chapter 7

트랜잭션

  • 7.1 애매한 트랜잭션의 개념
  • 7.2 약한 격리 수준
  • 7.3 직렬성

트랜잭션이 없으면 개발자가 동시성 문제를 전부 직접 방어해야 하는데, 그건 비현실적이야. 약한 격리 수준이 어떤 버그를 허용하는지 모르면, 프로덕션에서 특정 타이밍에만 터지는 동시성 버그에 당할 수밖에 없거든.

ACID의 의미부터 짚어보면, 현실에서 DB마다 다르게 구현돼. 원자성은 "전부 아니면 전무" — 오류 시 어보트하고 원래 상태로 돌려서 재시도를 안전하게 만들어주지. 일관성은 사실 DB의 속성이 아니라 애플리케이션의 속성이야 — ACID에 C를 넣은 건 좀 억지라는 뉘앙스. 격리성은 동시 트랜잭션이 서로 방해 안 하는 건데, 교과서적인 직렬성은 성능 비용이 너무 커서 대부분 더 약한 격리를 써. 내구성은 커밋된 데이터가 유실되지 않는다는 보장이고.

약한 격리 수준의 세계를 파고들면 미묘한 문제들이 드러나. 커밋 후 읽기는 더티 읽기와 더티 쓰기만 방지하는 가장 기본적인 수준이야. 스냅숏 격리는 각 트랜잭션이 시작 시점의 일관된 스냅숏을 보게 해서 비반복 읽기를 막아주지 — MVCC로 각 행의 여러 버전을 유지하고 트랜잭션 ID로 가시성을 결정하는 거야. 하지만 여기서도 갱신 손실이 터져 — 두 트랜잭션이 같은 값을 읽고 수정하면 나중 놈이 먼저 놈의 변경을 덮어쓰는 거. 원자적 쓰기, 명시적 잠금, 자동 감지, compare-and-set 같은 방어책이 있지만 복제 환경에서는 더 까다로워.

가장 교활한 건 쓰기 스큐야. 두 트랜잭션이 각각 다른 객체를 수정하지만 동일한 조건을 기반으로 결정을 내릴 때 발생해. 병원에서 최소 한 명이 당직이어야 하는데 두 의사가 동시에 "다른 의사가 있으니 나는 빠져도 되겠지"라고 판단하면 아무도 안 남는 거지. 이 패턴에서 SELECT 결과가 쓰기에 의해 무효화되는 현상을 팬텀이라 해.

결국 직렬성이 유일한 완전한 해답이야. 세 가지 구현이 있는데 — 순차 실행(한 번에 하나씩, RAM이 싸져서 가능해짐, 단일 CPU 제한), 2단계 잠금(30년 주류였지만 잠금 경합과 데드락으로 느림), SSI(2008년, 낙관적 접근 — 일단 실행하고 커밋 시 위반 확인, PostgreSQL 9.1부터 기본). 각각 다른 트레이드오프가 있지만, SSI가 현대적이고 대부분의 상황에서 나은 선택이야.


정리

7장 읽고 기억할 거:

  1. 약한 격리 수준은 미묘한 버그를 만든다. 커밋 후 읽기, 스냅숏 격리 — 각각이 방지하는 이상 현상과 허용하는 이상 현상을 정확히 알아야 한다
  2. 갱신 손실, 쓰기 스큐, 팬텀 — 동시성 문제의 삼총사. 특히 쓰기 스큐는 서로 다른 객체를 수정하면서도 발생해서 감지가 어렵다
  3. 직렬성의 세 구현은 각각 다른 트레이드오프. 순차 실행은 단순하지만 처리량 제한, 2PL은 검증됐지만 느리고 데드락, SSI는 현대적이고 낙관적이지만 어보트율이 변수