격리 수준 MVCC 인덱스 심화 확장 📝 문제풀이
◀ 심화 CH 17 📋 목차 심화 CH 19 ▶
🗄️ 심화 CHAPTER 18 · 5년차 트랙

동시에 수천 명이 같은 테이블을 건드릴 때, DB는 어떻게 버틸까?

CRUD는 이미 익숙할 거예요. 이번 장은 "동시성·정합성·성능"의 진짜 밑바닥을 팝니다. 격리 수준이 만드는 이상현상, MVCC가 읽기와 쓰기를 떼어놓는 원리, 락과 데드락, 인덱스가 타고 안 타는 경계, 그리고 복제·샤딩으로 확장하는 법 — 장애 대응과 시니어 면접의 단골이에요.

🎯 이 장을 끝내면
🧱
격리 수준
격리 수준과 세 가지 이상현상
ANSI 4단계 — 격리를 올릴수록 안전하지만 동시성은 떨어져요.

트랜잭션의 I(Isolation)는 "동시에 도는 트랜잭션이 서로를 얼마나 볼 수 있는가"를 정하는 다이얼이에요. 격리를 느슨하게 하면 동시성(처리량)은 오르지만 이상한 값이 보이고, 빡빡하게 하면 정합성은 좋아지지만 대기·경합이 늘어요. 먼저 막고 싶은 세 가지 이상현상부터 봅시다.

이상현상무슨 일
Dirty Read다른 트랜잭션이 아직 커밋 안 한(롤백될 수도 있는) 값을 읽음
Non-repeatable Read같은 행을 두 번 읽었는데 사이에 남이 UPDATE·커밋해서 값이 달라짐
Phantom Read같은 조건으로 두 번 조회했는데 사이에 남이 INSERT·커밋해서 행 개수가 달라짐

ANSI SQL 표준은 격리 수준을 4단계로 정의하고, 각 단계가 어떤 이상현상을 허용/차단하는지로 규정해요.

격리 수준DirtyNon-repeatablePhantom
READ UNCOMMITTED❌ 허용❌ 허용❌ 허용
READ COMMITTED✅ 차단❌ 허용❌ 허용
REPEATABLE READ✅ 차단✅ 차단❌ 허용*
SERIALIZABLE✅ 차단✅ 차단✅ 차단
🖼️ 그림으로 보기 — 격리 수준 × 이상현상 허용 매트릭스
격리 수준 Dirty Read Non-repeatable Read Phantom READ UNCOMMITTED 허용 허용 허용 READ COMMITTED 차단 허용 허용 REPEATABLE READ 차단 차단 ✕ * 허용(표준) SERIALIZABLE 차단 차단 차단 아래로 갈수록 안전 ↑ · 동시성 ↓ ✓ 차단(안전) · ✕ 허용(이상현상 발생) * RR의 Phantom은 표준상 허용 — MySQL(갭 락)·PostgreSQL(스냅샷)은 실무상 상당 부분 차단
⚠️
기본 격리 수준은 DB마다 달라요 — 이걸 모르면 이식하다 사고 납니다. MySQL(InnoDB)의 기본은 REPEATABLE READ, PostgreSQL·Oracle·SQL Server의 기본은 READ COMMITTED예요. 같은 애플리케이션 코드라도 MySQL에선 트랜잭션 내내 스냅샷이 고정되고, PostgreSQL에선 매 문장마다 최신 커밋을 보게 됩니다.
👻 표(*) 각주 — RR에서 Phantom은 정말 허용될까 ANSI 표준상 REPEATABLE READ는 Phantom을 허용해요. 하지만 MySQL InnoDB는 RR에서도 넥스트키 락(gap lock + record lock)으로 잠금 읽기(FOR UPDATE) 시 Phantom을 상당 부분 막습니다. PostgreSQL의 REPEATABLE READ는 아예 스냅샷 격리라 순수 SELECT에선 Phantom이 안 보여요. 그래서 "이론상 허용" ≠ "이 DB에서 발생"이에요. Phantom을 표준적으로 확실히 막는 최소 수준은 여전히 SERIALIZABLE입니다.
🔀
MVCC
읽기가 쓰기를 막지 않는 마법
Multi-Version Concurrency Control — 버전을 여러 개 들고 산다.

