백엔드 면접 용어 사전
데이터베이스

MVCC

버전을 여러 개 유지해 읽기가 쓰기를 막지 않게 하는 동시성 제어 방식.

같은 행의 여러 버전을 함께 보관해, 읽기가 쓰기를 기다리지 않게 하는 동시성 제어 방식 (Multi-Version Concurrency Control).

해결하려는 문제

락만 쓰면 이렇게 된다.

T1: UPDATE 상품 SET 재고 = 5 WHERE id = 1   (락 획득, 아직 커밋 안 함)
T2: SELECT 재고 FROM 상품 WHERE id = 1      ← T1이 끝날 때까지 대기

읽기만 하는데 기다려야 한다. 조회가 압도적으로 많은 서비스에서 치명적이다.

MVCC의 답 — 옛 버전을 보여 준다

읽기는 쓰기를 막지 않고, 쓰기는 읽기를 막지 않는다.

T2는 T1이 수정하기 전 버전을 읽는다. 기다릴 필요가 없다.

어떻게 구현되나 (InnoDB)

각 행에 숨은 컬럼이 붙어 있다.

행 데이터DB_TRX_IDDB_ROLL_PTR
[id=1, 재고=10]100→ undo 로그 위치
  • DB_TRX_ID — 이 버전을 만든 트랜잭션 번호
  • DB_ROLL_PTR이전 버전이 있는 undo 로그를 가리키는 포인터

수정하면 새 버전을 만들고, 이전 값은 undo 로그에 남긴다. 포인터로 이어져 버전 체인이 만들어진다.

현재: [재고=5, TRX=200] ──► undo: [재고=10, TRX=100] ──► undo: [재고=15, TRX=50]

읽을 때 어느 버전을 보나 — Read View

트랜잭션이 조회를 시작하면 그 시점에 활성 상태인 트랜잭션 목록(Read View)을 만든다. 버전 체인을 따라가며 "내가 볼 수 있는 버전"을 고른다.

  • 내 트랜잭션보다 먼저 커밋된 버전 → 볼 수 있음
  • 아직 커밋 안 됐거나 나보다 나중 → 건너뛰고 이전 버전으로

READ COMMITTED는 쿼리마다 Read View를 새로 만들고, REPEATABLE READ는 트랜잭션 시작 시 한 번 만들어 끝까지 쓴다. 이 차이가 두 격리수준의 동작 차이를 만든다.

대가 ① — 저장 공간

옛 버전을 계속 들고 있어야 한다.

대가 ② — 긴 트랜잭션이 재앙이 된다

오래 열려 있는 트랜잭션이 하나만 있어도 그 Read View가 볼 수 있는 옛 버전을 지울 수 없다. undo 로그가 계속 쌓인다.

PostgreSQL → 테이블 부풀림(bloat), VACUUM 필요

  • MySQL — → undo 테이블스페이스 증가, purge 지연

"긴 트랜잭션은 그 자체로 장애 요인" 이라는 말이 여기서 나온다. 읽기 전용이어도 마찬가지다.

면접 함정

"MVCC면 락이 필요 없나?"쓰기끼리는 여전히 충돌한다. 같은 행을 동시에 수정하면 뒤에 온 쪽이 기다리거나 실패한다. MVCC가 없앤 것은 읽기와 쓰기 사이의 대기뿐이다.

SELECT ... FOR UPDATE는 MVCC를 우회해 현재 최신 버전에 락을 건다 (current read). 재고 차감처럼 최신값이 꼭 필요할 때 쓴다.

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — 데이터베이스 — 인덱스·트랜잭션·격리수준·N+1