본문으로 건너뛰기 데이터베이스 트랜잭션 심화 — MVCC·2PL·데드락·WAL·실무 패턴

데이터베이스 트랜잭션 심화 — MVCC·2PL·데드락·WAL·실무 패턴

데이터베이스 트랜잭션 심화 — MVCC·2PL·데드락·WAL·실무 패턴

이 글의 핵심

ACID와 격리 수준을 넘어 MVCC·2PL·데드락 탐지·WAL·운영 패턴까지: 트랜잭션이 실제로 어떻게 직렬 가능성을 보장하고 장애 후에도 복구되는지 정리합니다.

들어가며

트랜잭션(transaction)은 여러 읽기·쓰기를 하나의 논리 단위로 묶어 원자성(atomicity), 일관성(consistency), 격리성(isolation), 지속성(durability)—즉 ACID—를 만족시키려는 DB의 핵심 추상화입니다. 애플리케이션에서는 BEGIN/COMMIT/ROLLBACK 한 줄로 끝나 보이지만, 스토리지 엔진은 동시에 실행되는 다른 트랜잭션과의 간섭, 장애 시 복구, 영속성 보장을 위해 MVCC, 락 프로토콜, 로그 구조를 조합합니다.

이 글은 ORM 사용법이 아니라 엔진이 내부적으로 어떤 트레이드오프를 두는지를 짚습니다. 운영 중 겪는 유령 읽기·반복 불가 읽기·데드락·복제 지연 같은 현상을 모델로 연결하면, 인덱스 설계와 쿼리 순서만으로는 설명되지 않는 문제를 줄일 수 있습니다.


선행 개념: 격리 수준과 이상 현상

SQL 표준은 격리 수준(isolation level)을 통해 허용되는 이상 현상(anomaly)을 정의합니다. 실무에서 자주 쓰는 이름은 다음과 같습니다.

이상 현상의미
더티 읽기(dirty read)아직 커밋되지 않은 다른 트랜잭션의 쓰기를 읽음
비반복 읽기(non-repeatable read)같은 행을 두 번 읽었을 때 값이 바뀜
유령 읽기(phantom read)같은 범위를 두 번 읽었을 때 행 집합이 달라짐

Serializable에 가까울수록 동시 실행이 직렬 스케줄(serial schedule)과 동등한 결과에 가깝게 제한됩니다. 반대로 격리를 느슨하게 하면 처리량은 나아질 수 있지만 애플리케이션 불변 조건(invariant)을 코드나 제약으로 보강해야 합니다.

아래에서 MVCC2PL은 이 격리·일관성을 구현하는 대표적인 두 축—버전 관리락 프로토콜—입니다.


MVCC(Multi-Version Concurrency Control)

MVCC는 한 논리 행(row)에 대해 여러 버전의 물리 표현을 유지하여, 읽기 트랜잭션이 쓰기와 불필요하게 블로킹되지 않도록 하는 동시성 제어 방식입니다. “현재 커밋된 스냅샷”이 아닌 시점의 일관된 읽기를 제공하는 데 유리합니다.

스냅샷과 트랜잭션 ID

대표적인 구현(예: PostgreSQL, InnoDB)은 트랜잭션 ID(xid) 또는 시스템 변경 번호(SCN 등)어떤 버전이 “내게 보이는가”를 판정합니다. 읽기는 보통 특정 스냅샷 기준으로 가장 최신의 가시 버전을 고릅니다. 쓰기는 새 버전을 추가하며, 이전 버전은 정리(vacuum / purge)될 때까지 남을 수 있습니다.

왜 중요한가: 장시간 읽기 보고서 쿼리가 쓰기를 막지 않도록 설계할 수 있지만, 오래된 스냅샷을 유지하면 정리가 지연되어 디스크·버전 체인 부담이 커질 수 있습니다.

가시성 규칙과 “과거의 행”

MVCC에서 한 트랜잭션이 버전 (v)을 볼 수 있는지는 대략 다음을 포함합니다(제품·격리 수준에 따라 세부 규칙이 다름).

  • 버전을 만든 트랜잭션이 커밋되었는가
  • 그 커밋이 내 스냅샷 기준으로 가시적인가
  • 삭제·갱신으로 무효화된 버전은 아닌가

이 판정이 인덱스만 스캔해도 발생하므로, 같은 PK를 여러 번 읽어도 값이 바뀌는 것처럼 보이는 현상은 격리 수준·스냅샷 경계와 함께 이해해야 합니다.

정리(VACUUM / Purge)와 테이블 팽창

버전이 쌓이면 Dead tuple, undo log 체인, 인덱스 엔트리가 증가합니다. PostgreSQL에서는 VACUUM, InnoDB에서는 purge 스레드가 오래된 버전을 제거합니다. 장시간 트랜잭션(열린 읽기, 방치된 세션)은 정리를 막아 저장 공간을 고정적으로 잡아먹는 원인이 됩니다.

MVCC만으로 해결되지 않는 것

