트랜잭션
트랜잭션은 관련된 읽기와 쓰기를 하나의 작업 단위로 다루는 방법이다. 주문을 저장하고 재고를 줄일 때 두 쓰기를 함께 확정하는 문제와, 두 주문이 같은 재고를 동시에 읽는 문제는 구별해야 한다. 앞의 묶음을 다루는 성질이 원자성이고, 겹쳐 실행되는 작업 사이에서 어떤 값을 보며 어떤 충돌을 허용할지는 격리 수준이 정한다.
복구 문제에서 ACID로
Jim Gray의 1981년 The Transaction Concept: Virtues and Limitations는 트랜잭션을 원자성·일관성·내구성 등의 성질과 함께 설명하고, 보호된 데이터 작업과 외부 세계의 실제 행동을 구분했다. DB에 기록한 변경을 복구하는 일과 이미 배출한 현금을 되찾는 일은 다르며, 해당 DB 트랜잭션 밖에서 전달한 메시지도 별도의 전달 계약으로 다뤄야 한다.1
Härder와 Reuter는 1983년 복구 논문에서 원자성·일관성·격리성·내구성을 ACID라는 묶음으로 설명했다. 네 글자는 실패를 만나도 한 작업을 어떻게 확정하고, 함께 실행되는 작업이 어떤 관계를 유지해야 하는지 정리한다. 이 논문의 발표를 트랜잭션 자체의 발명 시점으로 보는 대신, 이미 발전하던 복구 개념을 체계화한 기록으로 읽는다.2
일관성에는 애플리케이션의 역할이 남는다. DB의 제약 조건과 트랜잭션에 적은 업무 규칙이 실제로 원하는 규칙이어야 한다. DB가 커밋을 성공시켰다는 사실만으로 잘못 작성한 가격 계산까지 고쳐주지는 않는다.
롤백하면 어느 쓰기가 돌아가는가
다음 PostgreSQL 예제는 재고 변경과 주문 기록을 같은 트랜잭션에 넣고, 마지막에 일부러 롤백한다. 재고 경쟁을 해결하는 전체 주문 API가 아니라 두 쓰기의 확정 범위를 확인하는 예제다.
CREATE TEMP TABLE stock (
id integer PRIMARY KEY,
quantity integer NOT NULL CHECK (quantity >= 0)
);
CREATE TEMP TABLE orders (
id integer PRIMARY KEY,
item_id integer NOT NULL
);
INSERT INTO stock VALUES (1, 1);
BEGIN;
UPDATE stock SET quantity = quantity - 1 WHERE id = 1;
INSERT INTO orders VALUES (100, 1);
ROLLBACK;
SELECT quantity FROM stock WHERE id = 1;
SELECT count(*) FROM orders;
결과는 재고 1, 주문 수 0이다. 트랜잭션 안에서 수행한 두 변경을 함께 취소했다. 실제 접수에서는 재고가 남아 있는지 조건부 갱신하고 영향받은 행 수를 검사한 뒤 주문을 기록해야 한다. 메일 전송처럼 이 DB 밖에서 이미 일어난 효과는 위 롤백의 대상에 들어가지 않는다.
격리 수준의 이름만으로는 부족했던 이유
SQL의 격리 수준은 dirty read, nonrepeatable read, phantom 같은 관찰 가능한 이상 현상으로 설명되어 왔다. Berenson 등의 1995년 논문은 이런 현상과 정의의 모호함을 분석하면서 스냅샷 격리(snapshot isolation)와 쓰기 왜곡(write skew)을 다뤘다.3
스냅샷 격리에서는 트랜잭션이 일정 시점의 버전을 읽어도, 다른 행을 쓰는 두 작업이 함께 업무 규칙을 깨뜨릴 수 있다. 당직자 Alice와 Bob이 모두 근무 중일 때, 두 작업이 각각 “다른 사람이 남아 있다”고 읽고 자신을 당직에서 빼는 경우가 그렇다. 둘은 같은 행을 덮어쓰지 않았지만 최종 당직자는 0명이다.
실행 순서 | 연결 A | 연결 B |
|---|---|---|
시작 | Repeatable Read 시작 | Repeatable Read 시작 |
읽기 | 당직자 수 2 확인 | 당직자 수 2 확인 |
쓰기 | Alice를 당직에서 제외 | Bob을 당직에서 제외 |
확정 | 커밋 | 커밋 |
- 실행 순서
시작
- 연결 A
Repeatable Read 시작
- 연결 B
Repeatable Read 시작
- 실행 순서
읽기
- 연결 A
당직자 수 2 확인
- 연결 B
당직자 수 2 확인
- 실행 순서
쓰기
- 연결 A
Alice를 당직에서 제외
- 연결 B
Bob을 당직에서 제외
- 실행 순서
확정
- 연결 A
커밋
- 연결 B
커밋
이 표는 PostgreSQL의 두 연결이 읽기를 모두 마친 뒤 쓰기를 진행하도록 정한 순서다. A를 완전히 끝낸 뒤 B를 실행했다면 B는 Alice가 빠진 상태를 보므로, 같은 판단을 거쳐 두 사람 모두 빠지는 결과와 달라진다. 직렬가능성(serializability)은 성공한 트랜잭션들의 효과가 어떤 직렬 실행 순서와 같도록 요구한다.
MVCC와 직렬가능성을 구별하기
MVCC는 여러 데이터 버전을 관리하는 기법이다. 어떤 버전을 읽고 어떤 충돌을 거절하는지는 그 위의 규칙이 정한다. PostgreSQL 18의 일반 SELECT를 기준으로 Read Committed는 문장마다 스냅샷을 얻고, Repeatable Read는 첫 비트랜잭션 제어 문장에서 얻은 스냅샷을 유지한다. 자기 트랜잭션이 앞서 수행한 변경은 볼 수 있다.4
PostgreSQL의 Repeatable Read는 팬텀 읽기를 막지만 쓰기 왜곡 같은 직렬화 이상은 허용할 수 있다. Serializable에서는 SSI(Serializable Snapshot Isolation)가 읽기·쓰기 의존성을 검사해 직렬화할 수 없는 실행을 중단할 수 있다. Ports와 Grittner의 2012년 논문은 이 방식을 PostgreSQL에 구현하면서 다룬 문제를 설명한다.5
위 당직자 순서를 양쪽 모두 Serializable로 실행하면, 두 작업이 모두 성공해 당직자 0명으로 끝나는 결과를 받아들일 수 없다. 충돌은 쓰기 또는 커밋 과정에서 40001 직렬화 실패로 드러날 수 있다. 호출자는 실패한 트랜잭션 전체의 읽기와 판단부터 새로 시작할지 결정해야 하며, DB 밖에 이미 수행한 일과 재시도 횟수·종료 조건까지 정책에 포함한다.4
한 행의 버전 검사, 여러 행을 읽는 업무 규칙, 외부 메시지 전달은 보장 범위가 다르다. 작업 단위 패턴은 함께 커밋할 변경을 모으고, 멱등성은 같은 요청이 반복될 때의 효과를 다룬다.
예제 환경: PostgreSQL 18. 격리 수준의 세부 동작은 제품에 따라 달라지므로 다른 DB에 표의 이름만 옮겨 적용하지 않는다.
각주
- Jim Gray, The Transaction Concept: Virtues and Limitations, VLDB, 1981, 도입부와 “A General Model of Transactions”. 저자 자료. ↩
- Theo Härder, Andreas Reuter, Principles of Transaction-Oriented Database Recovery, ACM Computing Surveys 15(4), 1983, pp.287-317, 특히 pp.289-290. 논문 사본, IBM 서지. ↩
- Hal Berenson 외, A Critique of ANSI SQL Isolation Levels, SIGMOD, 1995, §4.2 및 A5B Write Skew. Microsoft Research 원문. ↩
- PostgreSQL 18, §13.2 “Transaction Isolation”. 공식 문서. ↩
- Dan R. K. Ports, Kevin Grittner, Serializable Snapshot Isolation in PostgreSQL, 2012. 논문. ↩