본문으로 건너뛰기
3.1 트랜잭션 생애주기

3.1 트랜잭션 생애주기

PostgreSQL의 트랜잭션은 BEGIN 또는 첫 SQL 실행으로 시작 → 여러 SQL 실행 → COMMIT 또는 ROLLBACK으로 종료 합니다. 그 사이에 서버 내부에서는 XID 할당, 락 획득, WAL 쌓기, 스냅샷 유지 같은 일들이 함께 진행됩니다. 한 트랜잭션이 시작해서 끝날 때까지 어떤 단계를 거치는지 봅니다.

상태 다이어그램

    %%{init: {'theme':'base','themeVariables':{
  'primaryColor':'#ede9fe',
  'primaryTextColor':'#3b0764',
  'primaryBorderColor':'#6d28d9',
  'lineColor':'#1d4ed8',
  'secondaryColor':'#d1fae5',
  'tertiaryColor':'#fef3c7',
  'noteBkgColor':'#fef3c7',
  'noteBorderColor':'#b45309',
  'noteTextColor':'#78350f',
  'actorBkg':'#dbeafe',
  'actorBorder':'#1d4ed8',
  'actorTextColor':'#1e3a8a',
  'activationBkgColor':'#d1fae5',
  'activationBorderColor':'#047857',
  'signalColor':'#1d4ed8',
  'signalTextColor':'#1e3a8a',
  'labelBoxBkgColor':'#fef3c7',
  'labelBoxBorderColor':'#b45309',
  'labelTextColor':'#78350f'
}}}%%
stateDiagram-v2
  [*] --> Idle
  Idle --> InTransaction : BEGIN<br/>또는 첫 SQL
  InTransaction --> InTransactionFailed : SQL 오류
  InTransaction --> Committed : COMMIT
  InTransaction --> Aborted : ROLLBACK
  InTransactionFailed --> Aborted : ROLLBACK<br/>(COMMIT은 무시되며 ROLLBACK으로)
  Committed --> Idle
  Aborted --> Idle
  Idle --> [*] : 연결 종료
  

pg_stat_activity.state 컬럼이 이 상태를 노출합니다.

state의미
idle연결돼 있지만 트랜잭션 없음
active쿼리 실행 중
idle in transactionBEGIN 후 다음 SQL 또는 COMMIT 대기
idle in transaction (aborted)트랜잭션 안에서 오류 후 ROLLBACK 대기
fastpath function calllow-level 호출 중
disabled연결만 있고 통계 추적이 꺼진 상태

idle in transaction이 길어지면 운영 사고로 직결됩니다. 자세한 내용은 Part XIV 14.4 트랜잭션 함정 참고합니다.

자동 트랜잭션 vs 명시 트랜잭션

PostgreSQL은 모든 SQL이 트랜잭션 안에서 동작합니다. 명시적으로 BEGIN이 없으면 단일 SQL이 그 자체로 한 트랜잭션이다(autocommit 모드).

-- 명시: 여러 SQL을 한 트랜잭션으로
BEGIN;
UPDATE orders SET status = 'shipped' WHERE id = 42;
INSERT INTO shipments(order_id, tracking_no) VALUES (42, 'T-9001');
COMMIT;

-- 묵시: 한 줄 = 한 트랜잭션 = COMMIT 자동
UPDATE orders SET status = 'shipped' WHERE id = 42;

psql의 기본은 autocommit 켜짐. \set AUTOCOMMIT off로 끄면 매번 COMMIT이 필요해집니다.

XID 할당 시점

PostgreSQL은 데이터를 실제로 바꾸는 SQL이 처음 들어올 때만 트랜잭션 ID(XID)를 발급합니다.

BEGIN;
SELECT * FROM orders;        -- 읽기만: XID 미할당
SELECT count(*) FROM users;  -- 읽기만: XID 미할당
UPDATE orders SET ... ;      -- 여기서 XID 발급
COMMIT;

읽기 전용 트랜잭션이 XID를 안 받는 이유는 XID 공간(32-bit)을 아껴 wraparound을 늦추기 위해서다. read-only 트랜잭션이 수만 건 동시에 떠 있어도 XID는 1만큼만 늘어납니다.

txid_current()(PG 13+ pg_current_xact_id())로 현재 XID 확인:

BEGIN;
SELECT pg_current_xact_id_if_assigned();  -- NULL (아직 안 받음)
UPDATE t SET v = 1 WHERE id = 1;
SELECT pg_current_xact_id_if_assigned();  -- 예: 4096
COMMIT;

단계 별 동작

    %%{init: {'theme':'base','themeVariables':{
  'primaryColor':'#ede9fe',
  'primaryTextColor':'#3b0764',
  'primaryBorderColor':'#6d28d9',
  'lineColor':'#1d4ed8',
  'secondaryColor':'#d1fae5',
  'tertiaryColor':'#fef3c7',
  'noteBkgColor':'#fef3c7',
  'noteBorderColor':'#b45309',
  'noteTextColor':'#78350f',
  'actorBkg':'#dbeafe',
  'actorBorder':'#1d4ed8',
  'actorTextColor':'#1e3a8a',
  'activationBkgColor':'#d1fae5',
  'activationBorderColor':'#047857',
  'signalColor':'#1d4ed8',
  'signalTextColor':'#1e3a8a',
  'labelBoxBkgColor':'#fef3c7',
  'labelBoxBorderColor':'#b45309',
  'labelTextColor':'#78350f'
}}}%%
sequenceDiagram
  participant Client
  participant Backend
  participant ProcArray
  participant WAL
  participant Storage

  Client->>Backend: BEGIN
  Backend->>Backend: 트랜잭션 컨텍스트 생성
  Note over Backend: state = InTransaction

  Client->>Backend: UPDATE orders ...
  Backend->>ProcArray: XID 할당 요청
  ProcArray-->>Backend: XID = 4096
  Backend->>Storage: tuple 갱신 (새 버전 작성)
  Backend->>WAL: WAL record 추가 (메모리)
  Backend-->>Client: UPDATE 1

  Client->>Backend: COMMIT
  Backend->>WAL: COMMIT record 추가 + flush
  WAL-->>Backend: fsync 완료
  Backend->>ProcArray: XID 제거
  Backend->>Backend: 락 해제
  Backend-->>Client: COMMIT
  

