이 글에서 얻는 것

  • DB 업데이트와 메시지 발행을 함께 처리할 때 왜 데이터 불일치가 생기는지 구조적으로 이해합니다.
  • 트랜잭션 아웃박스 + CDC를 도입할지, 단순 재시도/보상 트랜잭션으로 충분할지 판단 기준을 얻습니다.
  • 운영 단계에서 반드시 보는 지표(지연, 누락, 중복, 재처리 비용)와 임계치 설정 예시를 가져갑니다.

핵심 개념/이슈

1) 문제의 본질: 이중 쓰기(Double Write)

많은 팀이 아래 두 단계를 같은 요청 안에서 처리합니다.

  1. 주문 상태를 DB에 저장
  2. Kafka/RabbitMQ에 이벤트 발행

문제는 이 둘이 서로 다른 시스템이라는 점입니다. DB 커밋은 성공했는데 메시지 발행이 실패하면, 데이터는 저장됐지만 후속 시스템(정산, 재고, 알림)은 변화 사실을 모릅니다. 반대로 메시지는 발행됐는데 DB 롤백이 되면, 존재하지 않는 주문 이벤트가 퍼집니다.

이 불일치는 트래픽이 낮을 때는 가끔 보이고, 트래픽이 높아지면 장애 티켓의 30~40%를 차지할 정도로 커집니다.

2) 트랜잭션 아웃박스 패턴

핵심 아이디어는 단순합니다.

  • 비즈니스 테이블(order, payment) 업데이트와
  • outbox_events 테이블 insert를
  • 하나의 DB 트랜잭션으로 묶는다

즉, 애플리케이션 요청 경로에서는 외부 브로커에 직접 발행하지 않습니다. DB에 안전하게 기록된 outbox 레코드를 별도 프로세스가 읽어 브로커로 전달합니다.

이 방식의 장점은 명확합니다.

  • 커밋 성공 = 최소한 이벤트 원본은 유실되지 않음
  • 발행 실패 시 재시도로 복구 가능
  • 운영자가 SQL로 상태를 추적 가능

3) CDC(Change Data Capture) 결합 이유

아웃박스만 쓰면 “누가 outbox를 폴링하고 언제 발행하느냐”가 새 과제가 됩니다. 여기서 CDC(Debezium 등)를 붙이면 outbox 테이블의 INSERT를 로그 기반으로 감지해 브로커에 전달할 수 있습니다.

  • 폴링 주기(예: 1초)로 인한 지연/부하를 줄이고
  • DB binlog/redo log를 기반으로 변경을 안정적으로 캡처하며
  • 소비자는 기존 이벤트 스트림처럼 처리 가능

단, CDC를 붙였다고 자동으로 exactly-once가 보장되는 것은 아닙니다. 대부분 현실적 목표는 at-least-once + 소비자 멱등 처리입니다.


실무 적용

1) 테이블/이벤트 모델 최소 기준

outbox_events 최소 컬럼 권장:

  • id (UUID)
  • aggregate_type (Order, Payment)
  • aggregate_id
  • event_type
  • payload (JSON)
  • created_at
  • published_at (nullable)
  • retry_count

운영 기준 예시:

  • 평균 발행 지연(p95) 3초 이하
  • retry_count 5회 초과 비율 0.1% 이하
  • outbox 적체 건수 10,000건 초과 시 경보

2) 소비자 멱등성 설계

중복 발행은 반드시 발생한다고 가정합니다. 소비자 쪽에서 아래 중 하나는 필수입니다.

  • processed_event 테이블에 event_id unique 저장
  • 도메인 키 + 버전(unique)로 중복 업데이트 차단
  • 외부 API 호출 전 idem-key 헤더 강제

실무 우선순위는 보통 다음과 같습니다.

  1. 생산자 안정성(아웃박스)
  2. 소비자 멱등성
  3. 장애 시 재처리(runbook 자동화)

3) 운영 의사결정 기준

다음 조건 중 2개 이상이면 아웃박스 도입을 우선 검토하는 것이 안전합니다.

  • 주문/결제/정산처럼 누락 비용이 큰 도메인
  • 이벤트 누락 시 수동 복구 시간이 30분 이상
  • 월 1회 이상 “DB 반영됨 + 이벤트 누락” 사고 경험
  • 팀이 이미 Kafka/메시지 브로커를 핵심 연동으로 사용

반대로 내부 관리성 이벤트(누락 허용 가능) 수준이면, 초기에는 단순 재시도 + DLQ로 시작하고 트래픽/장애 데이터가 쌓일 때 확장하는 편이 총비용이 낮습니다.


트레이드오프/주의점

  1. 구조 단순성 vs 운영 신뢰성

    • 아웃박스는 테이블/커넥터/모니터링이 늘어 복잡해집니다.
    • 대신 장애 복구 가능성과 추적 가능성이 크게 올라갑니다.
  2. DB 부하 증가

    • outbox write가 추가되어 쓰기 IOPS가 증가합니다.
    • 파티셔닝/보관 정책 없이 방치하면 인덱스 비대화로 역효과가 납니다.
  3. 삭제/보관 정책 필수

    • published_at 기준 7~30일 보관 후 아카이브 또는 삭제 정책을 두세요.
    • 무기한 보관은 감사 요건이 아니라면 거의 항상 비용 손해입니다.
  4. CDC 운영 책임

    • 커넥터 장애, 스키마 변경, 권한 이슈를 누가 소유할지 명확히 정해야 합니다.
    • “플랫폼팀 소유 + 서비스팀 온콜 연계”처럼 책임 경계를 문서화하세요.

체크리스트 또는 연습

  • 우리 시스템에 이중 쓰기 지점이 몇 개인지 목록화했다.
  • outbox_events 스키마와 보관 정책(예: 14일)을 정의했다.
  • 소비자 멱등 키(event_id 또는 도메인 unique)를 구현했다.
  • p95 발행 지연, 적체 건수, 재시도 실패율 대시보드를 만들었다.
  • “이벤트 누락 발생 시 15분 내 복구” 런북을 작성하고 리허설했다.

연습 과제:

  1. 현재 주문 플로우에서 DB 저장 후 메시지 발행 코드를 찾아 실패 시나리오 3개를 적어보세요.
  2. outbox + CDC로 바꿨을 때 장애 전파 경로가 어떻게 줄어드는지 시퀀스 다이어그램으로 그려보세요.
  3. 소비자 멱등성 없이 운영했을 때와 비교해 월간 운영 비용(장애 대응 시간)을 추정해보세요.

관련 글