Zettelkasten

Transactional Outbox 패턴은 DB와 외부 시스템 간 일관성을 보장한다

·수정 2026.04.23·수정 3

요약

  • DB 업데이트와 외부 시스템 호출이 함께 필요할 때, outbox 테이블을 활용해 최종 일관성을 보장한다
  • 동기 처리 방식은 단순하지만 불일치 상태 발생 가능성이 있어, 실무에서는 Outbox + 멱등성 조합을 선호한다

본문

문제 상황

DB 업데이트와 외부 시스템 호출이 동시에 필요한 경우:

  • 결제 처리 후 알림 발송
  • 주문 생성 후 재고 시스템 연동
  • 회원가입 후 이메일 발송

해결 방법 비교

1. 동기 처리 + 롤백

BEGIN;
UPDATE accounts SET balance = balance - 100;
-- 외부 API 호출
-- 실패 시 ROLLBACK, 성공 시 COMMIT
COMMIT;

문제점: 외부 API는 성공했는데 COMMIT에서 DB 장애 발생 시 불일치 상태

2. Transactional Outbox 패턴 (권장)

-- 하나의 트랜잭션에서 처리
BEGIN;
UPDATE accounts SET balance = balance - 100;
INSERT INTO outbox (event_type, payload, status)
VALUES ('PAYMENT', '{"amount": 100}', 'PENDING');
COMMIT;

-- 별도 워커가 outbox 처리

워커 구현 방식

Polling 방식

while True:
    events = db.query("""
        SELECT * FROM outbox
        WHERE status = 'PENDING'
        ORDER BY created_at
        LIMIT 100
        FOR UPDATE SKIP LOCKED  -- 다중 워커 경합 방지
    """)

    for event in events:
        try:
            외부_시스템_호출(event.payload)
            db.execute("DELETE FROM outbox WHERE id = ?", event.id)
        except:
            db.execute("""
                UPDATE outbox
                SET retry_count = retry_count + 1,
                    next_retry_at = NOW() + INTERVAL '5 minutes'
                WHERE id = ?
            """, event.id)

    sleep(1)

CDC (Change Data Capture) 방식

[DB] → [Debezium] → [Kafka] → [워커]
         ↑
    binlog/WAL 실시간 감지
  • DB 변경 로그를 실시간 캡처
  • Debezium이 outbox 테이블 변경 감지 → Kafka 발행
  • 워커는 Kafka 컨슈머로 구현

방식 비교

방식 장점 단점
Polling 구현 단순, 인프라 간단 DB 부하, 폴링 주기만큼 지연
CDC 실시간, DB 부하 없음 인프라 복잡도 증가

워커 스케일링 전략

방식 설명
단일 워커 단순하지만 처리량 제한
다중 워커 + SKIP LOCKED 같은 행 중복 처리 방지
파티셔닝 워커별 담당 파티션 분리
Kafka 컨슈머 그룹 파티션별 자동 분배

실무 적용 가이드

  1. 시작: Polling + SKIP LOCKED
  2. 규모 확장 시: CDC + Kafka로 전환
  3. 필수: 외부 시스템 멱등성 보장 (재시도 대비)

참고