트랜잭션 — BEGIN·COMMIT·ROLLBACK
"전부 되거나, 전혀 안 되거나" — DB 의 가장 중요한 약속.
중급 파트의 시작. 트랜잭션은 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 — Durability | COMMIT 후 손실 없음 | 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 read | non-repeatable read | phantom 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.
이전: 13편 뷰 · 현재: 14편 (중급 시작) · 다음 → 15편 락 · 진행: 14/24