PostgreSQL 교재 · 14편 / 24편

트랜잭션 — BEGIN·COMMIT·ROLLBACK

"전부 되거나, 전혀 안 되거나" — DB 의 가장 중요한 약속.

중급읽는 시간 8분2026-05-17
BEGIN-COMMIT 사이의 트랜잭션 흐름이 다이어그램으로 표현된 일러스트

중급 파트의 시작. 트랜잭션은 DB 가 "여러 SQL 을 한 묶음으로" 다루는 약속입니다. 계좌 이체가 절반만 되면 큰일이죠 — 그걸 막아주는 게 트랜잭션. 14편은 ACID 의 의미, BEGIN/COMMIT/ROLLBACK, savepoint, 그리고 4가지 격리 수준의 차이까지.

왜 트랜잭션이 필요한가

-- A → B 로 10000원 이체
UPDATE accounts SET balance = balance - 10000 WHERE id = 'A';
-- (여기서 시스템 죽으면? A 만 차감되고 B 는 안 받음 — 1만원이 사라짐)
UPDATE accounts SET balance = balance + 10000 WHERE id = 'B';

-- 트랜잭션으로 묶기 — 둘 다 되거나 둘 다 안 되거나
BEGIN;
  UPDATE accounts SET balance = balance - 10000 WHERE id = 'A';
  UPDATE accounts SET balance = balance + 10000 WHERE id = 'B';
COMMIT;
-- 중간에 죽어도 ROLLBACK 처리됨

ACID — 트랜잭션의 4가지 약속

약자의미예시
A — Atomicity전체가 되거나 전혀 안 됨위 이체 예시
C — Consistency제약을 항상 만족CHECK·FK 위반은 ROLLBACK
I — Isolation동시 트랜잭션끼리 격리4가지 수준 (아래)
D — DurabilityCOMMIT 후 손실 없음WAL 디스크 기록

BEGIN · COMMIT · ROLLBACK 기본

BEGIN;                              -- 또는 BEGIN TRANSACTION;
  INSERT INTO orders (...) VALUES (...);
  INSERT INTO order_items (...) VALUES (...);
  UPDATE inventory SET stock = stock - 1 WHERE id = 5;
COMMIT;                             -- 확정

-- 또는
BEGIN;
  DELETE FROM users WHERE active = false;
  -- 영향 행 수 확인
  -- "예상보다 많네"
ROLLBACK;                           -- 취소 (아무 일 없었던 것처럼)

"자동 트랜잭션" 함정. 한 줄 SQL 도 사실 자동 트랜잭션입니다 — psql 에서 UPDATE 친 즉시 COMMIT 됨. 여러 줄을 묶고 싶으면 명시적으로 BEGIN ... COMMIT. 안에서 에러 나면 트랜잭션이 aborted 상태가 되어, 다음 명령이 다 거부됨 → ROLLBACK 으로 풀어야 함.

SAVEPOINT — 부분 ROLLBACK

BEGIN;
  INSERT INTO orders (...) VALUES (...);

  SAVEPOINT before_items;
    INSERT INTO order_items (...) VALUES (...);
    -- 에러 발생 → 여기까지만 취소하고 싶을 때
  ROLLBACK TO SAVEPOINT before_items;

  -- orders INSERT 는 그대로 살아있음
  INSERT INTO order_items (...) VALUES (...);   -- 다시 시도
COMMIT;

중간 체크포인트. 여러 단계 트랜잭션에서 일부만 되돌리고 싶을 때 유용 — ORM 의 nested transaction 도 대부분 SAVEPOINT 로 구현.

격리 수준 4종 — 어떤 동시성 이상을 막을까

수준dirty readnon-repeatable readphantom read
Read Uncommitted가능가능가능
Read Committed ★기본차단가능가능
Repeatable Read차단차단PG 는 차단
Serializable차단차단차단 (직렬화)

각 이상의 의미.

· dirty read — 다른 TX 의 COMMIT 안 된 변경을 봄 (PG 는 자동 차단).

· non-repeatable read — 같은 행을 한 TX 안에서 두 번 읽었는데 다름.

· phantom read — 같은 WHERE 로 두 번 SELECT 했는데 행 개수가 다름.

