한 트랜잭션이 데이터를 수정하는 동안, 다른 트랜잭션이 같은 데이터를 읽으면 어떤 값을 보게 될까요?

동시에 여러 트랜잭션이 실행되는 환경에서, 읽기와 쓰기가 서로를 차단하면 성능이 크게 떨어집니다. InnoDB의 MVCC(Multi-Version Concurrency Control) 는 데이터의 여러 버전을 유지하여 이 문제를 해결합니다. 읽기는 락 없이 처리하면서도 일관성을 보장하는 핵심 메커니즘입니다.

개념 정의

MVCC는 데이터를 변경할 때 ** 이전 버전을 Undo Log에 보관 **하여, 각 트랜잭션이 자신에게 맞는 버전을 읽을 수 있게 하는 동시성 제어 방식입니다.

PLAINTEXT
트랜잭션 A: UPDATE users SET name='김영희' WHERE id=1;  (name='김철수' → '김영희')
트랜잭션 B: SELECT name FROM users WHERE id=1;  ← 어떤 값을 보는가?

MVCC 없이: B가 A의 커밋을 기다려야 함 (읽기 차단)
MVCC 있을 때: B가 이전 버전('김철수')을 Undo Log에서 읽음 (읽기 비차단)

왜 필요한가

락 기반 동시성 제어의 한계:

PLAINTEXT
락 기반:
  읽기-읽기: 동시 가능
  읽기-쓰기: 차단 (읽기가 쓰기를 기다리거나, 쓰기가 읽기를 기다림)
  쓰기-쓰기: 차단

MVCC:
  읽기-읽기: 동시 가능
  읽기-쓰기: 동시 가능 (읽기는 이전 버전 참조)
  쓰기-쓰기: 차단 (여전히 락 필요)

** 읽기가 쓰기를 차단하지 않고, 쓰기가 읽기를 차단하지 않습니다.** 이것이 MVCC의 핵심 이점입니다.

InnoDB의 MVCC 구현

행의 숨겨진 컬럼

InnoDB의 모든 행에는 사용자가 보이지 않는 3개의 숨겨진 컬럼이 있습니다.

컬럼크기설명
DB_TRX_ID6바이트이 행을 마지막으로 수정한 트랜잭션 ID
DB_ROLL_PTR7바이트Undo Log에서 이전 버전을 가리키는 포인터
DB_ROW_ID6바이트행 ID (PK가 없을 때 자동 생성)
PLAINTEXT
실제 행 구조:
┌──────────┬──────────────┬──────────┬─────┬──────┬───────┐
│ DB_TRX_ID│ DB_ROLL_PTR  │ DB_ROW_ID│ id  │ name │ age   │
│ (trx 10) │ (→ undo log) │          │  1  │ 김영희│  28   │
└──────────┴──────────────┴──────────┴─────┴──────┴───────┘

Undo Log 기반 버전 체인

데이터가 변경되면 이전 버전이 Undo Log에 저장되고, ** 체인으로 연결 **됩니다.

SQL
-- 초기 상태 (trx 5에서 INSERT)
-- id=1, name='김철수', trx_id=5

-- trx 10: UPDATE users SET name='김영희' WHERE id=1;
-- trx 15: UPDATE users SET name='이수진' WHERE id=1;
PLAINTEXT
현재 행 (최신 버전):
  name='이수진', trx_id=15, roll_ptr → Undo Log [2]

Undo Log [2]:
  name='김영희', trx_id=10, roll_ptr → Undo Log [1]

Undo Log [1]:
  name='김철수', trx_id=5, roll_ptr → NULL (최초 버전)

버전 체인:

PLAINTEXT
이수진(trx 15) → 김영희(trx 10) → 김철수(trx 5)

Read View — 가시성 판단

Read View는 트랜잭션이 ** 어떤 버전의 데이터를 볼 수 있는지** 결정하는 스냅샷입니다.

Read View의 구성 요소

