격리수준을 고르는 기준 — 잔고부터 좋아요 카운트까지
SERIALIZABLE을 기본값으로 쓰지 않는 이유
이론만 보면 항상 가장 엄격한 격리수준을 쓰는 게 안전해 보입니다. 그런데 거의 모든
프로덕션 DB의 기본값이 Read Committed인 데는 이유가 있습니다. PostgreSQL의 SSI(직렬화
가능 스냅샷 격리)는 트랜잭션끼리의 읽기-쓰기 의존성을 계속 추적하다가, 직렬 실행과
다른 결과가 나올 위험이 보이면 한쪽을 could not serialize access due to read/write dependencies 에러로 실패시킵니다. 이 추적 자체가 CPU와 메모리를 씁니다. 실측
마이크로벤치마크에서는 락 경합이 심한 워크로드에서 SERIALIZABLE이 Read Committed
대비 5~15% 정도 지연을 늘리는 것으로 보고됩니다 — 게다가 실패한 트랜잭션은 애플리케이션이
재시도해야 하므로, 재시도 로직 없이 SERIALIZABLE만 켜면 사용자에게 에러가 그대로
노출됩니다.
즉 SERIALIZABLE은 "더 안전하지만 손이 많이 가는" 선택지입니다. 성능 저하를 감수할 가치가 있는지, 그리고 애플리케이션이 직렬화 실패를 재시도로 흡수할 준비가 되어 있는지를 먼저 따져야 합니다.
데이터의 성격으로 판단하기
이 책에서 가장 실전에 가까운 질문은 "우리 서비스의 어떤 데이터가 어떤 격리수준을 요구하는가"입니다. 저는 실무에서 다음 기준으로 나눕니다.
| 데이터 종류 | 예시 | 권장 격리수준 | 이유 |
|---|---|---|---|
| 금전적 정합성이 절대적 | 계좌 잔고, 재고, 포인트 | Repeatable Read + 명시적 락, 또는 Serializable | 하나라도 어긋나면 실제 손실이 발생 |
| 집계·통계성 데이터 | 조회수, 좋아요 수, 랭킹 | Read Committed | 몇 초 오차가 나도 비즈니스에 영향 없음, 원자적 증감 연산으로 충분 |
| 사용자 세션·설정 | 로그인 상태, 알림 설정 | Read Committed | 충돌 가능성이 애초에 낮음 |
| 회계·정산 배치 | 월말 마감, 통계 리포트 | Serializable | 정확성이 성능보다 압도적으로 중요, 배치라 지연 허용 폭이 큼 |
이 표를 만든 뒤에도 남는 판단은 하나입니다 — 격리수준을 올리기 전에, 원자적
연산이나 명시적 락으로 더 싸게 해결되는지부터 확인하세요. 좋아요 카운트를 Serializable로
지킬 필요는 없습니다. UPDATE posts SET likes = likes + 1처럼 원자적 증감 문 하나로
Lost Update가 애초에 발생하지 않기 때문입니다. 반대로 계좌 이체는 여러 행을 함께
읽고 계산해야 하므로 원자적 연산만으로는 부족하고, 명시적 락이나 더 높은 격리수준이
필요합니다.
-- 좋아요 수: Read Committed로 충분하다. 원자적 연산이 Lost Update를 막는다
UPDATE posts SET likes = likes + 1 WHERE id = 42;
-- 계좌 이체: 여러 행을 함께 읽고 계산하므로 명시적 락이 필요하다
BEGIN;
SELECT balance FROM accounts WHERE id IN (1, 2) ORDER BY id FOR UPDATE;
-- 두 계좌 모두 잠근 뒤에야 계산과 UPDATE를 진행한다
UPDATE accounts SET balance = balance - 1000 WHERE id = 1;
UPDATE accounts SET balance = balance + 1000 WHERE id = 2;
COMMIT;애플리케이션 레벨 락도 답이 될 수 있다
격리수준을 올리는 대신, 애플리케이션 레벨에서 낙관적 락(optimistic lock)으로 해결하는
방법도 있습니다. version 컬럼을 두고 갱신할 때마다 값을 확인하는 방식입니다.
UPDATE accounts SET balance = 600000, version = version + 1
WHERE id = 1 AND version = 3;
-- 영향받은 행이 0건이면 다른 트랜잭션이 먼저 갱신한 것 — 재시도이 방식은 DB의 격리수준을 그대로 Read Committed에 둔 채로 Lost Update를 막을 수
있어서, 충돌이 드물게만 일어나는 데이터(사용자가 자기 프로필을 수정하는 경우 등)에
잘 맞습니다. 반대로 충돌이 자주 나는 데이터(인기 상품 재고)에는 재시도가 계속
실패해서 사용자 경험이 나빠지므로, 비관적 락(FOR UPDATE)이 더 적합합니다.
결정을 내리기 전 마지막 질문
새 기능을 설계할 때 저는 이 순서로 묻습니다.
- 여러 트랜잭션이 같은 데이터를 정말 동시에 건드릴 가능성이 있는가? (없다면 이 장의 논의 자체가 불필요합니다.)
- 있다면, 원자적 연산(
x = x + 1형태) 하나로 해결되는가? - 안 된다면, 잠글 행이 미리 정해지는가? 정해진다면 명시적 락(
FOR UPDATE)과 락 순서 통일로 해결합니다. - 잠글 행이 동적이거나 충돌이 드물다면 낙관적 락을 검토합니다.
- 위 전부가 부족하다고 판단되는, 정말 정합성이 최우선인 배치성 작업에만 Serializable을 씁니다.
Serializable부터 시작해서 필요하면 낮추는 방식보다, Read Committed에서 시작해서 필요한 만큼만 강화하는 이 순서가 실무에서 훨씬 적은 사고로 이어집니다. 격리수준은 기본값이 아니라 데이터마다 따로 매기는 선택지라는 것, 이게 이 책 전체에서 가장 남기고 싶었던 문장입니다.