안동민 개발노트 아이콘

안동민 개발노트

11장 : 동시성 제어

MVCC

MVCC(Multi-Version Concurrency Control)는 데이터를 여러 버전으로 보관해, 일반적인 일관 읽기가 쓰기 트랜잭션과 직접 충돌하지 않도록 만드는 동시성 제어 기법입니다. 읽기 위주의 워크로드에서는 대기 시간을 크게 줄이지만, 쓰기-쓰기 충돌·명시적 잠금·DDL 충돌은 여전히 별도로 제어됩니다.


MVCC의 개념

순수한 락 기반 읽기 모델에서는 누군가 데이터를 수정 중일 때 다른 트랜잭션의 잠금 읽기가 대기할 수 있습니다. MVCC는 오래된 커밋 버전을 재구성해 일반 SELECT가 같은 행의 최신 수정 작업을 기다리지 않게 만듭니다.


Oracle의 MVCC — Undo Segment와 SCN

Oracle은 Undo SegmentSCN(System Change Number)으로 MVCC를 구현합니다.

SCN (System Change Number)

Undo Segment를 이용한 버전 관리

Oracle Consistent Read 과정
T1이 SCN 1500에 SELECT A 실행:

1. 현재 블록 확인: A=300, SCN=2000
   → 2000 > 1500 → 이 버전은 너무 최신, 사용 불가

2. Undo 체인 따라감: A=200, SCN=1500
   → 1500 ≤ 1500 → 이 버전 사용 가능!
   → A = 200 반환

T2가 SCN 1200에 SELECT A 실행:

1. 현재 블록: SCN=2000 > 1200 → 불가
2. Undo 1: SCN=1500 > 1200 → 불가
3. Undo 2: SCN=1000 ≤ 1200 → 사용 가능!
   → A = 100 반환

ORA-01555: Snapshot Too Old


MySQL InnoDB의 MVCC

MySQL InnoDB도 MVCC를 사용하지만 구현 방식이 다릅니다. 클러스터드 인덱스 레코드에 숨겨진 시스템 필드를 두고, undo log를 따라가며 필요한 과거 버전을 재구성합니다.

InnoDB 숨겨진 컬럼

ReadView

ReadView 생성 시점과 격리 수준
READ COMMITTED
  * 매 SELECT마다 새로운 ReadView 생성
  * → 다른 트랜잭션이 커밋하면 그 변경이 바로 보임

REPEATABLE READ
  * 트랜잭션의 첫 번째 consistent read(일반 SELECT)에서 ReadView 생성
  * → 이후 모든 SELECT가 같은 ReadView 사용
  * → 트랜잭션 동안 일관된 스냅샷 유지

Oracle vs MySQL MVCC 비교

비교OracleMySQL InnoDB
버전 저장Undo SegmentUndo Log / Undo Tablespace
시점 식별SCN (전역 번호)트랜잭션 ID + ReadView
스냅샷 기준 (RC)문장 시작 시점SELECT마다 새 ReadView
트랜잭션 단위 스냅샷SERIALIZABLE / READ ONLY 사용REPEATABLE READ의 첫 consistent read
기본 격리 수준READ COMMITTEDREPEATABLE READ
일반 SELECT의 행 잠금DML 행 락 없음consistent read는 데이터 락 없음
오래된 버전 보관 실패ORA-01555 가능purge 지연·undo tablespace 증가
쓰기 vs 쓰기행 잠금 대기행 잠금 대기

낙관적 잠금 vs 비관적 잠금

MVCC 환경에서 실무적으로 자주 사용되는 두 가지 동시성 제어 패턴입니다.

비관적 잠금 (Pessimistic Locking)

비관적 잠금
-- 재고 차감 시: 먼저 락을 건 후 처리
-- Oracle
SELECT stock FROM products WHERE id = 101 FOR UPDATE;
-- → 해당 행에 X-Lock, 다른 트랜잭션의 UPDATE/DELETE/FOR UPDATE 차단

-- MySQL 8.0+ (즉시 실패)
SELECT stock FROM products WHERE id = 101 FOR UPDATE NOWAIT;
-- → 잠금 획득 불가 시 즉시 에러 (대기 안 함)

SELECT stock FROM products WHERE id = 101 
FOR UPDATE SKIP LOCKED;
-- → 잠긴 행은 건너뛰고 잠금 가능한 행만 반환 (큐 패턴)

UPDATE products SET stock = stock - 1 WHERE id = 101;
COMMIT;

낙관적 잠금 (Optimistic Locking)

낙관적 잠금
-- version 컬럼을 이용하는 방식
SELECT id, stock, version FROM products WHERE id = 101;
-- → stock=10, version=3

-- 읽은 version으로 조건부 UPDATE
UPDATE products
SET stock = stock - 1, version = version + 1
WHERE id = 101 AND version = 3;
-- → 다른 트랜잭션이 먼저 수정했으면 version이 달라서 0건 갱신

