Post

Outbox + CDC 원자성

DB 트랜잭션과 메시지 발행 사이의 원자성 붕괴를 Outbox와 Debezium CDC로 봉인하고 멱등을 전제로 운영하는 방법

Outbox + CDC 원자성

이 글의 결론: Outbox는 이벤트 발행 기법이 아니라 DB 트랜잭션과 메시지 발행 사이의 원자성 붕괴를 구조적으로 제거하는 표준 패턴이다.

분산 환경에서 “상태 변경(DB)과 이벤트 발행(Kafka)”을 같은 사실로 취급하지 않으면, 운영 단계에서 복구 불가능한 불일치가 발생한다. Outbox는 이 불일치를 설계 수준에서 봉인한다.


문제 정의 결론: “DB 커밋 성공 + 이벤트 발행 실패”는 침묵하는 데이터 손실이다.

왜 치명적인가

  • 주문은 CREATED로 저장됐지만 이벤트가 나가지 않음
  • 결제/레스토랑 서비스는 그 주문의 존재를 영원히 모름
  • 재시도도, 재처리도 불가능(이벤트 자체가 없음)

이 문제는 로그에 남지 않고, 알람도 울리지 않으며, 나중에 정합성으로 복구할 수도 없다. 따라서 “두 작업을 한 트랜잭션으로 묶지 않으면 안 되는 문제”다.


Outbox 기본 구조 결론: 핵심은 “같은 트랜잭션에 기록하고, 나중에 확실히 보낸다”이다.

구성 요소

  • outbox 테이블

    • 이벤트 페이로드, 키, 타입, 상태(NEW/SENT/FAILED), 생성시각
  • 상태 기록

    • 발행 성공/실패를 DB에 남김
  • 퍼블리시

    • Kafka 등 메시지 브로커로 전달
  • 마킹(완료)

    • 성공 시 SENT로 변경

흐름

1
2
3
4
5
6
7
8
9
Business Tx
  ├─ 도메인 상태 변경
  └─ outbox row INSERT
(COMMIT)

Publisher
  ├─ outbox 조회
  ├─ 메시지 발행
  └─ 상태 업데이트(SENT/FAILED)

핵심: 상태 변경과 이벤트 의도 기록은 항상 함께 커밋된다.


폴링 vs CDC(Debezium) 결론: 선택 기준은 성능이 아니라 운영 실패 시 복구 난이도다.

폴링 스케줄러 방식

  • 애플리케이션이 주기적으로 outbox 조회
  • 장점:

    • 단순
    • 이해/디버깅 쉬움
  • 단점:

    • 폴링 지연
    • 대량 트래픽 시 DB 부하
    • 스케줄러 장애 시 누락 감지 필요

CDC(Debezium) 방식

  • DB 로그(binlog/WAL)를 읽어 outbox 변경을 스트리밍
  • 장점:

    • 낮은 지연
    • DB 폴링 부하 없음
    • 재처리/재연결 내구성
  • 단점:

    • 운영 복잡도 상승
    • 커넥터/권한/버전 관리 필요

선택 기준 요약:

  • 소규모/초기 → 폴링
  • 중대형/고신뢰 → CDC

중복과 멱등 결론: Outbox는 “유실”을 막을 뿐, 중복은 기본 전제다.

왜 중복이 생기는가

  • 퍼블리시 성공 후 상태 마킹 실패
  • CDC 재연결/리플레이
  • 컨슈머 at-least-once

멱등 키 설계 포인트

  • 자연키: aggregateId + eventType + version
  • 이벤트 ID: UUID + 유니크 인덱스
  • 컨슈머 측 검증: 이미 처리한 이벤트면 무시

JMeter/락 전략 비교의 의미 결론: 핵심은 성능이 아니라 경합을 어디서 감당할 것인가다.

  • DB 락:

    • 장점: 강한 일관성
    • 단점: TPS 증가 시 병목
  • 애플리케이션 레벨 멱등:

    • 장점: 확장성
    • 단점: 구현 복잡
  • Outbox + 멱등:

    • 경합을 DB(짧게) + 컨슈머(가볍게)로 분산

부하 테스트의 목적은 “빠른가”가 아니라 경합이 폭주할 때 어디가 먼저 무너지는지를 확인하는 것이다.


예시 1 결론: 주문 생성은 “상태 변경 + 이벤트 의도”를 한 번에 묶는다.

흐름