옛날 방식은 "읽으려면 읽기 락, 쓰려면 쓰기 락"이라 읽기와 쓰기가 서로를 막았어요. MVCC는 데이터를 덮어쓰는 대신 여러 버전(version)을 유지해서, 각 트랜잭션이 자기 시점의 스냅샷을 읽게 합니다. 그 결과 읽기는 쓰기를 막지 않고, 쓰기도 읽기를 막지 않아요. 쓰기끼리만 충돌하죠.

📸
스냅샷 확보
트랜잭션 시작 시점 버전 기준
📖
읽기
락 없이 내 버전만 읽음
✍️
쓰기
새 버전 생성·구버전은 undo로
🖼️ 그림으로 보기 — MVCC — 읽기가 쓰기를 막지 않는다
트랜잭션 T1 먼저 시작 · 읽기만 트랜잭션 T2 나중에 UPDATE 버전 v1 (과거) T1이 보는 스냅샷 undo로 재구성 버전 v2 (최신) T2가 만든 새 버전 txid = T2 스냅샷 읽기 · 락 없음 새 버전 생성 undo 포인터 읽기(T1)와 쓰기(T2)가 서로를 막지 않아요 — 각자 자기 시점의 버전을 봅니다
💡
구현 한 조각. InnoDB는 각 행에 트랜잭션 ID·롤백 포인터를 달고, 변경 전 이미지를 undo 로그에 남겨요. 오래된 스냅샷을 읽는 트랜잭션은 undo를 되짚어 과거 버전을 재구성합니다. PostgreSQL은 아예 새 튜플을 테이블에 append하고 옛 튜플은 나중에 VACUUM이 청소해요(그래서 dead tuple·bloat 관리가 중요).
📖 스냅샷 읽기 vs 현재 읽기 (핵심 구분) 스냅샷 읽기(consistent read) — 그냥 SELECT. MVCC로 내 시점의 과거 버전을 락 없이 읽어요. 남이 바꿔도 안 보임.
현재 읽기(current read)SELECT ... FOR UPDATE, FOR SHARE, UPDATE/DELETE. 최신 커밋 버전을 읽고 락을 겁니다. "재고를 읽고 차감"처럼 읽은 값 기준으로 갱신할 땐 스냅샷 읽기로는 갱신 유실(lost update)이 나요. 그래서 SELECT ... FOR UPDATE현재 읽기 + 락을 잡습니다.
-- 재고 차감: 반드시 현재 읽기(FOR UPDATE)로 잠그고 확인 후 갱신
BEGIN;
SELECT stock FROM product WHERE id = 42 FOR UPDATE;  -- 최신값 + 배타 락
UPDATE product SET stock = stock - 1 WHERE id = 42;
COMMIT;
🔒
비관 vs 낙관, 그리고 데드락
경합을 어떻게 다루느냐가 처리량을 좌우해요.
구분비관적 락(Pessimistic)낙관적 락(Optimistic)
전제충돌이 자주 난다충돌이 드물다
방식미리 락을 잡음(FOR UPDATE)락 없이 진행, 커밋 때 버전 비교
충돌 시남은 대기(blocking)실패→재시도(버전 불일치)
적합경합 심함·짧은 트랜잭션읽기 위주·경합 낮음·웹 폼

낙관적 락은 보통 버전 컬럼으로 구현해요. 읽을 때 버전을 같이 읽고, 갱신할 때 그 버전을 조건에 넣습니다. 그 사이 누가 먼저 바꿔서 버전이 올라갔다면 업데이트된 행 수가 0 → 충돌 감지 → 재시도.

-- 낙관적 락: 읽은 version이 그대로일 때만 갱신됨
UPDATE account
   SET balance = 900, version = version + 1
 WHERE id = 7 AND version = 3;   -- 영향 행 0이면 = 누군가 먼저 수정함 → 충돌