PLAINTEXT
Read View 생성 시점에 기록되는 정보:
- m_ids: 현재 활성(커밋되지 않은) 트랜잭션 ID 목록
- m_low_limit_id: 아직 할당되지 않은 가장 작은 트랜잭션 ID (이것 이상은 미래)
- m_up_limit_id: m_ids에서 가장 작은 트랜잭션 ID
- m_creator_trx_id: 이 Read View를 생성한 트랜잭션 ID

가시성 판단 알고리즘

행의 DB_TRX_IDtrx_id일 때:

PLAINTEXT
1. trx_id < m_up_limit_id
   → 이 버전은 Read View 생성 전에 커밋됨 → 보임

2. trx_id >= m_low_limit_id
   → 이 버전은 Read View 생성 후에 시작됨 → 안 보임

3. m_up_limit_id <= trx_id < m_low_limit_id
   → m_ids에 trx_id가 있으면: 아직 커밋 안 됨 → 안 보임
   → m_ids에 trx_id가 없으면: 커밋 완료됨 → 보임

4. trx_id == m_creator_trx_id
   → 자기 자신의 변경 → 보임

구체적 예시

PLAINTEXT
시간 순서:
  trx 5: INSERT INTO users VALUES (1, '김철수');  COMMIT;
  trx 10: BEGIN;
  trx 12: BEGIN;
  trx 10: UPDATE users SET name='김영희' WHERE id=1;
  trx 12: SELECT name FROM users WHERE id=1;  ← 여기서 Read View 생성

trx 12의 Read View:
  m_ids = [10]          (trx 10이 아직 활성)
  m_up_limit_id = 10
  m_low_limit_id = 13   (다음 할당될 trx id)
  m_creator_trx_id = 12

행의 현재 버전: name='김영희', trx_id=10

판단:
  trx_id(10) >= m_up_limit_id(10) → 조건 3으로
  m_ids에 10이 있는가? → 있음 → 안 보임!
  → Undo Log에서 이전 버전으로 이동

이전 버전: name='김철수', trx_id=5
  trx_id(5) < m_up_limit_id(10) → 보임!
  → 결과: '김철수'

격리 수준별 Read View 동작

READ COMMITTED

** 매 SELECT마다** 새로운 Read View를 생성합니다.

SQL
-- Session A
BEGIN;
UPDATE users SET name = '김영희' WHERE id = 1;

-- Session B (READ COMMITTED)
BEGIN;
SELECT name FROM users WHERE id = 1;  -- Read View 생성 → '김철수' (A 미커밋)

-- Session A
COMMIT;

-- Session B
SELECT name FROM users WHERE id = 1;  -- 새 Read View 생성 → '김영희' (A 커밋됨)
-- 같은 트랜잭션 내에서 결과가 달라짐! (Non-Repeatable Read)

REPEATABLE READ (InnoDB 기본값)

** 트랜잭션의 첫 SELECT에서만** Read View를 생성하고, 끝까지 유지합니다.

SQL
-- Session A
BEGIN;
UPDATE users SET name = '김영희' WHERE id = 1;

-- Session B (REPEATABLE READ)
BEGIN;
SELECT name FROM users WHERE id = 1;  -- Read View 생성 → '김철수'

-- Session A
COMMIT;

-- Session B
SELECT name FROM users WHERE id = 1;  -- 기존 Read View 재사용 → '김철수'
-- 같은 트랜잭션 내에서 결과가 항상 동일! (Repeatable Read 보장)
COMMIT;

비교 정리

READ COMMITTEDREPEATABLE READ
Read View 생성매 SELECT마다첫 SELECT 시 1회
Non-Repeatable Read발생 가능발생하지 않음
Phantom Read발생 가능InnoDB에서는 MVCC로 방지
용도최신 데이터 필요 시일관된 읽기 필요 시

Consistent Read (일관된 읽기)

MVCC를 통한 읽기를 Consistent Read(일관된 읽기) 라 합니다.

SQL
-- 일반 SELECT는 Consistent Read (락 없음)
SELECT * FROM users WHERE id = 1;