-- 영향받은 행이 0이면 충돌 발생 → 재시도 또는 에러 처리
낙관적 잠금 — 다른 구현 방법
1. version 컬럼 (가장 일반적):
  WHERE id = ? AND version = ?
  SET version = version + 1

2. 타임스탬프:
  WHERE id = ? AND updated_at = ?
  SET updated_at = NOW()

3. 해시 또는 체크섬:
  WHERE id = ? AND ORA_HASH(col1||col2||col3) = ?
  충돌 가능성이 있으므로 핵심 정합성에는 version 컬럼이 더 명확

4. 전체 컬럼 비교 (일부 ORM 전략):
  WHERE id = ? AND col1 = ? AND col2 = ? AND col3 = ?
비교비관적낙관적
방식미리 락 획득 (SELECT FOR UPDATE)커밋 시 충돌 검사 (version)
충돌 시대기 (블로킹)재시도 또는 에러
적합 상황충돌 빈번 (좌석 예약, 재고)충돌 드문 (프로필 수정)
성능동시성↓동시성↑
데드락 위험있음낮지만 다중 행 갱신에서는 가능
DB 잠금미리 길게 사용조건부 UPDATE 시 짧게 사용

MVCC와 쓰기 충돌

MVCC는 일반 읽기가 과거 버전을 보게 해 주지만, 두 트랜잭션이 같은 현재 행을 쓰는 문제까지 자동으로 없애지는 않습니다. UPDATE/DELETE는 여전히 현재 버전에 대한 행 잠금, 대기, 충돌 감지의 영향을 받습니다. 애플리케이션에서 읽은 낡은 값을 절대값으로 저장하는 경로는 조건부 UPDATE, version 컬럼, SELECT ... FOR UPDATE 같은 별도 제어가 필요합니다.


MVCC와 격리 수준의 관계


MVCC에서의 Phantom Read 방지

MVCC의 스냅샷은 “같은 읽기 기준에서 다시 보게 하는 것”에 강하지만, Phantom Read 방지 방식은 DBMS와 격리 수준에 따라 다릅니다. Oracle은 문장 단위 읽기 일관성이 기본이고 트랜잭션 단위 일관성은 SERIALIZABLE 또는 READ ONLY 트랜잭션으로 다룹니다. MySQL InnoDB는 REPEATABLE READ에서 consistent read 스냅샷과 잠금 읽기의 next-key lock을 조합합니다. PostgreSQL은 SERIALIZABLE에서 SSI로 직렬화 이상을 탐지하고, SQL Server는 SNAPSHOT/RCSI 같은 row versioning 옵션을 사용합니다.


MVCC 성능 고려사항


DBMS별 MVCC 구현 종합 비교


MVCC 실무 적용 패턴

긴 트랜잭션 탐지
Oracle
  SELECT s.sid, s.serial#, s.username,
         t.start_time, t.used_ublk
  FROM v$session s, v$transaction t
  WHERE s.taddr = t.addr
  ORDER BY t.start_time;

MySQL
  SELECT trx_id, trx_state, trx_started,
         TIMESTAMPDIFF(SECOND, trx_started, NOW()) duration
  FROM information_schema.innodb_trx
  ORDER BY trx_started;

PostgreSQL
  SELECT pid, usename, state, query_start,
         NOW() - query_start AS duration
  FROM pg_stat_activity
  WHERE state = 'idle in transaction'
  ORDER BY query_start;

정리

개념핵심 내용
MVCC다중 버전으로 일반 읽기-쓰기 차단 감소
OracleUndo Segment + SCN, Consistent Read
MySQL InnoDBUndo Log + 트랜잭션 ID + ReadView
PostgreSQL데이터 파일에 이전 버전 보관 + VACUUM
ReadView가시성 판단 기준 (활성 트랜잭션 스냅샷)
ORA-01555Undo 소멸로 인한 스냅샷 복원 실패
비관적 잠금FOR UPDATE로 미리 잠금, 충돌 빈번 시 적합
낙관적 잠금version 비교, 충돌 드문 경우 적합
SQL ServerRCSI/SNAPSHOT에서 row versioning 사용

MVCC의 핵심은 일반적인 일관 읽기가 쓰기와 직접 충돌하지 않도록 과거 버전을 제공한다는 것입니다. 덕분에 읽기 위주의 워크로드에서 높은 동시성을 달성할 수 있습니다. 단, 이전 버전을 보관하는 undo/버전 저장 공간을 적절히 관리하고, 오래 걸리는 조회가 purge 지연이나 오래된 버전 복원 실패를 만들지 않도록 주의해야 합니다.

다음 장에서는 트랜잭션의 지속성을 보장하는 복구와 로깅을 다루겠습니다.