Skip to content

트랜잭션·ACID·격리 수준 (MVCC)

개요

여러 세션이 같은 데이터를 동시에 건드릴 때, PostgreSQL은 어떻게 "모두가 효율적으로 접근하면서도 데이터 정합성을 지키는" 걸까요? 이 페이지는 그 동시성 제어의 핵심인 트랜잭션·ACID·MVCC·격리 수준을 PostgreSQL 공식 문서를 기준으로 풀어요.

이 내용은 실무에서 "왜 이 UPDATE가 저 트랜잭션에 안 보이지?", "왜 serialization error가 나지?" 같은 의문을 이해하는 바탕이 돼요. 데이터베이스 애플리케이션을 다루는 개발자라면 한 번은 짚고 가야 할 영역이에요.

핵심 개념

트랜잭션과 ACID

여러 작업을 하나의 트랜잭션으로 묶으면, 전부 성공하거나 전부 취소되는 걸 보장할 수 있어요. 이 성질을 네 글자로 ACID라고 불러요.

  • 원자성(Atomicity) — 트랜잭션 안의 모든 작업은 한 덩어리로 취급돼요. 중간에 실패하면 시작 전 상태로 되돌아가요(롤백).
  • 일관성(Consistency) — 트랜잭션이 끝나면 데이터베이스의 규칙(제약)을 항상 만족하는 상태가 돼요.
  • 격리성(Isolation) — 동시에 실행되는 트랜잭션끼리 서로를 지나치게 간섭하지 않게 해요. 어느 정도 간섭을 허용할지는 "격리 수준"이 정해요.
  • 지속성(Durability) — 커밋된 트랜잭션의 결과는 장애가 나도 유지돼요.

MVCC — 버전을 남겨두는 동시성 제어

PostgreSQL이 읽기와 쓰기가 서로 막히지 않게 하는 비결이 MVCC(Multiversion Concurrency Control) 예요. 트랜잭션이 행을 지우거나 바꿀 때, 기존 값을 즉시 없애는 대신 이전 버전을 남겨둬요. 그래서 오래된 트랜잭션은 자기 시점의 예전 버전을 계속 읽을 수 있죠.

제가 트랜잭션을 "미리 약속한 스냅샷"으로 본다고 생각하면 돼요. 각 트랜잭션은 시작 시점 기준의 데이터 상태(스냅샷)를 보고, 다른 트랜잭션이 그 사이 바꾼 걸 알지 못해요. 이렇게 하니 읽는 트랜잭션과 쓰는 트랜잭션이 서로를 기다리지 않아도 돼요.

격리 수준 (Isolation Level)

격리 수준은 "동시 트랜잭션이 서로 얼마나 보이게 할지"를 정하는데, PostgreSQL 공식 문서는 3가지 수준을 제시해요.

  • Read Committed (기본값) — 각 명령(SELECT 등)이 실행되는 시점의 스냅샷을 봐요. 트랜잭션이 진행되는 동안 다른 트랜잭션의 커밋 결과가 중간에 보일 수 있어요. 대부분의 작업에 적당한 기본값이에요.
  • Repeatable Read — 트랜잭션이 시작된 시점의 스냅샷을 트랜잭션 전체에 걸쳐 봐요. 트랜잭션이 진행되는 동안 다른 트랜잭션의 변경이 보이지 않아, 같은 값을 여러 번 읽어도 일관돼요. (다만 "팬텀" 문제는 여전히 가능해요.)
  • Serializable — 트랜잭션들이 마치 하나씩 순서대로 실행된 것처럼 보이게 해요. 동시에 실행해도 직렬 실행과 같은 결과를 보장해요. 다만 이 수준에서는 동시성 충돌을 감지하면 트랜잭션이 실패할 수 있어요(직렬화 실패). 실패한 트랜잭션은 애플리케이션에서 다시 시도하는 흐름이 필요해요.

격리가 엄격해질수록 정합성은 좋아지지만, 동시성과 성능(충돌 재시도)의 희생이 커져요. 그래서 필요한 만큼만 엄격하게 설정하는 게 실무의 핵심이에요.

사용 사례 / 실제 적용

  • Read Committed 기본 운영 — 대부분의 서비스 쿼리는 기본값(Read Committed)을 그대로 써요. 짧은 트랜잭션에서는 반복 조회가 크게 문제되지 않는 경우가 많아요.
  • Repeatable Read로 일관된 보고서 — 한 트랜잭션 안에서 여러 번 조회하며 합계·집계를 내야 하는데 다른 트랜잭션의 변경이 중간에 섞이면 안 되는 경우에 써요.
  • Serializable로 금전·정합성 보호 — 돈이 오가거나 재고처럼 정합성이 절대적으로 중요한 작업은 Serializable로 보호하되, 직렬화 실패(serialization_failure)가 나면 재시도하는 로직을 함께 둬요.
  • 트랜잭션 + 롤백 — 구독 생성처럼 여러 단계가 얽힌 작업을 하나의 트랜잭션으로 묶어, 중간 실패 시 처음부터 다시 시작하지 않도록 보장해요.

더 알아보기