MVCC는 왜 락 없이 격리를 흉내내는가
락을 걸지 않고 격리를 흉내내는 방법
락 기반 시스템의 근본 문제는 "읽기가 쓰기를 막는다"는 것입니다. 누군가 행을 읽는 동안 다른 트랜잭션이 그 행을 수정하려면 기다려야 합니다. 읽기 위주의 서비스에서 이건 치명적인 병목입니다. MVCC(Multi-Version Concurrency Control)의 아이디어는 단순하면서도 영리합니다 — 행을 수정할 때 덮어쓰지 말고, 새 버전을 하나 더 만들어라. 그러면 읽는 트랜잭션은 자기 시점에 맞는 버전을 보고, 쓰는 트랜잭션은 새 버전을 만들 뿐이니 서로 기다릴 이유가 없습니다.
PostgreSQL 튜플의 실제 구조
PostgreSQL에서 UPDATE는 실제로는 "UPDATE"가 아닙니다. 기존 행을 수정하는 대신,
새 튜플을 추가로 쓰고 기존 튜플을 무효 처리합니다.
-- 이 UPDATE 하나가 실제로는:
UPDATE accounts SET balance = 600000 WHERE id = 1;
-- 1. 기존 튜플(id=1, balance=500000)의 xmax를 현재 트랜잭션 ID로 설정한다
-- 2. 새 튜플(id=1, balance=600000)을 xmin=현재 트랜잭션 ID로 추가한다
-- 3. 기존 튜플은 삭제되지 않는다 — VACUUM이 나중에 정리한다모든 튜플은 xmin(이 버전을 만든 트랜잭션 ID)과 xmax(이 버전을 무효화한 트랜잭션
ID, 없으면 비어있음)를 헤더에 갖고 있습니다. 트랜잭션이 시작될 때 스냅샷을 하나
받는데, 이 스냅샷은 "그 시점에 커밋된 트랜잭션 ID의 집합"입니다. 어떤 튜플을 볼지는
간단한 규칙으로 정해집니다 — xmin이 내 스냅샷에서 커밋된 것으로 보이고, xmax가
비어있거나 아직 커밋 안 된 것으로 보이면 그 튜플이 "나에게 보이는" 버전입니다.
sequenceDiagram participant T1 as 트랜잭션 A (읽기) participant DB as 튜플 저장소 participant T2 as 트랜잭션 B (쓰기) T1->>DB: 스냅샷 획득 (xmin=100 버전이 보임) T2->>DB: UPDATE 실행 → xmax=100 버전 표시, xmin=101 버전 추가 T2->>DB: COMMIT T1->>DB: 같은 트랜잭션 안에서 재조회 DB-->>T1: Repeatable Read라면 여전히 xmin=100 버전을 반환
이게 왜 중요하냐면, Repeatable Read가 "값을 잠가서" 변경을 막는 게 아니라 애초에 다른 버전을 보여주지 않아서 변경이 안 보이는 것처럼 만들기 때문입니다. B의 UPDATE는 실제로 성공하고 커밋됩니다. A는 그저 자기 스냅샷 기준으로 예전 버전을 계속 보는 것뿐이고, 락은 전혀 관여하지 않았습니다.
VACUUM이 필요한 이유가 여기서 나온다
이 구조를 알면 "PostgreSQL은 왜 VACUUM이 필요한가"라는, 얼핏 관계없어 보이는 질문에도
답이 됩니다. UPDATE와 DELETE가 물리적으로 행을 지우지 않고 새 버전만 쌓기 때문에,
아무도 정리하지 않으면 죽은 튜플(dead tuple)이 테이블을 계속 부풀립니다. VACUUM은
"어떤 트랜잭션도 더 이상 볼 수 없는" 오래된 버전을 찾아 실제로 회수하는 작업입니다.
운영 중인 PostgreSQL에서 테이블 크기가 예상보다 훨씬 큰데 데이터는 그대로라면, 대부분
autovacuum이 밀린 경우입니다 — MVCC의 대가를 치르는 순간입니다.
MVCC로도 못 막는 것
MVCC는 "이미 존재하는 행의 값이 바뀌는 것"은 스냅샷만으로 우아하게 처리합니다. 하지만 두 가지는 스냅샷만으로 해결이 안 됩니다.
- 아직 존재하지 않는 행(Phantom) — 스냅샷은 "본 적 있는 튜플의 버전"을 관리하는 구조라서, 애초에 존재하지 않던 행이 새로 들어오는 것 자체는 막을 방법이 없습니다.
- 두 트랜잭션이 동시에 같은 행을 쓰려는 경우 — 둘 다 새 버전을 만들려고 하면 반드시 순서를 정해야 하고, 이건 스냅샷이 아니라 락으로 해결합니다. PostgreSQL도 내부적으로 튜플 단위 락을 씁니다.
다음 장에서는 바로 이 지점, MVCC가 손을 놓는 자리에서 락이 어떻게 개입하는지를 MySQL InnoDB의 갭 락을 중심으로 다룹니다.