PostgreSQL MVCC와 트랜잭션 격리 수준

PostgreSQL의 MVCC는 어떻게 동작하나요?

PostgreSQL의 MVCC(Multi-Version Concurrency Control, 다중 버전 동시성 제어)는 각 트랜잭션이 데이터의 스냅샷(snapshot)을 읽음으로써 lock 없이 동시성을 확보하는 메커니즘입니다. 동일 행(row)에 대해 여러 버전을 유지하며, 트랜잭션 격리 수준에 따라 어느 버전을 읽을지 결정합니다. 이를 통해 읽기 작업이 쓰기 작업을 차단하지 않고, 높은 동시성을 달성합니다.

PostgreSQL의 모든 행에는 두 개의 숨겨진 시스템 컬럼이 존재합니다: xmin(행을 생성한 트랜잭션 ID)과 xmax(행을 삭제한 트랜잭션 ID). 이들이 각 트랜잭션의 visibility 판단 기준이 됩니다.

트랜잭션이 시작될 때 PostgreSQL은 현재 활성 트랜잭션 목록을 캡처하여 스냅샷을 생성합니다. 트랜잭션 T1이 행 R을 읽을 때, 다음 규칙이 적용됩니다:

  • xmin이 T1의 트랜잭션 ID보다 작고, xmax가 없거나 T1보다 크면 해당 버전은 T1에 보입니다.
  • 그 외의 경우 행 버전은 T1에 보이지 않으며, 이전 버전(tuple chain)을 탐색합니다.

이러한 메커니즘으로 인해 PostgreSQL은 reader가 writer를 blocking하지 않는 "non-blocking read" 모델을 구현합니다. 그 대신 주기적인 VACUUM 작업(dead tuple 정리)이 필요하며, 이는 일반적으로 autovacuum 프로세스가 자동으로 수행합니다.

PostgreSQL의 네 가지 트랜잭션 격리 수준은 무엇인가요?

PostgreSQL은 SQL 표준에 정의된 4가지 격리 수준을 지원합니다. 다음은 각 수준의 작동 방식과 이상 현상(anomaly) 방지 여부입니다:

격리 수준 설정 코드 Dirty Read Non-repeatable Read Phantom Read 특징
READ UNCOMMITTED READ UNCOMMITTED 방지* 미방지 미방지 PostgreSQL에서는 READ COMMITTED로 상향 적용
READ COMMITTED READ COMMITTED 방지 미방지 미방지 기본값; 각 쿼리 실행 시마다 스냅샷 갱신
REPEATABLE READ REPEATABLE READ 방지 방지 미방지** 트랜잭션 시작 시점의 스냅샷 사용
SERIALIZABLE SERIALIZABLE 방지 방지 방지 SSI(Serializable Snapshot Isolation) 구현

*PostgreSQL은 SQL 표준과 달리 READ UNCOMMITTED 요청 시 내부적으로 READ COMMITTED 동작을 수행합니다(공식 문서 7.4절).

**PostgreSQL의 REPEATABLE READ는 SQL 표준의 REPEATABLE READ보다 강력한 격리를 제공하며, 표준의 Phantom Read를 대부분의 경우 방지합니다.

READ COMMITTED는 어떻게 작동하나요?

READ COMMITTED는 PostgreSQL의 기본 격리 수준입니다. 이 모드에서 각 SQL 문(statement)이 실행될 때마다 새로운 스냅샷을 획득합니다. 따라서 한 트랜잭션 내에서도 각 쿼리는 최신의 커밋된 데이터를 읽을 수 있습니다.

예시:

  • 트랜잭션 T1: SELECT COUNT(*) FROM orders; → 결과 100
  • 트랜잭션 T2: 새 주문 10개 INSERT + COMMIT
  • 트랜잭션 T1: SELECT COUNT(*) FROM orders; → 결과 110 (같은 트랜잭션이지만 다른 결과)

READ COMMITTED는 non-repeatable read(동일 행을 두 번 읽을 때 값이 변할 수 있음)가 발생할 수 있으므로, 금융 거래처럼 같은 데이터를 여러 번 검증해야 하는 업무에는 부적합합니다. 반면 높은 동시성(throughput)이 필요한 경우에 적합합니다.

REPEATABLE READ는 어떻게 작동하나요?

REPEATABLE READ는 트랜잭션 시작 시점의 스냅샷을 고정하고, 트랜잭션 종료까지 그 스냅샷을 유지합니다. 이를 통해 동일 행을 여러 번 읽어도 같은 값을 얻을 수 있습니다.

PostgreSQL의 구현:

  • 트랜잭션 시작 시 xmin 스냅샷을 확정
  • 이후 모든 쿼리는 그 스냅샷을 기준으로 visibility 판단
  • 다른 트랜잭션의 커밋 결과는 보이지 않음

예시:

  • T1 시작: BEGIN ISOLATION LEVEL REPEATABLE READ;
  • T1: SELECT balance FROM account WHERE id=1; → 1000
  • T2: UPDATE account SET balance=900 WHERE id=1; COMMIT; (T2 수행)
  • T1: SELECT balance FROM account WHERE id=1; → 1000 (여전히 같은 값)

Phantom read(범위 쿼리에서 새 행이 보이는 현상)는 제한된 조건 하에서만 발생합니다. 예를 들어, 집계 함수(aggregate function)를 사용한 경우에는 PostgreSQL의 REPEATABLE READ가 phantom read를 방지합니다.

SERIALIZABLE은 어떻게 작동하나요?

SERIALIZABLE은 PostgreSQL 9.1 이후 SSI(Serializable Snapshot Isolation)로 구현되며, 데이터 간 의존성을 추적하여 serialization 위반을 감지하고 충돌 트랜잭션을 abort 시킵니다.