-- 락을 거는 읽기 (MVCC가 아닌 현재 버전 읽기)
SELECT * FROM users WHERE id = 1 FOR SHARE;      -- 공유 락
SELECT * FROM users WHERE id = 1 FOR UPDATE;     -- 배타 락

FOR SHARE/FOR UPDATE를 사용하면 MVCC가 아닌 현재 최신 버전 을 읽으며, 해당 행에 락을 겁니다.

장기 트랜잭션의 위험

MVCC에서 장기 실행 트랜잭션은 심각한 문제를 일으킬 수 있습니다.

PLAINTEXT
트랜잭션 A: BEGIN; (2시간 전에 시작, 커밋 안 함)
  → A의 Read View가 참조할 수 있는 모든 이전 버전을 유지해야 함
  → 2시간 동안의 모든 UPDATE/DELETE의 Undo Log가 purge 불가
  → Undo Log 크기 급증 → 디스크 공간 부족, 성능 저하
SQL
-- 장기 트랜잭션 확인
SELECT trx_id, trx_state, trx_started,
       TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS duration_seconds
FROM information_schema.INNODB_TRX
ORDER BY trx_started;

-- Undo Log 크기 확인
SHOW ENGINE INNODB STATUS\G
-- History list length: 이 값이 계속 증가하면 purge가 밀리고 있는 것

예방책:

  • 트랜잭션을 가능한 짧게 유지합니다
  • wait_timeout, interactive_timeout을 적절히 설정합니다
  • 모니터링으로 장기 트랜잭션을 조기 감지합니다

UPDATE/DELETE에서의 MVCC

UPDATE는 다음 순서로 처리됩니다.

PLAINTEXT
1. 해당 행에 배타 락(X Lock) 설정
2. 현재 버전을 Undo Log에 복사
3. 행의 데이터를 새 값으로 변경
4. DB_TRX_ID를 현재 트랜잭션 ID로 업데이트
5. DB_ROLL_PTR이 Undo Log의 이전 버전을 가리키도록 설정

DELETE는 실제 삭제가 아닌 ** 삭제 플래그(delete mark)** 를 설정합니다. 실제 물리적 삭제는 purge 스레드 가 나중에 처리합니다.

주의할 점

장기 트랜잭션이 Undo Log를 폭발시킨다

BEGIN만 해놓고 커밋하지 않은 트랜잭션이 있으면, 그 시점 이후의 모든 Undo Log를 purge할 수 없습니다. 수 시간 방치되면 Undo 테이블스페이스가 수 GB로 커지고, MVCC 조회 시 긴 버전 체인을 따라가야 하므로 SELECT 성능도 저하됩니다.

FOR UPDATE는 MVCC가 아니라 Current Read다

일반 SELECT는 스냅샷을 읽지만, FOR UPDATE는 최신 커밋된 데이터를 읽습니다. 같은 트랜잭션 안에서 일반 SELECT와 FOR UPDATE의 결과가 다를 수 있어 혼란을 일으킵니다.

SQL
-- RR 격리 수준에서
SELECT balance FROM accounts WHERE id = 1;           -- 1000 (스냅샷)
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE; -- 500  (Current Read!)

History list length를 모니터링해야 한다

SHOW ENGINE INNODB STATUS에서 History list length 값이 계속 증가하면 purge가 밀리고 있다는 신호입니다. 장기 트랜잭션을 찾아 종료해야 합니다.

정리

항목설명
MVCC 핵심Undo Log에 이전 버전 보관, 읽기와 쓰기가 서로 비차단
Read View트랜잭션이 볼 수 있는 데이터 버전 범위를 결정하는 스냅샷
RC vs RRRC는 매 SELECT마다 새 Read View, RR은 첫 SELECT에서만 생성
Consistent Read일반 SELECT, 락 없이 스냅샷 읽기
Current ReadFOR UPDATE/FOR SHARE, 최신 데이터 읽기 + 락
장기 트랜잭션 위험Undo Log purge 차단 → 디스크 증가 + 성능 저하
댓글 로딩 중...