MVCC

MVCC(Multi-Version Concurrency Control)는 데이터를 수정할 때 이전 버전을 유지하여, 읽기 작업과 쓰기 작업이 서로 차단하지 않고 동시에 수행될 수 있게 하는 동시성 제어 기법입니다.

왜 필요한가

[MVCC가 없는 경우 — 락 기반]
트랜잭션A: UPDATE 상품 가격 (쓰기 락 획득)
트랜잭션B: SELECT 상품 가격 → 대기 ⏳ (A가 끝날 때까지)

→ 읽기도 쓰기 락에 의해 차단됨
→ 동시 사용자가 많으면 성능 급감

[MVCC가 있는 경우]
트랜잭션A: UPDATE 상품 가격 (새 버전 생성)
트랜잭션B: SELECT 상품 가격 → 이전 버전을 읽음 → 대기 없음 O

→ 읽기와 쓰기가 서로 차단하지 않음
→ 동시 처리량 대폭 향상

핵심 원리

"데이터를 수정해도 이전 버전을 삭제하지 않고 보관한다"

[시간순 흐름]
시점 1: 가격 = 45,000 (version 1)
시점 2: UPDATE → 가격 = 39,000 (version 2 생성, version 1 보관)
시점 3: UPDATE → 가격 = 42,000 (version 3 생성, version 1,2 보관)

┌─────────────────────────────────────────────┐
│ 버전 저장소 (Undo Log / 버전 체인)            │
│                                             │
│ version 3: 가격 = 42,000 (최신)              │
│ version 2: 가격 = 39,000                     │
│ version 1: 가격 = 45,000                     │
└─────────────────────────────────────────────┘

→ 각 트랜잭션은 자신의 시작 시점에 맞는 버전을 읽음

MySQL(InnoDB)의 MVCC 동작

Undo Log 기반

[데이터 수정 시]

1. 현재 데이터를 Undo Log에 복사 (이전 버전 보관)
2. 실제 데이터를 새 값으로 수정
3. 수정된 행에 트랜잭션 ID와 Undo Log 포인터 기록

┌──────────── 실제 테이블 ────────────┐
│ id │ price  │ trx_id │ undo_ptr    │
│ 1  │ 42,000 │ 103    │ ──────────→ │──┐
└────────────────────────────────────┘  │
                                        ▼
┌──────────── Undo Log ──────────────┐
│ price = 39,000 │ trx_id: 102 │ ──→ │──┐
└─────────────────────────────────────┘  │
                                         ▼
┌──────────── Undo Log ──────────────┐
│ price = 45,000 │ trx_id: 101 │ NULL │
└─────────────────────────────────────┘

→ 포인터를 따라가면 이전 버전들을 탐색할 수 있음

읽기 시 버전 선택 — 스냅샷

[REPEATABLE READ에서의 동작]

트랜잭션A (trx_id: 104, 시작 시점: T1)
트랜잭션B (trx_id: 105)

T1: 트랜잭션A 시작
T2: 트랜잭션A: SELECT price → 스냅샷 생성
    → "104보다 먼저 커밋된 버전만 보겠다"
    → version(trx_id: 101, price: 45,000) 확인 → 커밋됨 → 이 값을 읽음 O

T3: 트랜잭션B: UPDATE price = 39,000 + COMMIT (trx_id: 105)

T4: 트랜잭션A: SELECT price
    → 여전히 "104보다 먼저 커밋된 버전만 본다"
    → trx_id: 105는 104 이후 → 무시
    → trx_id: 101의 45,000을 읽음 O

→ 트랜잭션A는 시작 시점의 일관된 데이터를 계속 봄
→ 반복 읽기 보장 (Non-Repeatable Read 방지)

격리 수준별 MVCC 동작 차이

READ COMMITTED

각 SELECT마다 새로운 스냅샷 생성

트랜잭션A 시작 (trx_id: 104)

T1: SELECT price → 스냅샷 생성 → 45,000 읽음
T2: 트랜잭션B: UPDATE price = 39,000 + COMMIT
T3: SELECT price → 새 스냅샷 생성 → 39,000 읽음 (B의 커밋 반영)

→ 같은 트랜잭션 내에서 다른 값을 읽을 수 있음
→ Non-Repeatable Read 발생 가능

REPEATABLE READ (MySQL 기본)

트랜잭션 시작 시 한 번만 스냅샷 생성, 끝까지 유지

트랜잭션A 시작 (trx_id: 104)

T1: SELECT price → 스냅샷 생성 → 45,000 읽음
T2: 트랜잭션B: UPDATE price = 39,000 + COMMIT
T3: SELECT price → 기존 스냅샷 유지 → 45,000 읽음 (B의 커밋 무시)

