숏컷 코드
BEGIN;
UPDATE accounts
SET balance = balance - 100
WHERE id = 1;
SAVEPOINT after_withdraw;
UPDATE accounts
SET balance = balance + 100
WHERE id = 2;
COMMIT;문법
트랜잭션은 "부분 성공을 허용하지 않겠다"는 원자성 선언이다
BEGIN ~ COMMIT 사이의 모든 SQL 문은 하나의 작업 단위로 처리됩니다. 중간에 오류가 발생하면 ROLLBACK 으로 모든 변경을 취소해 시작 전 상태로 되돌릴 수 있습니다. 송금처럼 "출금과 입금이 반드시 함께 성공해야 하는" 작업에서 트랜잭션 없이 처리하면 출금만 성공하고 입금이 실패하는 불일치 상태가 생깁니다. PostgreSQL 에서 단일 SQL 문도 암묵적 트랜잭션으로 처리되지만, 여러 문을 묶으려면 명시적 BEGIN 이 필요합니다.
BEGIN;
-- 재고 차감
UPDATE products SET stock = stock - 1 WHERE id = 100;
-- 주문 생성
INSERT INTO orders (product_id, quantity, user_id)
VALUES (100, 1, 42);
-- 모두 성공하면 확정
COMMIT;
-- 또는 문제가 생기면 전체 취소
-- ROLLBACK;BEGIN, COMMIT, ROLLBACK, SAVEPOINT는 되돌릴 범위가 다르다
BEGIN은 작업 단위를 열고, COMMIT은 전체를 확정하고, ROLLBACK은 전체를 취소하고, SAVEPOINT는 중간 지점만 되돌릴 수 있게 합니다. 트랜잭션을 쓸 때는 "전체를 취소할지, 일부만 취소할지"를 먼저 정하면 구조가 단순해집니다.
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
SAVEPOINT after_withdraw;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
ROLLBACK TO after_withdraw;
COMMIT;단일 문장 자동 커밋과 명시적 트랜잭션은 쓰는 상황이 다르다
PostgreSQL은 별도 BEGIN이 없으면 각 SQL 문을 하나씩 자동 커밋합니다. 한 문장으로 끝나는 작업은 이 기본 동작으로 충분하지만, 여러 문장이 함께 성공하거나 함께 실패해야 하면 명시적 트랜잭션이 필요합니다. "UPDATE 하나, INSERT 하나"처럼 보이는 흐름도 비즈니스적으로 한 덩어리면 같은 트랜잭션에 묶어야 합니다.
-- 자동 커밋: 한 문장만 독립적으로 실행
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- 명시적 트랜잭션: 두 문장이 함께 성공해야 함
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;SAVEPOINT 는 트랜잭션 안에서 부분 롤백 지점을 만든다
SAVEPOINT name 을 선언하면 그 시점까지의 변경을 보존하면서, 이후 실패한 작업만 ROLLBACK TO name 으로 되돌릴 수 있습니다. 트랜잭션 전체를 취소하지 않고 일부 작업만 재시도하거나 건너뛸 때 유용합니다. 세이브포인트는 중첩도 가능하며, RELEASE SAVEPOINT name 으로 명시적으로 해제할 수 있습니다.
BEGIN;
INSERT INTO audit_log (action) VALUES ('start');
SAVEPOINT before_risky_op;
-- 실패할 수 있는 작업
UPDATE external_accounts SET status = 'synced' WHERE id = 99;
-- 위 작업만 취소하고 트랜잭션은 유지
ROLLBACK TO before_risky_op;
INSERT INTO audit_log (action) VALUES ('risky_op_skipped');
COMMIT;오래 열린 트랜잭션은 잠금 경합과 MVCC 부풀음을 일으킨다
PostgreSQL 은 MVCC(Multi-Version Concurrency Control) 모델을 사용합니다. 트랜잭션이 열려 있는 동안 그 트랜잭션이 시작될 때의 스냅샷보다 오래된 행 버전을 VACUUM 이 정리할 수 없습니다. 트랜잭션이 오래 유지되면 죽은 튜플이 쌓여 테이블이 부풀고, VACUUM 효율이 떨어집니다. 또한 UPDATE / DELETE 는 행 수준 잠금을 획득하므로 트랜잭션이 길수록 다른 세션의 대기 시간이 늘어납니다. pg_stat_activity 로 오래된 트랜잭션을 주기적으로 모니터링해야 합니다.
-- 오래된 트랜잭션 확인
SELECT pid, now() - xact_start AS duration, state, query
FROM pg_stat_activity
WHERE xact_start IS NOT NULL
AND state != 'idle'
ORDER BY duration DESC;
-- 잠금 대기 확인
SELECT pid, wait_event_type, wait_event, query
FROM pg_stat_activity
WHERE wait_event_type = 'Lock';트랜잭션 격리 수준은 동시성과 일관성의 트레이드오프다
PostgreSQL 의 기본 격리 수준은 READ COMMITTED 입니다. 이 수준에서는 같은 트랜잭션 안에서 SELECT 를 두 번 실행했을 때 그 사이에 다른 트랜잭션이 커밋했다면 결과가 달라질 수 있습니다. 반복 읽기 일관성이 필요하면 REPEATABLE READ 또는 SERIALIZABLE 을 사용해야 합니다. 격리 수준이 높을수록 직렬화 충돌 오류(serialization failure)가 늘어나 재시도 로직이 필요합니다.
-- 격리 수준 명시
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SELECT balance FROM accounts WHERE id = 1;
-- 이 시점의 잔액이 트랜잭션 내에서 일관되게 유지됨
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT;선택 기준
| 상황 | 적합한 선택 |
|---|---|
| 여러 문을 하나로 묶어야 할 때 | BEGIN ... COMMIT |
| 전체 작업을 취소해야 할 때 | ROLLBACK |
| 일부 작업만 취소하고 싶을 때 | SAVEPOINT + ROLLBACK TO |
| 반복 읽기 일관성이 필요할 때 | REPEATABLE READ 격리 수준 |
| 오래된 트랜잭션 모니터링 | pg_stat_activity 쿼리 |
주의할 점
트랜잭션을 오래 열어 두면 MVCC 스냅샷이 유지되어 VACUUM 이 죽은 튜플을 정리하지 못합니다.
이는 테이블 부풀음(table bloat)과 쿼리 성능 저하로 이어집니다.
행 수준 잠금도 트랜잭션 동안 유지되므로 다른 세션의 대기가 길어질 수 있습니다.
트랜잭션은 필요한 범위만 짧게 묶고, pg_stat_activity 로 장기 트랜잭션을 주기적으로 확인해야 합니다.
BEGIN;
SELECT * FROM posts WHERE id = 1;
-- 여기서 오래 대기
UPDATE posts SET title = 'new' WHERE id = 1;
COMMIT;트랜잭션 안에서 사용자 입력이나 외부 API 응답을 오래 기다리면, 실제 SQL 수는 적어도 잠금과 스냅샷 유지 비용이 길게 이어질 수 있습니다.
BEGIN;
SAVEPOINT before_item;
INSERT INTO order_items (order_id, product_id, quantity)
VALUES (1, 999, 1); -- 오류 발생
INSERT INTO audit_log (message) VALUES ('keep going');
COMMIT;한 문장에서 오류가 나면 현재 트랜잭션은 abort 상태가 되어, ROLLBACK 또는 ROLLBACK TO SAVEPOINT 전까지 뒤의 문장은 계속 실패합니다. 예외를 잡았다고 곧바로 다음 SQL을 실행해도 복구되지 않는다는 점을 기억해야 합니다.
참고 링크
1 sources