백엔드 데이터 인프라 38편 — MVCC 격리 수준과 동시성 함정

2026-05-17백엔드 데이터 인프라

백엔드 데이터 인프라 38편. MVCC 격리 수준 4가지와 Dirty Read·Non-Repeatable Read·Phantom Read·Serialization Anomaly 함정 풀어쓴 학습 노트.

📚 백엔드 데이터 인프라 · 38편 — MVCC 격리 수준과 동시성 함정

이 글은 백엔드 데이터 인프라 시리즈 70편 중 38편이에요. 37편 MVCC 에서 "기본 원리" 를 다뤘으니, 이번 38편은 그 위에 격리 수준 4개 + 동시성 함정 4종.

4가지 동시성 함정

격리 수준 알기 전 — "왜 격리가 필요한가" 의 함정들.

(1) Dirty Read — 미커밋 읽기

-- 트랜잭션 A
BEGIN;
UPDATE accounts SET balance = 50000 WHERE id = 1;
-- 아직 COMMIT X

-- 트랜잭션 B (낮은 격리)
SELECT balance FROM accounts WHERE id = 1;
-- 50000 (잘못된 미커밋 값)

-- 트랜잭션 A ROLLBACK
ROLLBACK;

B 가 본 50000 = 가짜. PG 는 Dirty Read 발생 X (가장 낮은 READ COMMITTED 도 막음).

(2) Non-Repeatable Read — 반복 읽기 불일치

-- 트랜잭션 A
BEGIN;
SELECT balance FROM accounts WHERE id = 1;   -- 100000

-- 트랜잭션 B (동시)
BEGIN;
UPDATE accounts SET balance = 80000 WHERE id = 1;
COMMIT;

-- 트랜잭션 A (다시 같은 SELECT)
SELECT balance FROM accounts WHERE id = 1;   -- 80000 (변경됨!)

같은 SELECT 가 — 두 번 다른 결과. READ COMMITTED 에서 가능. REPEATABLE READ 이상에서 방지.

(3) Phantom Read — 행 추가 보임

-- 트랜잭션 A
BEGIN;
SELECT COUNT(*) FROM users WHERE city = 'Seoul';   -- 100

-- 트랜잭션 B
INSERT INTO users (city, ...) VALUES ('Seoul', ...);
COMMIT;

-- 트랜잭션 A
SELECT COUNT(*) FROM users WHERE city = 'Seoul';   -- 101 (한 행 추가됨!)

같은 조건의 행 수가 — 두 번 다름. PG REPEATABLE READ 이상에서 방지 (다른 DB 와 다른 점).

(4) Serialization Anomaly — 직렬화 이상

가장 미묘. 동시 트랜잭션들의 결과가 — "어느 순서로 실행해도 안 나올 수 있는 상태".

-- 두 트랜잭션이 동시
-- A: SELECT 합계 → INSERT 새 행
-- B: SELECT 합계 → INSERT 새 행

-- 직렬 실행 시 = 한 행만 추가 가능 (제약)
-- 동시 실행 시 = 두 행 추가됨 (제약 위반)

SERIALIZABLE 만 방지.

격리 수준 4개 비교

수준 Dirty Non-Repeatable Phantom Serialization
READ UNCOMMITTED 발생 발생 발생 발생
READ COMMITTED (PG 기본) 방지 발생 발생 발생
REPEATABLE READ 방지 방지 PG는 방지 발생
SERIALIZABLE 방지 방지 방지 방지

PG 표 — READ COMMITTED 는 표준, REPEATABLE READ 는 "PG 가 표준보다 강함" (Phantom 방지), SERIALIZABLE 만 완전.

격리 수준 설정

-- 트랜잭션 단위
BEGIN ISOLATION LEVEL SERIALIZABLE;
-- ... 작업
COMMIT;

-- 또는 BEGIN 후
BEGIN;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- ...
COMMIT;

-- 세션 기본
SET default_transaction_isolation = 'repeatable read';

PG 기본 = READ COMMITTED. 99% 의 시나리오에 충분.

READ COMMITTED — 기본

SELECT 마다 새 스냅샷. 트랜잭션 안에서도 — 다른 트랜잭션 COMMIT 이 보임.

BEGIN;
SELECT balance FROM accounts;   -- 100
-- 다른 트랜잭션 UPDATE + COMMIT
SELECT balance FROM accounts;   -- 80 (변경 보임)
COMMIT;

장점 — 가장 높은 동시성. 단점 — Non-Repeatable Read 가능.

REPEATABLE READ — 트랜잭션 일관

트랜잭션 시작 시점 스냅샷 유지. 다른 트랜잭션 변경 안 보임.

BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT balance FROM accounts;   -- 100
-- 다른 트랜잭션 UPDATE
SELECT balance FROM accounts;   -- 여전히 100 (트랜잭션 일관)
COMMIT;

PG REPEATABLE READ = Phantom 도 방지 (표준 SQL 보다 강함).

함정 — 동시 UPDATE 에러

BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT balance FROM accounts WHERE id = 1;
-- 다른 트랜잭션이 UPDATE + COMMIT
UPDATE accounts SET balance = balance - 1000 WHERE id = 1;
-- ERROR: could not serialize access due to concurrent update

REPEATABLE READ 에서 "동시 변경 감지" = 명시적 에러. 앱에서 재시도 필요.

SERIALIZABLE — 완전한 직렬화

마치 "트랜잭션이 차례로 실행된 것처럼". 완벽한 일관성. 단 — 충돌 시 한쪽 ROLLBACK.