MVCC는 읽기·쓰기 경합을 완화하지만, 동일 행에 대한 동시 갱신, 유니크 제약 검증, SELECT ... FOR UPDATE 같은 명시적 잠금 구간에서는 또는 충돌 검사가 필요합니다. “MVCC라서 락이 없다”로 이해하면 데드락이나 직렬화 실패를 잘못 진단하기 쉽습니다.

Serializable Snapshot Isolation(SSI)에 대해

PostgreSQLSERIALIZABLE은 스냅샷만으로는 부족한 쓰기 간 충돌의존 그래프( rw-dependency )로 추적해, 커밋 시점에 직렬화 불가로 판단하는 SSI 계열 구현을 사용합니다. 그 결과 성공한 트랜잭션은 “직렬 스케줄과 동등한 결과”에 더 가깝게 제한되지만, 재시도가 늘 수 있습니다. 제품마다 SERIALIZABLE의 엄격함·오류 코드·재시도 권장이 다르므로 해당 DB 매뉴얼의 격리 수준 장을 함께 보는 것이 좋습니다.


2단계 락킹(Two-Phase Locking, 2PL)

2PL은 트랜잭션이 락을 획득하는 구간해제하는 구간을 두 단계로 나누는 규칙입니다.

  1. 성장 단계(growing phase): 필요한 공유(S) / 배타(X) 락 등을 획득만 할 수 있고 해제하지 않음
  2. 수축 단계(shrinking phase): 해제만 할 수 있고 새로 획득하지 않음

Strict 2PL커밋/롤백 시점까지 배타 락을 유지하는 변형으로, 스케줄이 직렬 가능(serializable)함을 보장하는 전통적인 증명 경로와 연결됩니다.

공유(S) / 배타(X) 락의 전형적인 호환 규칙은 다음과 같습니다(개념 모델).

이미 S이미 X
S 요청호환대기
X 요청대기대기

읽기만 필요하면 S로 경합을 줄이며, 갱신이 확정되면 X로 승격하는 패턴이 흔합니다. 실제 엔진은 행·페이지·갭 단위로 세분화하며, 의도 락(intent lock)으로 계층 간 충돌을 줄입니다.

격리 수준과 락의 대응(개념)

실제 DB는 MVCC와 락을 혼합합니다. 예를 들어 Repeatable Read에서는 같은 읽기가 반복되도록 스냅샷을 고정하며, 쓰기 충돌이나 갭(gap)에는 Next-Key / Gap Lock(InnoDB) 같은 메커니즘이 개입합니다. Serializable은 스냅샷만으로 부족한 경우 직렬화 실패를 유발하거나 더 강한 락을 사용하기도 합니다.

실무에서의 함의

  • 락 순서를 통일하면 데드락 확률을 줄일 수 있습니다(예: 항상 userorder 행 순).
  • 인덱스가 없는 조건으로 갱신하면 범위 락이 커져 동시성이 급격히 나빠질 수 있습니다.
  • 짧은 트랜잭션이 2PL·MVCC 모두에서 정석인 이유는 락 보유 시간버전·undo 유지 시간을 줄이기 위해서입니다.

데드락 탐지 및 해결

데드락(deadlock)은 트랜잭션들이 서로가 보유한 락을 기다리는 순환 대기가 생긴 상태입니다. MVCC가 있어도 쓰기 락·갭 락·DDL 등에서 발생합니다.

탐지: 대기 그래프와 타임아웃

  • 대기 그래프(wait-for graph): 트랜잭션을 노드, “A가 B가 가진 자원을 기다림”을 간선으로 두고 사이클이 있으면 데드락으로 판단합니다.
  • 타임아웃: 구현이 단순하지만 정상적으로 오래 걸리는 작업까지 끊을 위험이 있어 보조적으로 쓰는 경우가 많습니다.

해결: 희생자(victim) 선택과 롤백

엔진은 한 트랜잭션을 강제 중단(rollback)하여 사이클을 깹니다. 선택 기준은 작업량, 우선순위, 락 개수 등 제품별 휴리스틱입니다. 애플리케이션은 deadlock detected류 오류를 재시도할 수 있게 설계하는 것이 일반적입니다.

예방 패턴

  • 같은 순서로 여러 테이블·행에 락을 잡기
  • 불필요한 SELECT ... FOR UPDATE 제거, 필요한 인덱스로 락 범위 최소화
  • 배치 작업을 피크 시간과 분리
  • 애플리케이션 레벨 락(Redis 등)을 쓸 때는 만료·펜스 토큰으로 좀비 락을 방지

Write-Ahead Logging(WAL)

WAL데이터 파일을 디스크에 쓰기 전에, 변경 내역을 순차 로그에 먼저 내구성 있게 기록하는 방식입니다. 핵심 이유는 장애 복구(crash recovery)그룹 커밋을 통한 처리량입니다.

로그 선행 기록이 필요한 이유

메모리 버퍼에만 있는 페이지는 전원 손실 시 사라집니다. WAL이 LSN(log sequence number) 순서로 안정 저장되면, 재시작 시 REDO로 커밋된 변경을 재적용하며, UNDO로 미완료 트랜잭션을 되돌릴 수 있습니다(엔진별로 UNDO는 undo 세그먼트·rollback 등으로 구현).

