한양과학기술고등학교

에듀테크 프로젝트 대시보드

MVCC — 다중 버전 동시성 제어

읽기가 쓰기를 기다려야 할까?

여러 트랜잭션이 같은 행을 동시에 다룰 때, 가장 단순한 방법은 잠금(lock)이다. 누군가 쓰는 중이면 다른 사람은 읽지도 못하고 기다린다. 은행 앱에서 잔액을 조회하는데 누군가의 이체가 끝날 때까지 로딩만 도는 셈이다. MVCC(Multi-Version Concurrency Control)는 행을 덮어쓰지 않고 새 버전을 하나 더 만든다. 그래서 읽는 사람은 절대 막히지 않고, 각자 자기 트랜잭션이 시작된 시점의 "스냅샷"을 본다. PostgreSQL·MySQL(InnoDB)·Oracle이 이 방식을 쓴다.

잠금 방식

쓰기 중이면 읽기도 대기 → 읽기 ↔ 쓰기 충돌. 처리량이 잠금에 묶인다.

MVCC

UPDATE는 새 버전 tuple 삽입, 옛 버전은 그대로 둠. 읽기는 대기 없음.

핵심: 모든 행에 숨은 두 칸이 있다

PostgreSQL은 모든 튜플(행 버전)에 xmin(이 버전을 만든 트랜잭션 ID)과 xmax(이 버전을 삭제·갱신한 트랜잭션 ID, 없으면 0)를 붙인다. 트랜잭션은 시작할 때 스냅샷 = "지금 이미 커밋된 트랜잭션 ID 집합"을 찍는다. 어떤 버전이 나에게 보이는지는 이 규칙으로 결정된다:

보인다(tuple) = tuple.xmin 이 내 스냅샷 기준 커밋됨 # 이 버전이 이미 확정되어 있고 AND ( tuple.xmax == 0 OR tuple.xmax 가 아직 커밋 안 됨 ) # 아직 아무도 지우지 않았다

직접 돌려보기 — 계좌 잔액 시나리오

잔액 1,000원짜리 행 하나로 시작한다. T50이 조회하는 도중 T51이 500원을 더한다. 단계를 넘기며 각 트랜잭션이 무엇을 보는지 확인해 보자.

0 / 6

Heap — accounts 행 버전들

트랜잭션 & 스냅샷

시작하려면 다음 단계를 누르세요.

버전 체인과 청소(VACUUM)

UPDATE를 반복하면 옛 버전들이 버전 체인으로 쌓인다. 최신 버전이 헤드, 이전 버전들은 뒤에 매달린다. 어떤 살아있는 스냅샷도 더 이상 볼 수 없게 된 죽은 튜플(dead tuple)은 공간만 차지하므로, PostgreSQL의 VACUUM(또는 InnoDB의 purge 스레드)이 주기적으로 회수한다. 청소가 밀리면 테이블이 붓는 테이블 블로트가 생긴다.

1,800원
xmin=71 xmax=0
→
1,500원
xmin=63 xmax=71
→
1,000원
xmin=41 xmax=63
→
청소 대상
dead tuple

장점과 대가

장점대가(trade-off)
읽기가 쓰기를 막지 않고, 쓰기도 읽기를 막지 않는다. 옛 버전 저장 공간이 필요하고 VACUUM/purge 비용이 든다.
일관된 스냅샷(반복 읽기 시 같은 결과)을 잠금 없이 제공. 인덱스도 여러 버전을 가리켜 커질 수 있다(HOT update로 완화).
롤백이 싸다 — 새 버전을 안 보이게만 하면 끝. 쓰기-쓰기 충돌은 여전히 잠금/재시도가 필요(직렬화 이상).

관련 주제: 트랜잭션과 ACID · 데이터베이스 인덱스 · 경쟁 상태 · LSM 트리