🧭
JPA와 연결해서 기억하기. @Version 필드를 두면 JPA가 위 UPDATE의 WHERE version = ?를 자동으로 만들고, 불일치 시 OptimisticLockException을 던져요. 비관적 락은 @Lock(PESSIMISTIC_WRITE)FOR UPDATE를 붙입니다.
🤝 공유 락(S) vs 배타 락(X) 공유 락(Shared, 읽기 락) — 여러 트랜잭션이 동시에 보유 가능. 읽는 동안 남의 쓰기는 막아요.
배타 락(Exclusive, 쓰기 락)한 트랜잭션만 보유. 남의 읽기(현재 읽기)·쓰기를 모두 막아요. S와 S는 호환, S와 X·X와 X는 비호환이에요. 이 호환성 규칙이 대기·경합의 밑바탕입니다.
⚠️
데드락 — 예방의 핵심은 "락 순서 일관성"이에요. A가 행1→행2 순으로 잠그고 B가 행2→행1 순으로 잠그면 서로의 락을 기다리며 영원히 멈춰요(순환 대기). 예방의 가장 실용적인 방법은 모든 트랜잭션이 자원을 같은 순서로 잠그게 하는 거예요(예: 항상 id 오름차순). DB는 데드락을 감지하면 한쪽을 희생자로 롤백하니, 애플리케이션은 재시도를 준비해야 합니다. 트랜잭션을 짧게 유지하는 것도 경합을 줄이는 기본기예요.
🖼️ 그림으로 보기 — 데드락 — 순환 대기
트랜잭션 T1 트랜잭션 T2 행 A (락) 행 B (락) 보유 보유 B를 기다림 A를 기다림 순환 대기 = DEADLOCK 예방: 모든 트랜잭션이 자원을 같은 순서로 잠그기 (예: 항상 id 오름차순)
🌳
인덱스 심화
B-tree가 타는 경계와 안 타는 함정
인덱스는 "정렬된 사본" — 정렬을 못 쓰면 안 타요.

대부분의 관계형 인덱스는 B-tree(정확히는 B+tree)예요. 값이 정렬되어 있어 특정 값·범위를 로그 시간에 찾고, 리프가 연결돼 범위 스캔·정렬(ORDER BY)에도 강해요. 핵심은 "인덱스는 정렬된 자료구조라, 정렬 순서를 활용할 수 있을 때만 효과가 난다"입니다.

🧩 복합 인덱스 — 선두 컬럼(leftmost prefix)이 전부 (a, b, c) 복합 인덱스는 a로 먼저 정렬, 같은 a 안에서 b, 그 안에서 c로 정렬돼요. 그래서 a, a, b, a, b, c 조건은 인덱스를 타지만, b만 또는 c만으로는 못 타요(선두 컬럼이 빠졌으니까). ORDER BY a, b 같은 정렬도 이 인덱스로 추가 정렬 없이 해결됩니다. 그래서 복합 인덱스는 컬럼 순서 설계가 절반이에요. 자주 쓰는 조건·선택도(cardinality)가 높은 컬럼을 앞에 두는 게 정석입니다.
💡
커버링 인덱스(covering index). 쿼리가 필요로 하는 컬럼이 전부 인덱스 안에 있으면, 실제 테이블 행(데이터 페이지)까지 가지 않고 인덱스만 읽어 답을 냅니다. 테이블 접근(random I/O)이 사라져 아주 빨라요. EXPLAIN에서 MySQL은 Using index로 표시돼요.

반대로, 인덱스가 있어도 안 타는 대표적인 경우들이 있어요. 공통 원리는 "인덱스에 저장된 원본 값의 정렬을 그대로 못 쓰게 만드는 순간 무력화된다"입니다.

