현대적인 DB는 Repeatable Read를 구현할 때 내부적으로 스냅샷(Snapshot) 방식을 사용. -> Repeatable Read를 선택하면 자동으로 유령 행 문제까지 해결
1. 고립 수준별 특징¶
| 고립 수준 | 정의 및 특징 | 문제점 (이론적 관점) |
|---|---|---|
| Read Committed | 각 쿼리가 실행되는 시점의 커밋된 데이터만 읽음. | 반복 불가능한 읽기: 한 트랜잭션 내에서 같은 쿼리를 두 번 실행했을 때 결과가 다를 수 있음. |
| Repeatable Read | 트랜잭션 내에서 한 번 읽은 행은 값이 바뀌지 않도록 보장함. | 팬텀 리드(Phantom Read): 기존 행은 보호되지만, 새로 삽입된 행(유령 행)이 나타날 수 있음. |
| Snapshot Isolation | 트랜잭션 시작 시점의 데이터 '스냅샷'을 기반으로 작업함. | 이론적으로는 팬텀 리드까지 해결하지만, Write Skew(쓰기 왜곡) 문제는 발생 가능. |
2. Write Skew 예시¶
전제 조건: 병원에는 최소 1명의 의사가 상주해야 함. (현재 아담, 밥 2명 근무 중)
트랜잭션 A (아담): 현재 의사 수를 조회함 (2명 확인). → "내가 퇴근해도 1명(밥)이 남네?" → 퇴근 처리(Update).
트랜잭션 B (밥): 동시에 의사 수를 조회함 (2명 확인). → "내가 퇴근해도 1명(아담)이 남네?" → 퇴근 처리(Update).
최종 결과: 두 트랜잭션 모두 성공적으로 커밋됨. 결과적으로 병원에는 의사가 0명이 남는 정합성 오류 발생.
이는 각 행에 대한 직접적인 수정 충돌이 아니라, "조회 결과(조건)"에 근거한 결정이 서로를 무효화하기 때문에 발생합니다.
3. Write Skew 해결책: 비관적 제어 vs 낙관적 제어¶
강연자는 실무 경험을 바탕으로 비관적 동시성 제어를 선호한다고 밝힙니다.
- 비관적 제어 (강연자 추천):
SELECT ... FOR UPDATE를 사용하여 읽는 순간 행에 잠금(Lock)을 겁니다. 다른 사람이 기다리게 하더라도 데이터가 꼬여서 나중에 트랜잭션을 재시도(Retry)하는 비용보다 낫다고 판단합니다. - 낙관적 제어 (Serializable): 일단 진행하고 마지막에 충돌을 감지합니다. 충돌 시 에러를 뱉고 애플리케이션이 다시 시도하게 만듭니다.
PostgresSQL의 MVCC 구현¶
PostgreSQL의 MVCC(Multi-Version Concurrency Control)와 스냅샷은 데이터의 가시성을 관리하여 읽기 작업이 쓰기 작업을 방해하지 않도록 설계되었습니다. 핵심은 데이터를 덮어쓰지 않고 새로운 버전을 생성하는 방식에 있습니다.
1. 데이터 저장 구조: Tuple의 숨겨진 컬럼¶
Postgres의 모든 테이블 행(Tuple)에는 MVCC 제어를 위한 메타데이터 필드가 숨겨져 있습니다.
xmin: 해당 튜플을 삽입한 트랜잭션의 ID (XID).xmax: 해당 튜플을 삭제하거나 업데이트한 트랜잭션의 ID. (삭제되지 않았다면0)ctid: 해당 튜플의 물리적 위치 정보. 업데이트 시 새로운 위치를 가리키는 포인터 역할을 합니다.
2. 스냅샷(Snapshot)의 정체¶
스냅샷은 특정 시점에 "어떤 트랜잭션이 활성 상태인가"를 기록한 데이터 구조체입니다. pg_snapshot 데이터 타입으로 표현되며, 다음과 같은 정보를 포함합니다.
xmin: 아직 실행 중인 가장 오래된 트랜잭션의 ID. 이보다 작은 XID를 가진 트랜잭션은 모두 종료(커밋/롤백)된 것으로 간주합니다.xmax: 아직 할당되지 않은 다음 트랜잭션 ID. 이보다 크거나 같은 XID를 가진 트랜잭션은 스냅샷 시점에 시작조차 하지 않은 상태입니다.xip_list:xmin과xmax사이에서 현재 활성(Running) 상태인 트랜잭션 목록입니다.
3. 가시성 판단 로직 (Visibility Check)¶
쿼리가 실행될 때, Postgres는 현재 스냅샷을 기준으로 각 튜플을 보여줄지 말지 결정합니다.
-
xmin확인: -
xmin이 아직 커밋되지 않았다면? 보이지 않음. xmin이 현재 스냅샷의xip_list에 있다면? 보이지 않음.-
xmin이 커밋되었고 스냅샷 범위 밖(이미 완료됨)이라면? 다음 단계로. -
xmax확인: -
xmax가0이거나 롤백되었다면? 보임 (유효한 데이터). xmax가 커밋되었다면? 보이지 않음 (이미 삭제된 데이터).xmax가 현재 스냅샷 시점에 아직 활성 상태라면? 보임 (아직 삭제 확정이 아님).
4. 업데이트 과정: Copy-on-Write¶
Postgres에서 UPDATE는 내부적으로 DELETE + INSERT로 작동합니다.
- 기존 행:
xmax를 현재 트랜잭션 ID로 설정하여 "삭제 예정"으로 마킹합니다. - 새로운 행: 새로운
ctid에 데이터를 복사하고xmin을 현재 트랜잭션 ID로 설정합니다. - 결과적으로 한 데이터에 대해 여러 버전이 디스크에 공존하게 되며, 스냅샷에 따라 각기 다른 버전을 보게 됩니다.
5. 정리를 돕는 도구: VACUUM¶
MVCC의 부작용은 더 이상 아무도 참조하지 않는 과거 버전(Dead Tuple)이 쌓인다는 점입니다.
- VACUUM은
xmax가 커밋되고, 현재 실행 중인 가장 오래된 스냅샷보다 더 과거의 트랜잭션에 의해 삭제된 튜플을 찾아 공간을 재사용할 수 있게 비워줍니다.
MySQL InnoDB의 MVCC 구현¶
MySQL의 InnoDB 스토리지 엔진에서 Undo Log와 Read View는 트랜잭션의 격리 수준(Isolation Level)을 보장하고, MVCC(Multi-Version Concurrency Control)를 구현하는 핵심 요소입니다.
1. Undo Log (언두 로그)¶
Undo Log는 데이터가 변경되었을 때 변경 전의 이전 데이터를 보관하는 영역입니다.
-
주요 목적:
-
트랜잭션 롤백: 사용자가
ROLLBACK을 수행하면 Undo Log에 저장된 데이터를 사용하여 변경 전 상태로 복구합니다. -
MVCC 구현: 특정 트랜잭션이 데이터를 수정 중이더라도, 다른 트랜잭션이 격리 수준에 따라 변경 전의 데이터를 읽을 수 있게 해줍니다.
-
작동 방식: 데이터를 수정(UPDATE/DELETE)할 때마다 InnoDB는 기존 레코드를 복사하여 Undo Log에 기록하고, 현재 레코드의 시스템 컬럼인
ROLL_PTR(Rollback Pointer)이 해당 Undo Log를 가리키게 합니다. 이를 통해 여러 버전의 데이터가 체인 형태로 연결됩니다.
2. Read View (리드 뷰)¶
Read View는 특정 시점에 실행 중인 트랜잭션들의 목록을 찍은 스냅샷입니다. REPEATABLE READ나 READ COMMITTED 격리 수준에서 "어떤 버전의 데이터를 보여줄 것인가"를 결정하는 기준이 됩니다.
Read View에는 다음과 같은 중요한 정보들이 포함됩니다:
- m_ids: Read View 생성 당시 활성 상태(커밋되지 않은)인 트랜잭션 ID 목록.
- min_trx_id:
m_ids중 가장 작은 트랜잭션 ID. (이보다 작은 ID는 모두 커밋된 상태) - max_trx_id: Read View 생성 당시 아직 시작되지 않은 다음 트랜잭션에 할당될 ID. (이보다 크거나 같은 ID는 모두 "미래"의 트랜잭션)
3. MVCC의 작동 원리 (Undo Log + Read View)¶
InnoDB는 데이터를 읽을 때, 해당 레코드의 DB_TRX_ID(수정한 트랜잭션 ID)와 현재 트랜잭션의 Read View를 비교하여 어떤 데이터를 보여줄지 결정합니다.
- 자신이 수정한 데이터: 레코드의
TRX_ID가 현재 내 ID와 같다면 즉시 읽습니다. - 이미 커밋된 데이터:
TRX_ID가min_trx_id보다 작다면, Read View 생성 전에 이미 커밋된 것이므로 읽습니다. - 아직 커밋되지 않았거나 미래의 데이터:
TRX_ID가m_ids에 포함되어 있거나max_trx_id보다 크다면, 현재 트랜잭션 입장에서는 볼 수 없는 데이터입니다. - Undo Log 추적: 볼 수 없는 데이터인 경우, 레코드의
ROLL_PTR을 따라 Undo Log로 거슬러 올라가며 조건에 맞는(볼 수 있는) 과거 버전의 데이터를 찾아 읽습니다.
요약 및 차이점¶
| 구분 | Undo Log | Read View |
|---|---|---|
| 역할 | 변경 전의 데이터 그 자체를 저장 | 데이터를 읽는 판단 기준(스냅샷) 제공 |
| 위치 | 시스템 테이블스페이스 / Undo 테이블스페이스 | 트랜잭션 내부 메모리 |
| 핵심 기능 | 롤백 및 과거 데이터 복원 | 트랜잭션 간의 가시성(Visibility) 제어 |
격리 수준에 따른 Read View 생성 시점¶
- READ COMMITTED: 매
SELECT쿼리마다 새로운 Read View를 생성합니다. 따라서 쿼리 실행 사이에 커밋된 데이터를 볼 수 있습니다. - REPEATABLE READ: 트랜잭션의 첫 번째
SELECT시점에 Read View를 생성하고 이를 트랜잭션 종료 시까지 유지합니다. 이로 인해 동일 트랜잭션 내에서는 항상 같은 데이터를 보게 됩니다.