핵심 포인트:

  1. 데이터 페이지는 즉시 디스크로 가지 않는다 — shared_buffers의 메모리만 갱신
  2. WAL fsync가 commit의 진짜 비용 — COMMIT의 지연은 대부분 WAL fsync 대기 시간
  3. XID는 ProcArray에 등록되어 다른 트랜잭션의 스냅샷 계산에 쓰인다
  4. 락은 commit/rollback 시 해제된다 (예외: savepoint)

SAVEPOINT — 트랜잭션 안의 분기점

긴 트랜잭션 안에서 부분적으로 롤백하고 싶을 때.

BEGIN;
INSERT INTO orders(...) VALUES (...);
SAVEPOINT s1;
  INSERT INTO order_items(...) VALUES (...);
  -- 오류 발생
ROLLBACK TO SAVEPOINT s1;   -- order_items만 취소, orders는 유지
INSERT INTO order_items(...) VALUES (...);  -- 재시도
COMMIT;

내부적으로 subtransaction(서브트랜잭션)이 생기고, 각각 자기 XID를 받습니다. SAVEPOINT 남발은 성능 함정 — subtransaction 수가 64개를 넘으면 multixact·snapshot 처리 비용이 폭증합니다.

SAVEPOINT 폭주 안티패턴: 일부 ORM이 모든 작업을 자동으로 SAVEPOINT로 감싸는 경우가 있습니다. 한 트랜잭션에 SAVEPOINT가 수백 개 들어가면 SubtransSLRU 락 경합으로 클러스터 전체가 느려집니다. 자세한 내용은 Part XIV 14.4 참고합니다.

PREPARE TRANSACTION — 2PC

분산 트랜잭션을 위한 2-phase commit.

BEGIN;
INSERT INTO orders(...) VALUES (...);
PREPARE TRANSACTION 'order-42';   -- COMMIT을 보류
-- 이 시점에 다른 노드의 상태를 확인
COMMIT PREPARED 'order-42';        -- 또는
-- ROLLBACK PREPARED 'order-42';

max_prepared_transactions = 0이 기본이라 운영자가 명시적으로 켜야 동작합니다. 고립된 사용은 거의 없고, XA 분산 트랜잭션 매니저 또는 일부 메시지 큐에서만 씁니다.

준비된 트랜잭션은 세션이 끊겨도 살아남아 락을 잡고 있다. 잊혀진 prepared transaction이 autovacuum을 막아 사고 사례가 많습니다.

-- 살아 있는 prepared transaction 확인
SELECT gid, prepared, owner FROM pg_prepared_xacts;

트랜잭션과 자원

한 트랜잭션이 잡고 있는 자원:

자원영향
XIDwraparound 카운터 진행. 매우 긴 트랜잭션이 vacuum freeze를 막는다
heavy-weight·predicate lock 모두 commit까지 유지
스냅샷다른 트랜잭션의 dead tuple을 vacuum 못 하게 막음 (xmin horizon)
임시 파일정렬·해시 spill 결과는 트랜잭션 종료 시 정리
WALcommit 시 flush 필요. 매우 큰 트랜잭션은 commit이 길어짐

이래서 긴 트랜잭션은 만악의 근원이라는 관용구가 생겼습니다.

진단 SQL

-- 가장 오래된 트랜잭션 찾기
SELECT pid, datname, usename, state, xact_start, now()-xact_start AS age, query
  FROM pg_stat_activity
 WHERE state IN ('idle in transaction','idle in transaction (aborted)','active')
   AND xact_start IS NOT NULL
 ORDER BY xact_start
 LIMIT 10;

-- prepared transaction 잔재
SELECT gid, prepared, owner FROM pg_prepared_xacts;

idle in transaction 상태가 분 단위로 떠 있으면 애플리케이션 버그(BEGIN 후 처리 시간을 너무 끌거나 COMMIT을 안 함)일 가능성이 높습니다.

idle_in_transaction_session_timeout 파라미터를 설정해 일정 시간 이상 idle한 트랜잭션을 자동으로 끊는 것이 운영 표준입니다.

ALTER SYSTEM SET idle_in_transaction_session_timeout = '5min';
SELECT pg_reload_conf();

정리

  • 모든 SQL은 트랜잭션 안에서 동작합니다. BEGIN이 없으면 한 SQL이 하나의 트랜잭션
  • XID는 첫 쓰기 SQL 때만 발급 — 읽기 전용은 XID를 안 받음
  • COMMIT의 진짜 비용은 WAL fsync. 데이터 페이지는 메모리만 갱신되고 나중에 dirty flush
  • SAVEPOINT는 부분 롤백에 유용하지만 남발은 SubtransSLRU 경합 유발
  • PREPARE TRANSACTION = 2PC, 거의 안 쓰지만 잊혀진 prepared가 사고를 자주 일으킴
  • idle in transaction은 vacuum·락에 영향 — idle_in_transaction_session_timeout으로 안전망

다음 절(3.2)에서는 PostgreSQL의 동시성 핵심인 MVCC와 스냅샷을 봅니다.