1
2
3
4
5
6
7
8
CreateOrder Tx
  ├─ Order INSERT
  └─ Outbox INSERT (OrderCreated)
COMMIT

Publisher
  ├─ Publish OrderCreated
  └─ Mark SENT

포인트:

  • 이벤트는 DB 사실의 파생물
  • 발행 실패 시 재시도 가능

예시 2 결론: 결제 중복 처리는 “Outbox 이전에 도메인에서 차단”해야 한다.

시나리오

  • 동일 paymentAttemptId로 두 번 요청
  • 도메인:

    • 이미 처리된 시도면 예외
  • 결과:

    • DB 변경 ❌
    • Outbox 기록 ❌
    • 이벤트 ❌

핵심: Outbox는 사후 보장, 중복 방지는 사전 도메인 규칙이다.


선택지 비교 표 결론: “정합성·지연·복잡도의 삼각형”

선택지정합성지연복잡도권장 상황
직접 발행낮음낮음낮음이벤트 유실 허용
Outbox 폴링높음중간중간중소 규모, 빠른 도입
CDC(Debezium)매우 높음낮음높음대규모, 재처리 필수

흔한 오해 / 실수 3가지

  1. Outbox를 “이벤트 발행 트릭”으로 보는 오해
    • 문제: Kafka 발행 신뢰성을 높이기 위한 보조 수단 정도로 인식
    • 결과: 상태 변경과 이벤트 기록을 분리 구현
    • 교정 기준: Outbox는 메시지 신뢰성이 아니라 트랜잭션 원자성 문제 해결책이다.
  2. Outbox가 있으면 중복이 사라진다고 착각
    • 문제: 중복 이벤트를 버그로 간주
    • 결과: 컨슈머에서 중복 처리로 장애 발생
    • 교정 기준: Outbox는 유실을 막고, 중복은 기본 전제다.
  3. CDC는 성능 최적화 수단이라는 오해
    • 문제: 폴링보다 빠르다는 이유만으로 Debezium 선택
    • 결과: 운영 복잡도 급증, 장애 대응 불가
    • 교정 기준: CDC 선택 기준은 지연이 아니라 복구·재처리 가능성이다.

대표 실패 시나리오 3가지

  1. Outbox는 기록됐는데 퍼블리셔가 멈춘 경우
    • 상황: DB에는 NEW 이벤트가 누적
    • 원인: 퍼블리셔 장애/권한 문제
    • 결과: 다운스트림 서비스는 상태를 영원히 모름
    • 대응: outbox 적체량 알람 + 수동 재발행 경로 확보
  2. 퍼블리시 성공 후 상태 마킹 실패
    • 상황: Kafka에는 이벤트 존재, outbox는 NEW 유지
    • 원인: 퍼블리셔 재시작/DB 타임아웃
    • 결과: 재시도 시 동일 이벤트 중복 발행
    • 대응: 이벤트 ID 기반 멱등 처리 전제 + 중복 허용 설계
  3. CDC 재연결 후 과거 이벤트 재전송
    • 상황: Debezium 커넥터 재기동
    • 원인: 오프셋 재설정
    • 결과: 컨슈머에서 과거 이벤트 재적용
    • 대응: 컨슈머 멱등성 + 상태 머신 검증

실무 체크리스트 (Outbox/CDC 운영 점검)

  1. 상태 변경과 outbox INSERT가 같은 트랜잭션인가
  2. outbox에 이벤트 고유 ID가 있는가
  3. outbox 상태 전이(NEW/SENT/FAILED)가 명확한가
  4. 퍼블리셔 재시도 정책이 정의돼 있는가
  5. 퍼블리셔 장애 시 적체량을 감지할 수 있는가
  6. 중복 발행을 허용하고 있는가(멱등 전제)
  7. 컨슈머가 이벤트 중복을 안전하게 무시하는가
  8. 폴링 주기/배치 크기 기준이 문서화돼 있는가
  9. CDC 재연결 시 재전송을 감당할 수 있는가
  10. 실패 이벤트를 별도로 조회/조치할 수 있는가
  11. 수동 재발행 절차가 준비돼 있는가
  12. outbox 테이블 증가에 대한 정리 전략이 있는가
  13. 스키마 변경 시 outbox/CDC 영향 분석이 가능한가
  14. 장애 시 “유실/중복/지연” 중 허용 범위가 합의됐는가
  15. 이벤트 한 건을 DB→Kafka→컨슈머까지 추적할 수 있는가

This post is licensed under CC BY 4.0 by the author.