안 타는 경우이유
WHERE YEAR(created_at) = 2026컬럼에 함수·연산을 적용하면 인덱스의 원본 값을 못 씀 → 범위 조건으로 바꿔라
WHERE name LIKE '%kim'앞 와일드카드는 시작점을 못 잡아 스캔. 'kim%'는 탈 수 있음
WHERE phone = 1012345678 (컬럼은 문자열)암묵적 형변환이 컬럼 쪽에 걸리면 인덱스 무력화
낮은 선택도(예: 성별=남)대상 행이 너무 많으면 옵티마이저가 풀 스캔을 택하기도 함
🔎
추측 말고 EXPLAIN. 실행계획으로 어떤 인덱스를 탔는지·스캔 방식(type)·예상 행 수(rows)·Extra를 확인하세요. MySQL의 type에서 ref/range는 인덱스 활용, ALL은 풀 테이블 스캔이에요. 실측이 필요하면 EXPLAIN ANALYZE로 실제 시간까지 봅니다. 함수 조건이 불가피하면 함수 기반 인덱스(PostgreSQL·최신 MySQL) 또는 생성 컬럼(generated column) + 인덱스로 우회해요.
🌐
확장
복제 · 샤딩 · 반정규화
한 서버로 안 될 때, 무엇을 포기하고 무엇을 얻나.

트래픽이 커지면 수직 확장(스케일 업)엔 한계가 와요. 그때 쓰는 세 가지 카드가 읽기 복제·샤딩·반정규화입니다.

📚 읽기 복제(Read Replication) Primary(쓰기)의 변경을 Replica(읽기 전용)들로 복제해, 읽기 트래픽을 여러 대로 분산해요. 읽기 90%인 서비스에서 아주 효과적입니다. 다만 복제는 비동기인 경우가 많아 복제 지연(replication lag)이 생겨요. 그래서 "방금 쓴 값을 곧바로 replica에서 읽으면 아직 안 보일 수 있어요"(read-your-writes 문제). 쓰기 직후 조회는 primary로 라우팅하거나, 지연을 감안한 설계가 필요합니다.
⚠️
복제는 "읽기 분산"이지 "쓰기 분산"이 아니에요. 모든 쓰기는 결국 Primary 한 곳으로 몰려요. 쓰기 자체가 병목이면 복제로는 못 풀고, 샤딩으로 데이터를 수평 분할해야 합니다.
🔪 샤딩(Sharding) — 수평 분할 한 테이블을 샤드 키(shard key) 기준으로 여러 DB 서버에 나눠 담아요(예: user_id % N 또는 범위/해시). 쓰기·저장 용량이 서버 수만큼 늘어납니다. 대신 대가가 커요. 샤드를 넘나드는 조인·트랜잭션·정렬·집계가 어려워지고, 샤드 키 선택이 운명을 가릅니다. 키가 편향되면 특정 샤드만 뜨거워지는(hotspot) 데이터 쏠림이 생겨요. 그래서 고르게 분산되고, 대부분의 쿼리가 한 샤드로 떨어지는 키를 골라야 합니다.
💡
반정규화(Denormalization). 정규화는 중복을 없애 정합성을 지키지만, 조회 때마다 여러 테이블 조인이 필요해 느려질 수 있어요. 읽기 성능이 절실하면 일부 컬럼을 의도적으로 중복 저장해 조인을 줄입니다(예: 주문 테이블에 상품명 사본). 대가는 갱신 시 여러 곳을 맞춰줘야 하는 정합성 부담이에요 — "읽기 속도 ↔ 쓰기 복잡도"의 교환입니다.
Read Replica Replication Lag Sharding Shard Key Hotspot Denormalization
🧠 이 장 핵심 요약
📝
문제풀이 · 점검
시니어 관점으로 점검
면접·장애대응에서 실제로 묻는 결의 문제예요. 틀려도 바로 해설이 나옵니다.
🧪
개념을 "안다"와 "설명할 수 있다"는 달라요. 네 유형(객관식 · O/X · 빈칸 · 코드결과)으로 점검해보세요.
심화 CH 19 📦 빌드와 의존성과 모듈 — Maven/Gradle, 의존성 관리, JPMS (제작 예정)