체크포인트(checkpoint)

WAL이 무한정 길어지면 재시작 시 복구 시간(RTO)이 늘어납니다. 체크포인트는 더티 페이지를 데이터 파일에 반영하고 재생 시작점을 앞당겨 WAL을 순환·아카이브할 수 있게 합니다.

fsync, 배치, 그룹 커밋

COMMIT은 결국 로그가 안전 매체에 기록되었음을 의미해야 합니다. fsync 비용이 크기 때문에 DB는 그룹 커밋으로 여러 트랜잭션의 로그 flush를 묶습니다. 반대로 동기 복제강한 내구성 설정지연을 늘릴 수 있어 SLA와 맞춰야 합니다.

백업·PITR과의 관계

PostgreSQL의 아카이브 WAL처럼 WAL을 보존하면 특정 시점 복구(PITR)가 가능합니다. 운영에서는 보관 정책, 복제 슬롯, 디스크 용량을 함께 설계합니다.


프로덕션 트랜잭션 패턴

아래는 애플리케이션에서 격리·잠금을 드러내는 예시입니다(문법은 DB별로 다를 수 있음).

-- 세션 단위로 격리 수준을 올릴 때(필요할 때만)
BEGIN;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;

SELECT balance FROM accounts WHERE id = 1 FOR UPDATE;
-- 잔액 검증 후 갱신: 동일 행에 배타 락이 잡힌 상태에서 수행
UPDATE accounts SET balance = balance - 100 WHERE id = 1 AND balance >= 100;
COMMIT;

FOR UPDATE다른 트랜잭션이 같은 행을 동시에 갱신하는 경쟁을 줄이지만, 데드락 가능성은 남습니다. 조건부 단일 UPDATE(WHERE ... AND balance >= ?)만으로도 불변 조건을 지킬 수 있으면 락 범위를 줄일 수 있습니다.

짧고 명확한 경계

네트워크 호출·대용량 처리·외부 API는 트랜잭션 밖으로 빼으며, DB 안에서는 필요한 행만 빠르게 갱신합니다. 긴 트랜잭션은 락·버전·복제 지연을 동시에 악화시킵니다.

멱등성(idempotency)과 재시도

네트워크 타임아웃 후 같은 요청이 두 번 올 수 있습니다. 자연 키·idempotency key·UNIQUE 제약으로 “한 번만 반영”을 보장하며, 데드락·직렬화 실패지수 백오프 재시도를 적용합니다.

SELECT ... FOR UPDATE와 업무 규칙

잔액·재고처럼 읽은 값에 의존한 갱신은 스냅샷만으로는 경쟁 조건이 남을 수 있습니다. 같은 행을 잠근 뒤 갱신하거나, 단일 SQL(UPDATE ... WHERE quantity >= ?)로 조건을 한 번에 검증하는 패턴이 안전합니다.

격리 수준 선택

Read Committed가 기본인 경우가 많습니다. 보고서·집계에서 Repeatable Read / Serializable이 필요하면 명시적으로 올리으며, 직렬화 실패(40001 등) 처리 경로를 마련합니다.

분산 환경에서의 한계

DB 트랜잭션은 보통 단일 클러스터 경계에서 강합니다. 마이크로서비스 간 일관성은 Saga, Outbox, 이벤트 소싱 등 패턴과 함께 설계합니다.


트러블슈팅 체크리스트

증상점검
데드락 빈번락 순서, 인덱스 부재, 불필요한 범위 잠금
유령 읽기 의심격리 수준, 스냅샷 경계, 동일 트랜잭션 내 반복 조건
디스크 급증장시간 트랜잭션, vacuum/purge 지연, WAL 보존
커밋 지연동기 복제·fsync 설정, 디스크 I/O 병목
복제 지연대량 쓰기, 핫 로우, 단일 스레드 적용 병목

마무리

MVCC는 읽기 일관성과 처리량을, 2PL과 그 변형은 직렬성·충돌 제어를, WAL내구성과 복구를 담당합니다. 데드락은 이들이 한 워크로드에서 만날 때 발생하며, 완화책은 짧은 트랜잭션·일관된 락 순서·적절한 인덱스·재시도 가능한 애플리케이션에 있습니다.

프로덕션에서는 “기본 격리 + 명시적 상위 격리”, “짧은 트랜잭션 + 멱등 키”, “WAL·체크포인트·복제 지연 모니터링”을 함께 두며, 장애 시 어떤 불변 조건을 DB가 보장하고 어떤 것을 애플리케이션이 보강하는지를 팀 합의로 남기는 것이 안전합니다.


자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. 데이터베이스 트랜잭션 ACID 속성과 격리 수준 완벽 이해. Dirty Read·Phantom Read·Lost Update 현상과 해결 전략, 락·MVCC·DeadLock 회피까지 실전 가이드. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.


이 글에서 다루는 키워드 (관련 검색어)

데이터베이스, 트랜잭션, MVCC, WAL, 동시성, PostgreSQL, SQL 등으로 검색하시면 이 글이 도움이 됩니다.