백엔드 데이터 인프라 17편 — 트랜잭션 BEGIN COMMIT ROLLBACK

2026-05-17백엔드 데이터 인프라

백엔드 데이터 인프라 17편. 트랜잭션의 ACID 보장과 BEGIN·COMMIT·ROLLBACK·SAVEPOINT 표준 패턴을 풀어쓴 학습 노트.

📚 백엔드 데이터 인프라 · 17편 — 트랜잭션 BEGIN COMMIT ROLLBACK

이 글은 백엔드 데이터 인프라 시리즈 70편 중 17편이에요. 13~14편 UPDATE·DELETE 에서 운영 5단계 (BEGIN-SELECT-UPDATE-검증-COMMIT) 를 봤는데, 아직 안 짚은 BEGIN·COMMIT 이 바로 트랜잭션이에요.

트랜잭션 한 줄 설명

여러 SQL을 한 묶음으로 처리하고 모두 성공하거나 모두 실패하게 만드는 게 트랜잭션이에요. 묶음 안 어떤 SQL이라도 실패하면 그 묶음의 변경은 전부 취소돼요.

전형 예 — 계좌 이체:

BEGIN;
    UPDATE accounts SET balance = balance - 10000 WHERE id = 1;   -- A 출금
    UPDATE accounts SET balance = balance + 10000 WHERE id = 2;   -- B 입금
COMMIT;

A에서 빼고 B에 더하는 두 SQL이 둘 다 성공해야 이체가 끝나요. 첫 번째만 성공하고 두 번째가 실패하면 A 돈이 사라지죠. 트랜잭션이 이 위험을 차단해줘요.

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

7편 관계형 모델 에서 짧게 소개한 ACID 를 좀 더 깊이 봐요.

이름 의미
A Atomicity (원자성) 모두 성공 또는 모두 실패
C Consistency (일관성) 제약 조건 항상 만족
I Isolation (격리성) 동시 트랜잭션 서로 안 보임
D Durability (영속성) 커밋 후엔 사라지지 않음

PG는 ACID 를 100% 보장해요. DB가 약속해주는 안전망인 셈이죠.

기본 3가지 명령

BEGIN — 트랜잭션 시작

BEGIN;
-- 또는 START TRANSACTION;

이 시점부터 다음 COMMIT 또는 ROLLBACK 까지가 한 트랜잭션이에요.

COMMIT — 확정

COMMIT;

지금까지의 변경을 영구 적용해요. WAL (Write-Ahead Log, 변경 선기록 로그) 에 기록한 뒤 디스크로 내려가고, 그제야 다른 트랜잭션에 보이기 시작해요.

ROLLBACK — 취소

ROLLBACK;

지금까지의 변경을 모두 취소하고 트랜잭션 시작 전 상태로 되돌려요.

자동 커밋 vs 명시 트랜잭션

PG 기본은 자동 커밋이에요. 즉 각 SQL이 그대로 개별 트랜잭션이 돼요.

UPDATE users SET name = 'Alice' WHERE id = 1;   -- 자동으로 BEGIN·COMMIT

명시적으로 묶고 싶으면 BEGIN 으로 열어요.

BEGIN;
UPDATE users SET name = 'Alice' WHERE id = 1;
UPDATE users SET email = 'new@example.com' WHERE id = 1;
COMMIT;

JPA (Java Persistence API, 자바 ORM 표준) @Transactional (자바 백엔드 입문 43편) 은 메서드에 자동으로 BEGIN·COMMIT 을 박아주는 어노테이션이에요.

SAVEPOINT — 부분 ROLLBACK

BEGIN;
    INSERT INTO orders (user_id, amount) VALUES (1, 10000);
    SAVEPOINT after_order;

    UPDATE inventory SET stock = stock - 1 WHERE product_id = 100;
    -- 재고 부족 → 롤백
    ROLLBACK TO SAVEPOINT after_order;

    UPDATE orders SET status = 'PENDING_STOCK' WHERE id = <new_order_id>;
COMMIT;

큰 트랜잭션 안에 부분 취소점을 박는 기능이에요. 복잡한 비즈니스 로직에서 가끔 써요.

에러 후 트랜잭션

PG 트랜잭션 안에서 SQL 에러가 나면 이후 모든 SQL 이 거부돼요.

BEGIN;
SELECT * FROM nonexistent_table;    -- 에러
SELECT 1;                            -- ERROR: current transaction is aborted
COMMIT;                              -- ROLLBACK 동작