BEGIN ISOLATION LEVEL SERIALIZABLE;
-- 복잡한 비즈니스 로직
COMMIT;   -- 또는 ERROR: could not serialize ...

PG SERIALIZABLE = SSI (Serializable Snapshot Isolation). 다른 DB 의 "엄격한 락" 보다 효율적. 단 — 충돌 빈도 높으면 재시도 폭주.

Lost Update — 동시 수정 함정

-- 카운터 증가 (계좌 잔액 비슷)
-- 트랜잭션 A: 100 + 10 = 110
-- 트랜잭션 B: 100 + 20 = 120
-- 결과 = 110 또는 120 (둘 중 하나)
-- 의도 = 130 (둘 다 반영)

해결:

(1) 단순 SQL — UPDATE col = col + val

UPDATE counters SET val = val + 10;   -- A
UPDATE counters SET val = val + 20;   -- B
-- 결과 = 130 (UPDATE 안에서 read-modify-write)

PG 가 자동으로 — UPDATE 시점에 최신 값 읽어 갱신. 락 짧고 안전.

(2) FOR UPDATE — 비관적

BEGIN;
SELECT val FROM counters WHERE id = 1 FOR UPDATE;
-- val = 100
UPDATE counters SET val = 110 WHERE id = 1;
COMMIT;

확실. 락 비용.

(3) @Version — 낙관적

UPDATE counters SET val = 110, version = version + 1
WHERE id = 1 AND version = 5;
-- 행 0 = 다른 트랜잭션이 변경 → 재시도

26편 UPDATE 깊이 의 JPA @Version. 충돌 드문 시스템에 효율.

격리 수준 선택 룰

시나리오 추천
일반 웹 API READ COMMITTED (PG 기본)
일관된 보고서·통계 REPEATABLE READ
금융·돈·재고 SERIALIZABLE + 재시도
단순 카운터 증가 READ COMMITTED + col = col + val

99% 한국 백엔드 = READ COMMITTED. 금융 시스템·동시성 핵심만 SERIALIZABLE.

Spring JPA 격리 설정

@Transactional(isolation = Isolation.REPEATABLE_READ)
public void process() { ... }

@Transactional(isolation = Isolation.SERIALIZABLE)
public void transfer() { ... }

자바 백엔드 입문 43편 @Transactional 의 isolation 옵션.

재시도 패턴 — 모든 격리에서

@Transactional(isolation = Isolation.SERIALIZABLE)
@Retryable(retryFor = CannotSerializeException.class, maxAttempts = 3)
public void process() {
    // ...
}

SERIALIZABLE·REPEATABLE READ 충돌 시 — 자동 재시도. 운영 표준.

함정 5가지

(1) 격리 수준 모름

READ COMMITTED 기본이라 — Non-Repeatable Read 가능. 의도와 다른 결과 가능.

(2) SERIALIZABLE 남용

모든 트랜잭션 SERIALIZABLE = 재시도 폭주·성능 폭락.

(3) FOR UPDATE 누락

비관적 락 필요한 곳에 안 박으면 — Lost Update.

(4) @Version 충돌 안 처리

OptimisticLockException 무시 = 데이터 손실.

(5) 큰 트랜잭션 + 높은 격리

오랜 시간 + REPEATABLE READ = bloat 증가. autovacuum 영향.

🎯 격리 수준 선택

일반 API = READ COMMITTED. 보고서 = REPEATABLE READ. 금융·재고 = SERIALIZABLE + 재시도. 99% 한국 백엔드 = READ COMMITTED 가 정답.

한 줄 정리 — 격리 수준 4개. PG READ COMMITTED 기본 + REPEATABLE READ Phantom 도 방지 + SERIALIZABLE SSI. 동시성 함정 4종 = Dirty·Non-Repeatable·Phantom·Serialization. Lost Update = col = col + val 또는 FOR UPDATE 또는 @Version. 재시도 패턴 표준.

시험 직전 한 번 더 — 격리 수준 입문자가 매번 헷갈리는 것

  • 동시성 함정 4종 = Dirty·Non-Repeatable·Phantom·Serialization
  • PG Dirty Read 발생 X (READ COMMITTED 도 막음)
  • READ COMMITTED (PG 기본) = 각 SELECT 새 스냅샷
  • REPEATABLE READ = 트랜잭션 일관 + PG 는 Phantom 도 방지
  • SERIALIZABLE = SSI (Serializable Snapshot Isolation)
  • BEGIN ISOLATION LEVEL ...
  • SET TRANSACTION ISOLATION LEVEL ...
  • @Transactional(isolation = ...)
  • Lost Update 해결 3방법:
  • UPDATE col = col + val (안전·단순)
  • FOR UPDATE (비관적)
  • @Version (낙관적)
  • REPEATABLE READ + 동시 UPDATE = could not serialize
  • SERIALIZABLE 충돌 시 = could not serialize
  • @Retryable 표준 패턴
  • 일반 API = READ COMMITTED
  • 금융 = SERIALIZABLE + 재시도
  • SERIALIZABLE 남용 X
  • 큰 트랜잭션 + 높은 격리 = bloat
  • FOR UPDATE = 확실·비용
  • @Version = OptimisticLockException
  • 격리 수준 ≠ 락
  • MVCC 위에서 동작
  • 한국 백엔드 99% = READ COMMITTED

시리즈 다른 편

시리즈 다음 글

다음 글(39편)에서는 전문 검색 — to_tsvector·to_tsquery·GIN 인덱스.

공식 문서: PostgreSQL 18 — MVCC에서 더 자세한 사양을 확인할 수 있어요.

※ 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

답글 남기기

error: Content is protected !!