→ 같은 트랜잭션 내에서 항상 같은 값을 읽음
→ Non-Repeatable Read 방지 O
격리 수준스냅샷 생성 시점같은 트랜잭션 내 일관성
READ COMMITTED매 SELECT마다보장하지 않음
REPEATABLE READ트랜잭션 첫 SELECT 시보장 O

MVCC가 해결하는 것과 해결하지 못하는 것

[MVCC가 해결하는 것]
O 읽기-쓰기 동시 수행 (서로 차단하지 않음)
O 더티 리드 방지 (커밋된 버전만 읽음)
O 반복 읽기 보장 (REPEATABLE READ에서)
O 일관된 읽기 (Consistent Read)

[MVCC가 해결하지 못하는 것]
X 쓰기-쓰기 충돌 → 여전히 락 필요
X 갱신 분실 (Lost Update) → 비관적/낙관적 락으로 해결
[쓰기-쓰기 충돌 — MVCC만으로 부족]

트랜잭션A: READ 재고 = 10 (MVCC 스냅샷)
트랜잭션B: READ 재고 = 10 (MVCC 스냅샷)
트랜잭션A: UPDATE 재고 = 9 + COMMIT
트랜잭션B: UPDATE 재고 = 9 + COMMIT ← 갱신 분실 X

→ 이 경우는 SELECT ... FOR UPDATE (비관적 락) 등이 필요

MVCC + 락의 조합

// 읽기 전용 — MVCC만으로 충분 (락 불필요)
@Transactional(readOnly = true)
public ProductResponse findById(Long id) {
    // MVCC 스냅샷으로 일관된 읽기
    Product product = productRepository.findById(id)
            .orElseThrow(() -> new IllegalArgumentException("상품 없음"));
    return new ProductResponse(product);
}

// 쓰기 — MVCC + 비관적 락 조합
@Transactional
public void decreaseStock(Long productId, int quantity) {
    // FOR UPDATE로 쓰기 락 획득 → 다른 트랜잭션 대기
    Product product = productRepository.findByIdForUpdate(productId)
            .orElseThrow(() -> new IllegalArgumentException("상품 없음"));

    product.decreaseStock(quantity);
}
[동시 접근 시]

읽기 + 읽기: MVCC로 둘 다 대기 없이 처리 O
읽기 + 쓰기: MVCC로 읽기는 이전 버전 조회, 쓰기는 최신 데이터 수정 O
쓰기 + 쓰기: 락으로 순차 처리 (하나가 대기) O

MVCC의 비용 — Undo Log 관리

[Undo Log 누적 문제]

트랜잭션이 오래 유지되면:
→ 해당 트랜잭션의 스냅샷에서 참조하는 이전 버전을 삭제할 수 없음
→ Undo Log가 계속 쌓임 → 디스크/메모리 사용량 증가

[Purge Thread]
MySQL InnoDB는 백그라운드에서 Purge Thread가
더 이상 참조되지 않는 Undo Log를 정리함

→ 긴 트랜잭션을 피하는 것이 MVCC 환경에서 중요
// X 긴 트랜잭션 — Undo Log 누적 유발
@Transactional
public void longProcess() {
    Product product = productRepository.findById(1L).orElseThrow();

    externalApiCall();       // 외부 API 호출 (수 초 소요)
    sendEmail();             // 이메일 발송 (수 초 소요)
    generateReport();        // 리포트 생성 (수 초 소요)

    product.update(...);     // 이 시점까지 트랜잭션이 열려 있음
}

// O 트랜잭션 범위 최소화
public void optimizedProcess() {
    updateProduct();          // 짧은 트랜잭션
    externalApiCall();        // 트랜잭션 밖에서 처리
    sendEmail();
    generateReport();
}

@Transactional
public void updateProduct() {
    Product product = productRepository.findById(1L).orElseThrow();
    product.update(...);
    // 빠르게 커밋 → Undo Log 정리 가능
}

DB별 MVCC 구현 차이

항목MySQL (InnoDB)PostgreSQL
이전 버전 저장Undo Log (별도 공간)테이블 내에 직접 저장
버전 정리Purge ThreadVACUUM 프로세스
기본 격리 수준REPEATABLE READREAD COMMITTED
스냅샷 방식트랜잭션 ID 기반XID(트랜잭션 ID) 기반
[MySQL InnoDB]
테이블: 최신 버전만 저장
Undo Log: 이전 버전 보관 → Purge Thread가 정리

[PostgreSQL]
테이블: 모든 버전을 테이블 안에 저장 (튜플 버전 관리)
→ 테이블이 비대해질 수 있음 → VACUUM으로 정리 필수

면접 포인트

CSLOG에서 보기