해결책은 에러 후 ROLLBACK 을 명시하거나 SAVEPOINT 로 묶어 부분 취소를 거는 거예요.

격리 수준 — Isolation Levels

여러 트랜잭션이 동시에 돌 때 서로 어떻게 보이는지를 정하는 게 격리 수준이에요. 4단계가 있고, 더 깊은 내용은 38편 MVCC (Multi-Version Concurrency Control, 다중 버전 동시성 제어) 에서 다뤄요.

수준 의미
READ UNCOMMITTED 다른 트랜잭션 미커밋 변경 보임 (PG는 지원 X — READ COMMITTED 처리)
READ COMMITTED (PG 기본) 커밋된 변경만 보임
REPEATABLE READ 트랜잭션 시작 시점 스냅샷 일관
SERIALIZABLE 완벽한 직렬화 (성능 비용)

PG 기본은 READ COMMITTED 이고, 한국 회사 대부분이 이 수준에서 충분히 운영해요.

BEGIN;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-- 또는 BEGIN ISOLATION LEVEL REPEATABLE READ;
...
COMMIT;

락 — FOR UPDATE·FOR SHARE

행 단위 락으로 동시 변경을 차단해요.

BEGIN;
SELECT * FROM accounts WHERE id = 1 FOR UPDATE;   -- 이 행 락 획득
UPDATE accounts SET balance = balance - 10000 WHERE id = 1;
COMMIT;

FOR UPDATE 는 다른 트랜잭션이 이 행을 UPDATE·DELETE 못 하게 막아요. 커밋이나 롤백이 날 때까지 대기시키는 거죠.

SELECT * FROM products WHERE id = 1 FOR SHARE;   -- 공유 락 (UPDATE만 차단)
SELECT * FROM products WHERE id = 1 FOR UPDATE SKIP LOCKED;   -- 락된 행은 건너뜀
SELECT * FROM products WHERE id = 1 FOR UPDATE NOWAIT;   -- 락 대기 X (즉시 에러)

SKIP LOCKED 는 큐 처리에 굉장히 유용해서 Kafka 대안으로도 쓰여요. NOWAIT 는 락 충돌을 즉시 감지하고 싶을 때 골라요.

운영 시나리오 — 5가지

(1) 계좌 이체

BEGIN;
UPDATE accounts SET balance = balance - 10000 WHERE id = 1;
UPDATE accounts SET balance = balance + 10000 WHERE id = 2;
INSERT INTO transfers (from_id, to_id, amount) VALUES (1, 2, 10000);
COMMIT;

(2) 주문 + 재고 + 결제

BEGIN;
INSERT INTO orders (...) RETURNING id;
UPDATE inventory SET stock = stock - 1 WHERE product_id = 100;
INSERT INTO payments (order_id, amount, status) VALUES (...);
COMMIT;

(3) 데이터 이관 (멱등)

BEGIN;
INSERT INTO archive SELECT * FROM logs WHERE created_at < '2025-01-01';
DELETE FROM logs WHERE created_at < '2025-01-01';
COMMIT;

(4) 통계 갱신 (재시도 가능)

BEGIN;
TRUNCATE daily_stats;
INSERT INTO daily_stats SELECT ... FROM orders GROUP BY ...;
COMMIT;

(5) 큐 처리 (FOR UPDATE SKIP LOCKED)

BEGIN;
SELECT * FROM job_queue
WHERE status = 'PENDING'
ORDER BY created_at
LIMIT 10
FOR UPDATE SKIP LOCKED;
-- 잡은 작업들 처리...
UPDATE job_queue SET status = 'PROCESSED' WHERE id IN (...);
COMMIT;

여러 워커가 동시에 큐를 처리해도 중복 처리가 0 이에요.

Spring JPA 의 트랜잭션

자바 백엔드 입문 43편 @Transactional 핵심만 짚어요.

@Service
public class TransferService {

    @Transactional
    public void transfer(Long fromId, Long toId, int amount) {
        accountRepository.decrementBalance(fromId, amount);
        accountRepository.incrementBalance(toId, amount);
        transferRepository.save(new Transfer(fromId, toId, amount));
    }
}

@Transactional 은 메서드 시작 시 BEGIN, 정상 종료 시 COMMIT, 예외 발생 시 ROLLBACK 을 자동으로 처리해줘요. 자바 백엔드에서 사실상 표준 패턴이에요.

