# 트랜잭션 격리 수준과 이상 현상 > **한 줄 요지:** 격리 수준은 "무엇을 보장하느냐"가 아니라 "어떤 이상 현상을 허용하느냐"의 다이얼이다. 기본은 READ COMMITTED로 깔고, 불변식이 걸린 소수 트랜잭션만 SERIALIZABLE/명시적 락으로 격상하되 반드시 재시도/충돌 처리를 함께 설계한다. **메타** - 분류: 백엔드 > DB & 스토리지 > 트랜잭션 격리 수준과 이상 현상 - 날짜: 2026-06-28 - 깊이: 실무 - 페르소나: 박정합(DBA·정합성), 이처리(백엔드·처리량), 최내부(엔진 내부·회의론), 김주니(주니어 질문자) --- ## 1. 배경 — 왜 이 논의가 필요한가 여러 트랜잭션이 같은 데이터를 동시에 건드리면, 완벽한 정합성을 위해선 트랜잭션을 한 줄로 세워 직렬 실행해야 한다. 하지만 그러면 동시 처리량이 사실상 1이 되어 대규모 서비스에선 쓸 수 없다. 그래서 DB는 "직렬로 실행한 것 같은 결과"를 동시 실행으로 흉내 내며, **격리 수준**으로 "어디까지 흉내 낼지"를 조절한다. 다이얼을 낮추면 빨라지는 대신 이상 현상이 새어 나온다. 대규모 시스템(결제·재고·예약)에서 이 선택을 잘못하면 데이터가 조용히 틀어진다. 특히 "이름은 같지만 DB마다 동작이 다른" 함정 때문에 문서를 보지 않으면 틀린 가정을 하게 된다. --- ## 2. 쟁점 요약 - **READ COMMITTED로 충분한가?** vs 어디까지 격리를 올려야 하나. - **이상 현상 3종(dirty/non-repeatable/phantom)의 정확한 차이**와 막는 메커니즘. - 표준 격리 수준 정의는 **락 기반 1990년대 모델**을 전제 — MVCC 시대엔 안 맞는 구석. - **같은 이름, 다른 동작**: PostgreSQL RR(=Snapshot Isolation)과 MySQL InnoDB RR은 다르다. - **Snapshot Isolation의 함정: write skew** — 표준 3현상에 없지만 실무에서 데이터를 깬다. - **SERIALIZABLE의 비용**: 직렬화 실패(40001) → 재시도 로직 필수. 전역 적용은 처리량을 죽인다. --- ## 3. 입장별 정리 ### 박정합 (DBA · 강한 격리 옹호) - 데이터가 틀리면 시스템은 존재 이유가 없다. 돈·재고·예약처럼 불변식이 걸린 트랜잭션은 SERIALIZABLE 또는 명시적 락(`SELECT … FOR UPDATE`)으로 반드시 보호. - 단, 전역 SERIALIZABLE은 과하다는 데 동의 — "기본 RC, 위험한 곳만 격상". - phantom을 막으려면 존재하지 않는 행까지 막아야 하므로 **범위 락**(gap/next-key lock)이 필요하다. ### 이처리 (백엔드 · 처리량 우선) - RC + 앱 레벨 멱등성 키 + 낙관적 락으로 대부분 해결된다. SERIALIZABLE은 처리량을 죽인다. - MySQL RR의 gap lock은 운영에서 **의외의 INSERT 데드락**을 만든다 → 기본을 RC로. - SERIALIZABLE을 켜면 앱이 40001을 받고 **트랜잭션 통째로 재시도**할 줄 알아야 한다. 재시도 없이 켜면 에러 폭발. ### 최내부 (엔진 내부 · 회의론) - 격리 수준은 "무엇을 보장"이 아니라 "어떤 이상을 허용"으로 정의된다. - **"우리 DB는 RU 지원"부터 의심**: PostgreSQL은 RU 요청을 RC로 승격(MVCC라 미커밋 버전을 보여줄 수 없음). - PostgreSQL RR = **Snapshot Isolation** → dirty/non-rep/phantom은 막지만 **write skew는 못 막는다**. - MVCC: UPDATE는 덮어쓰기가 아니라 새 버전 생성. 각 버전에 xmin/xmax. 트랜잭션은 스냅샷 기준 가시 버전만 읽음 → 락 없이 읽기 격리. ### 김주니 (질문자) - "non-repeatable read와 phantom read의 차이" 같은 전제를 드러내는 질문으로 토론의 기반 개념을 노출. - non-repeatable = **이미 읽은 그 행**의 값이 변함(UPDATE). phantom = **같은 조건의 결과 집합**에 행이 생기거나 사라짐(INSERT/DELETE). --- ## 4. 트레이드오프 표 | 격리 수준 | 막는 이상 현상 | 동시성/처리량 | 대가·주의 | 전형적 용도 | |---|---|---|---|---| | READ UNCOMMITTED | 거의 없음(dirty 허용) | 최고 | 커밋 안 된 값 읽음. PG는 RC로 승격 | 근사 통계, 거의 안 씀 | | READ COMMITTED | dirty | 높음 | 한 tx 내 두 읽기 다를 수 있음, lost update 위험 | 대부분 OLTP 기본값 | | REPEATABLE READ / SI | + non-repeatable, (PG는 phantom도) | 중간 | **write skew 미차단**, MySQL gap lock 데드락 | 리포트, 일관된 다중 읽기 | | SERIALIZABLE | 전부(write skew 포함) | 낮음 | 40001 직렬화 실패 → **재시도 필수** | 돈·재고·예약 등 불변식 | ### 이상 현상 ↔ 막는 메커니즘 | 이상 현상 | 원인 연산 | 막는 법 | |---|---|---| | Dirty read | 미커밋 읽기 | RC 이상(커밋된 버전만 노출) | | Non-repeatable read | UPDATE | RR/SI(스냅샷 고정) | | Phantom read | INSERT/DELETE | 범위 락(gap lock) 또는 SI/SERIALIZABLE | | Lost update | 같은 행 read-modify-write | `FOR UPDATE`(비관적) / 버전 컬럼(낙관적) | | Write skew | 다른 행 + 합쳐진 불변식 | SERIALIZABLE(SSI)만 | --- ## 5. 결론 / 의사결정 가이드 - **일반 CRUD·조회 위주** → READ COMMITTED(기본). 처리량 우선, 단일 문장 정합성으로 충분. - **한 tx에서 여러 번 읽고 일관성 필요(리포트·집계)** → REPEATABLE READ / SI. - **잔액·재고·좌석(read-modify-write)** → `SELECT … FOR UPDATE`(비관적) 또는 버전 컬럼(낙관적). RC에서도 안전. - **서로 다른 행이지만 합쳐서 불변식(on-call·중복 예약·한도 합계)** → SERIALIZABLE + 재시도. write skew는 SSI만 막는다. - **대량 동시 결제·주문** → RC + 멱등성 키 + 낙관적 락, 핫 로우만 국소 격상. 전역 SERIALIZABLE은 처리량을 죽인다. > **합의된 결론:** 기본은 READ COMMITTED. 불변식이 걸린 소수 트랜잭션만 SERIALIZABLE 또는 명시적 락으로 격상하고, 격상하면 **재시도/충돌 처리**를 반드시 함께 설계한다. ### 핵심 주의: 이름을 믿지 마라 - PostgreSQL RR = Snapshot Isolation(phantom까지 막음, write skew는 못 막음). - MySQL InnoDB RR = 스냅샷 읽기 + gap lock(대체로 phantom 막되 일부 케이스 다름). - Oracle엔 RR이 없고 SERIALIZABLE이 SI에 가깝다. - READ UNCOMMITTED는 PostgreSQL에서 RC로 승격된다. --- ## 6. 추가 학습 키워드 - **MVCC & VACUUM** — 버전 체인, dead tuple, autovacuum, TXID wraparound (격리의 청구서) - **SSI (Serializable Snapshot Isolation)** — 읽기-쓰기 의존성 그래프 기반 직렬화 보장 - **Gap / Next-key Lock** — MySQL InnoDB의 phantom 방지 범위 락, 데드락 단골 - **Lost Update vs Write Skew** — 같은 행 vs 다른 행, 막는 메커니즘이 다름 - **낙관적 vs 비관적 락** — 충돌 빈도에 따른 처리량 트레이드오프 - **2PL vs MVCC** — 락 기반 직렬화와 다중 버전, 표준 격리 정의의 역사적 배경