PostgreSQL MVCC: 덮어쓰지 않는 데이터베이스가 동시성을 제어하는 법

2026. 3. 31. 17:19CS/DB

이번 글에서는 사내에서 높은 신뢰성과 확장성, 그리고 강력한 데이터 무결성을 위해 널리 채택하고 있는 PostgreSQL에 대해 학습한 내용을 공유하고자 합니다. 특히 PostgreSQL의 핵심 메커니즘 중 하나인 MVCC(Multi-Version Concurrency Control)의 동작 원리를 깊이 있게 파악하는 데 초점을 맞추었습니다


1. PostgreSQL의 특징

PostgreSQL는 다른 RDBMS와 비교했을 때 가장 큰 장점은 '타협하지 않는 엄격함'입니다. 이와 관련된 여러가지 특징이 있습니다. 

  • 향상된 체크 제약 조건(Check Constraints): not null 뿐만아니라 아주 복잡한 조건을 DB 레벨에서 강제할 수 있습니다. 

예를 들어 "재고 수량은 반드시 0보다 커야 하며, 입고일은 출고일보다 빨라야 한다"는 조건을 설정하면, 어떤 애플리케이션 버그로 인해 잘못된 값이 들어와도 DB가 잘못된 값이 들어오지 못하도록 막습니다. 

  • 원자성 보장(Write-Ahead Logging): 데이터를 디스크에 실제로 쓰기 전에, 변경 이력을 로그에 먼저 기록합니다. 

만약 데이터를 저장하는 도중에 갑자기 서버 전원이 나가더라도, 로그를 보고 어디까지 저장됐는지를 복구할 수 있어 데이터 유실 가능성을 낮출 수 있습니다. 

  • 동시성 제어 (MVCC)의 정교함: PostgreSQL은 데이터를 수정할 때 원본을 바로 덮어쓰지 않고 새로운 버전을 만듭니다. 

덕분에 읽기 작업이 진행중일 때 쓰기 작업이 들어와도 서로를 기다리지 않고  자신이 작업을 시작한 시점에 일관된 데이터를 볼 수 있습니다. 

이 글에서는 3가지 큰 특징 중에서도 MVCC에 대해서 심층도 있게 학습한 내용을 공유 해보려고 합니다. 


2. PostgreSQL의 MVCC

PostgreSQL의 MVCC 핵심 메커니즘을 이해하기 위해서는 3가지 요소에 대해서 이해해야합니다. 

  • 스냅샵(SnapShot)과 튜플(Tuple)의 구조
  • 수정(update)시 발생하는 과정: "insert +delete"
  • 청소부 역할: VACCUM

먼저 스냅샷과 튜플의 구조에 대해서 이해해보겠습니다. PostgreSQL에서 테이블의 한 행을 저장하는 단위를 튜플이라고 부릅니다. 각 튶플에는 눈에 보이지 않는 두 가지 중요한 타임스탬프 정보가 숨어있습니다. 

  • xmin (insert Id): 이 데이터를 생성한 트랜잭션의 ID
  • xmax (delete Id): 이 데이터를 삭제하거나 수정한 트랜잭션의 ID (수정하지 않았다면 0) 

xmin, xmax는 PostgreSQL의 오픈소스에서 확인해볼수 도 있는데 구조체 내의 멤버변수를 의미합니다. (네이밍에 대해서 궁금해서 찾아봤습니다. ) 
 
두번째는 수정시 발생하는 과정(VACCUM 포함)에 대해서 심층도 있게 보려고합니다. PostgreSQL는 데이터를 수정할 때 기존 데이터를 직접 고치지 않고 다음 4단계를 거칩니다. 

1단계: 초기 상태 (데이터 삽입 직후)

가장 먼저 데이터가 처음 저장된 상태입니다. 모든 데이터 행(Tuple)은 눈에 보이지 않는 xmin(생성 트랜잭션 ID)과 xmax(삭제/수정 트랜잭션 ID) 정보를 가지고 있습니다.

  • xmin: 100 (100번 트랜잭션이 만들었음)
  • xmax: 0 (아직 삭제되거나 수정되지 않은 유효한 데이터)

 

2단계: 데이터 수정 (UPDATE) 발생

이제 101번 트랜잭션이 Alice의 잔액을 200으로 수정하는 UPDATE 명령을 내립니다.
그림을 보면 원본(Tuple A)은 그대로 두고, 옆에 새 데이터(Tuple B)를 추가합니다.

  • Tuple A (원본): xmax가 101로 바뀝니다. "101번 트랜잭션에 의해 삭제 예정"이라는 뜻입니다.
  • Tuple B (신규): xmin이 101인 새로운 행이 생깁니다. 잔액은 200입니다.

 

3단계: 커밋 후 가시성 (조회 시점의 차이)

101번 트랜잭션이 성공적으로 완료(Commit)되었습니다. 이제 디스크에는 두 개의 버전(A, B)이 공존합니다.
이 부분이 MVCC의 핵심입니다. 조회하는 사람이 언제 작업을 시작했느냐에 따라 보는 데이터가 다릅니다.

  • 사용자 A (102번): 데이터 수정 전에 이미 조회를 시작했으므로, 구버전인 Tuple A를 봅니다.
  • 사용자 B (103번): 수정이 끝난 후 조회를 시작했으므로, 신버전인 Tuple B를 봅니다.

결과적으로 수정 중에도 읽기 작업이 멈추지 않고 대기 없이 바로 데이터를 볼 수 있습니다.

 

4단계: 사후 정리 (VACUUM 실행)

시간이 지나서 더 이상 구버전(Tuple A)을 참조하는 트랜잭션이 하나도 없게 되었습니다. 이때 VACUUM 프로세스가 등장합니다.
그림처럼 VACUUM은 쓸모없어진 구버전을 지우고, 그 자리를 나중에 다시 사용할 수 있도록 만듭니다.

  • Tuple A (삭제): 물리적으로 지워집니다.
  • 빈 공간 (Reusable Space): 새로운 데이터가 들어올 수 있는 빈자리로 변합니다.

마무리

PostgreSQL는 MVCC 덕분에 대규모 트래픽에서도 데이터의 일관성을 유지할 수 있도록 지원해 많은 개발자로부터 선택되어 사용되고 있습니다. 이번에 처음으로 PostgreSQL를 사용하게 됬지만, "왜 PostgreSQL이어야 하지?"라는 개인적인 물음에 답을 줄수 있는 공부였던것 같습니다.