격리 수준 명시

@Transactional(isolation = Isolation.REPEATABLE_READ)
public void process() { ... }

트랜잭션 + 외부 API 함정

@Transactional
public void process() {
    repository.save(...);
    externalApi.send(...);    // ❌ 외부 호출 안 트랜잭션
    repository.update(...);
}

외부 API 호출이 트랜잭션 안에 들어가면 DB 락이 외부 API 응답 시간 동안 잡혀버려서 다른 트랜잭션이 줄줄이 대기해요. OutBox 패턴 (DB 에 발송 큐를 박아 비동기로 보내는 방식) 이나 ApplicationEvent (Spring 의 이벤트 발행·구독, 자바 백엔드 입문 38편) 로 분리해요.

함정 5가지

(1) 트랜잭션 안 외부 호출

위에서 본 그 함정이에요. 외부 호출은 트랜잭션 밖으로 빼서 이벤트나 Outbox 로 처리해요.

(2) 너무 큰 트랜잭션

100만 행을 한 트랜잭션으로 UPDATE 하면 WAL 폭발에 락 폭주가 와요. chunk 로 분할해요.

(3) 자동 커밋 모드에서 BEGIN 잊음

UPDATE users SET ...;   -- 자동 커밋, ROLLBACK 불가

운영에선 명시적 BEGIN 이 안전해요.

(4) 에러 후 다음 SQL 실행 시도

에러가 나면 이후 SQL 이 전부 거부되니 ROLLBACK 이나 SAVEPOINT 로 정리해요.

(5) @Transactional 자가 호출

public void caller() {
    save(...);   // ❌ @Transactional 안 먹힘 (프록시 회피)
}

@Transactional
public void save(...) { ... }

자바 백엔드 입문 25편 AOP (Aspect-Oriented Programming, 횡단 관심사 분리) 에서 자세히 다룬 자가 호출 함정이에요.

🎯 트랜잭션 5룰

(1) 명시적 BEGIN·COMMIT. (2) 외부 API는 트랜잭션 밖. (3) 큰 작업은 chunk. (4) 에러 후 ROLLBACK. (5) JPA는 @Transactional 자동. FOR UPDATE SKIP LOCKED는 큐 처리 핵심.

한 줄 정리 — 트랜잭션 = ACID 묶음. BEGIN·COMMIT·ROLLBACK + SAVEPOINT. 격리 수준 4단계 (PG 기본 = READ COMMITTED). FOR UPDATE 행 락, SKIP LOCKED는 큐. JPA는 @Transactional 자동. 외부 API는 트랜잭션 밖.

시험 직전 한 번 더 — 트랜잭션 입문자가 매번 헷갈리는 것

  • 트랜잭션 = 여러 SQL 한 묶음 (모두 성공 또는 모두 실패)
  • BEGIN = 시작 (또는 START TRANSACTION)
  • COMMIT = 확정
  • ROLLBACK = 취소
  • ACID = Atomicity·Consistency·Isolation·Durability
  • 자동 커밋 = PG 기본 (각 SQL이 개별 트랜잭션)
  • 명시 트랜잭션 = BEGIN ... COMMIT
  • SAVEPOINT = 부분 취소점
  • ROLLBACK TO SAVEPOINT name
  • 에러 후 = 이후 SQL 거부 (ROLLBACK 또는 SAVEPOINT)
  • 격리 수준 4 = READ UNCOMMITTED·READ COMMITTED·REPEATABLE READ·SERIALIZABLE
  • PG 기본 = READ COMMITTED
  • FOR UPDATE = 행 락 (UPDATE·DELETE 차단)
  • FOR SHARE = 공유 락
  • SKIP LOCKED = 락된 행 건너뜀 (큐 처리 표준)
  • NOWAIT = 락 대기 X
  • @Transactional = JPA 자동 트랜잭션
  • @Transactional 자가 호출 = 안 먹힘 (프록시)
  • 외부 API는 트랜잭션 밖 = Outbox·이벤트 패턴
  • 큰 작업 = chunk 분할
  • WAL = 트랜잭션 영속성 보장 메커니즘
  • READ COMMITTED = 커밋된 변경만 보임
  • REPEATABLE READ = 시작 시점 스냅샷 일관

시리즈 다른 편 (앞뒤 글 모음)

이전 글:

다음 글:

※ 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

답글 남기기

error: Content is protected !!