-- 격리 수준 설정 (트랜잭션 단위)
BEGIN ISOLATION LEVEL REPEATABLE READ;
  SELECT balance FROM accounts WHERE id = 'A';
  -- 다른 트랜잭션이 balance 바꿔도 이 TX 에서는 처음 값 그대로
  SELECT balance FROM accounts WHERE id = 'A';   -- 같은 값
COMMIT;

-- 세션 기본값
SET default_transaction_isolation = 'repeatable read';

Read Committed vs Repeatable Read — 가장 자주 선택

PG 의 기본은 Read Committed. 한 TX 안에서도 매 SELECT 가 그 시점의 최신 commit 을 봄. 가벼운 OLTP 에 적합.

Repeatable Read 는 TX 시작 시점의 스냅샷을 끝까지 사용. 보고서 생성·일관된 분석에 유용. 단 다른 TX 가 같은 행을 바꾸면 "serialization failure" 가 나서 재시도해야 합니다.

-- Repeatable Read 에서 흔한 패턴
BEGIN ISOLATION LEVEL REPEATABLE READ;
  -- ... 여러 SELECT 와 일관성 검증 ...
COMMIT;
-- 만약 ERROR: could not serialize access
-- → 트랜잭션 전체를 다시 시도

Serializable — 진짜 직렬화 환상

BEGIN ISOLATION LEVEL SERIALIZABLE;
  -- 동시에 도는 다른 SERIALIZABLE TX 와 충돌이 감지되면 abort
  -- 충돌이 없으면 마치 혼자만 도는 것처럼 동작 보장
COMMIT;

가장 강한 격리. PG 의 SSI(Serializable Snapshot Isolation) 가 구현 — 성능은 Repeatable Read 와 비슷하나 abort 가 더 자주. 강한 일관성이 필수인 결제·재고 같은 영역에 사용.

실전 패턴 1 — 락 충돌 회피 (FOR UPDATE)

-- 재고 확인 + 차감을 안전하게
BEGIN;
  SELECT stock FROM inventory WHERE id = 5 FOR UPDATE;   -- 행 락
  -- (다른 TX 는 이 행을 못 만짐)
  UPDATE inventory SET stock = stock - 1 WHERE id = 5;
COMMIT;

15편(락) 에서 깊이.

실전 패턴 2 — 재시도 (서비스 코드)

// pseudo-code
async function transferMoney(from, to, amount) {
  for (let attempt = 0; attempt < 3; attempt++) {
    try {
      await db.tx(async (t) => {
        await t.query('SET TRANSACTION ISOLATION LEVEL SERIALIZABLE');
        await t.query('UPDATE accounts SET balance = balance - $1 WHERE id = $2', [amount, from]);
        await t.query('UPDATE accounts SET balance = balance + $1 WHERE id = $2', [amount, to]);
      });
      return;
    } catch (err) {
      if (err.code === '40001') continue; // serialization failure → 재시도
      throw err;
    }
  }
  throw new Error('서버 혼잡 — 다시 시도하세요');
}

안티패턴 5가지

1. BEGIN 만 하고 COMMIT 잊음. 트랜잭션이 무한히 열려 락이 쌓임 → 서비스 정지.

2. 트랜잭션 안에서 외부 API 호출. HTTP 응답 기다리는 동안 DB 락 보유 → 느려짐.

3. 너무 큰 트랜잭션. 1만 행 한 번에 UPDATE — WAL 폭증·락 시간 폭증. 배치를 잘라서.

4. autocommit 가정. 라이브러리/풀에 따라 다름. 명시적으로 BEGIN/COMMIT 또는 ORM 의 transaction() 사용.

5. ROLLBACK 안 함. aborted 상태에서 다음 명령이 다 거부됨 — catch 절에서 명시적으로 ROLLBACK.

15편 — 락과 동시성 (FOR UPDATE)

행/테이블 락, deadlock 진단, pg_locks/pg_stat_activity.

📚 PostgreSQL 배우기 교재
이전: 13편 뷰 · 현재: 14편 (중급 시작) · 다음 → 15편 락 · 진행: 14/24

© 2026 주나이테크(주) @JUNAITECH