SSI의 작동 원리:

  1. 읽기 의존성(rw-dependency) 및 쓰기 의존성(ww-dependency) 추적
  2. 위험한 구조(dangerous structure) 감지 시 한 트랜잭션을 rollback
    3 실제 lock 기반 직렬화(2PL)보다 non-blocking read 유지

SSI는 높은 격리 수준을 제공하지만 retry 로직 구현이 필수입니다. 트랜잭션이 serialization conflict 오류로 abort될 수 있기 때문입니다.

실무에서 격리 수준의 선택 기준은 무엇인가요?

격리 수준 선택은 데이터 일관성 요구사항과 동시성 처리량(throughput) 간의 trade-off입니다.

금융 거래, 재고 관리 등 높은 일관성이 필수인 경우:

  • REPEATABLE READ 또는 SERIALIZABLE 권장
  • 단, SERIALIZABLE 사용 시 retry 메커니즘 필수

고빈도 조회 위주의 시스템(예: 로그 저장, 분석):

  • READ COMMITTED로 충분하며 성능 우수

혼합 업무:

  • 기본값 READ COMMITTED 유지하되, 특정 트랜잭션에만 REPEATABLE READ 적용

정리하면 어떤가요?

PostgreSQL의 MVCC는 행별 버전 관리를 통해 lock 없는 동시성을 달성하는 핵심 메커니즘입니다. 네 가지 격리 수준은 각각 다른 visibility rule을 적용하며, 이상 현상(dirty read, non-repeatable read, phantom read) 방지 정도가 다릅니다.

  • READ COMMITTED(기본값): 높은 처리량, 낮은 일관성
  • REPEATABLE READ: 균형잡힌 격리 + 성능
  • SERIALIZABLE: 최고 격리 + 낮은 처리량 (conflict 발생 시 retry 필요)

격리 수준 선택은 애플리케이션의 데이터 무결성 요구사항에 따라 결정되어야 하며, 성능 측정을 통해 검증이 필수입니다.

자주 묻는 질문

MVCC에서 dead tuple 증가 시 성능이 저하되는 이유는 무엇인가요?

MVCC는 delete 연산을 물리적 삭제가 아닌 xmax 설정으로 처리합니다(logical deletion). 따라서 쿼리가 스냅샷에 보이지 않는 dead tuple을 scan해야 하며, 인덱스도 dead tuple을 포함한 모든 버전을 관리합니다. 결과적으로 seq scan 시간과 index size가 증가하고, visibility check 연산이 누적됩니다. 이를 방지하기 위해 autovacuum이 정기적으로 dead tuple을 제거하며, 고빈도 delete 작업이 있는 테이블은 vacuum 설정(autovacuum_vacuum_scale_factor, autovacuum_analyze_scale_factor)을 조정하여 튜닝할 수 있습니다.

REPEATABLE READ에서 발생할 수 있는 phantom read는 어떤 경우인가요?

PostgreSQL의 REPEATABLE READ는 스냅샷 기준 visibility를 사용하므로, 대부분의 phantom read를 방지합니다. 다만 다음 두 경우 제한된 형태의 phantom이 발생할 수 있습니다: (1) FOR UPDATE/FOR SHARE 명시적 lock 없이 범위 갱신 후 재조회, (2) 외부 데이터 또는 다른 세션의 DDL(예: 테이블 추가)로 인한 스키마 변경. 대부분의 실무 업무에서는 이를 무시할 수 있으므로, REPEATABLE READ는 SERIALIZABLE보다 강력한 보장을 제공합니다.

SERIALIZABLE 격리 수준 사용 시 주의할 점은 무엇인가요?

SERIALIZABLE은 SSI 기반 conflict 감지를 사용하므로, serialization conflict 발생 시 트랜잭션이 rollback됩니다. 클라이언트는 SQLSTATE 40001(serialization_failure) 오류를 받으면 트랜잭션을 재시도(retry)해야 합니다. 재시도 로직이 없으면 사용자에게 오류가 노출되므로, 애플리케이션 레벨의 retry 메커니즘(exponential backoff 권장)이 필수입니다. 또한 conflict가 빈번하면 처리량이 급락할 수 있으므로, 사전에 성능 테스트가 필수입니다.

같은 테이블의 다른 행에 대해 REPEATABLE READ와 SERIALIZABLE 중 어느 것을 선택해야 하나요?

데이터 무결성 요구사항에 따라 결정합니다. (1) 단순 범위 조회 + 특정 행만 갱신하는 경우: REPEATABLE READ로 충분하며, explicit lock(FOR UPDATE)을 추가하면 더욱 안전합니다. (2) 복수 행 간 복잡한 의존성(예: 계정 간 송금)이 있는 경우: SERIALIZABLE 사용을 권장하되, retry 로직 비용 대비 이득을 측정해야 합니다. 일반적으로 REPEATABLE READ + 필요시 명시적 lock이 가장 실용적인 균형입니다.

PostgreSQL과 다른 DBMS(MySQL, Oracle)의 격리 수준 구현이 다른 이유는 무엇인가요?

PostgreSQL은 완전한 MVCC 구현으로 all readers are non-blocking을 달성하는 반면, MySQL(InnoDB)은 lock 기반 격리(2PL, Two-Phase Locking)를 기본으로 합니다. Oracle은 undo segment 기반 MVCC를 사용하지만 PostgreSQL과는 snapshot 정책이 다릅니다. 이로 인해 동일 격리 수준 이름이어도 실제 anomaly 방지 정도와 성능 특성이 상이하므로, DBMS별로 격리 수준 문서를 확인해야 합니다. 참고로 PostgreSQL의 REPEATABLE READ는 SQL 표준보다 강력한 보장을 제공합니다(PostgreSQL 공식 문서).

관련 글