이 글에서 얻는 것
- DB 업데이트와 메시지 발행을 함께 처리할 때 왜 데이터 불일치가 생기는지 구조적으로 이해합니다.
- 트랜잭션 아웃박스 + CDC를 도입할지, 단순 재시도/보상 트랜잭션으로 충분할지 판단 기준을 얻습니다.
- 운영 단계에서 반드시 보는 지표(지연, 누락, 중복, 재처리 비용)와 임계치 설정 예시를 가져갑니다.
핵심 개념/이슈
1) 문제의 본질: 이중 쓰기(Double Write)
많은 팀이 아래 두 단계를 같은 요청 안에서 처리합니다.
- 주문 상태를 DB에 저장
- 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 헤더 강제
실무 우선순위는 보통 다음과 같습니다.
- 생산자 안정성(아웃박스)
- 소비자 멱등성
- 장애 시 재처리(runbook 자동화)
3) 운영 의사결정 기준
다음 조건 중 2개 이상이면 아웃박스 도입을 우선 검토하는 것이 안전합니다.
- 주문/결제/정산처럼 누락 비용이 큰 도메인
- 이벤트 누락 시 수동 복구 시간이 30분 이상
- 월 1회 이상 “DB 반영됨 + 이벤트 누락” 사고 경험
- 팀이 이미 Kafka/메시지 브로커를 핵심 연동으로 사용
반대로 내부 관리성 이벤트(누락 허용 가능) 수준이면, 초기에는 단순 재시도 + DLQ로 시작하고 트래픽/장애 데이터가 쌓일 때 확장하는 편이 총비용이 낮습니다.
트레이드오프/주의점
구조 단순성 vs 운영 신뢰성
- 아웃박스는 테이블/커넥터/모니터링이 늘어 복잡해집니다.
- 대신 장애 복구 가능성과 추적 가능성이 크게 올라갑니다.
DB 부하 증가
- outbox write가 추가되어 쓰기 IOPS가 증가합니다.
- 파티셔닝/보관 정책 없이 방치하면 인덱스 비대화로 역효과가 납니다.
삭제/보관 정책 필수
- published_at 기준 7~30일 보관 후 아카이브 또는 삭제 정책을 두세요.
- 무기한 보관은 감사 요건이 아니라면 거의 항상 비용 손해입니다.
CDC 운영 책임
- 커넥터 장애, 스키마 변경, 권한 이슈를 누가 소유할지 명확히 정해야 합니다.
- “플랫폼팀 소유 + 서비스팀 온콜 연계”처럼 책임 경계를 문서화하세요.
체크리스트 또는 연습
- 우리 시스템에 이중 쓰기 지점이 몇 개인지 목록화했다.
- outbox_events 스키마와 보관 정책(예: 14일)을 정의했다.
- 소비자 멱등 키(event_id 또는 도메인 unique)를 구현했다.
- p95 발행 지연, 적체 건수, 재시도 실패율 대시보드를 만들었다.
- “이벤트 누락 발생 시 15분 내 복구” 런북을 작성하고 리허설했다.
연습 과제:
- 현재 주문 플로우에서 DB 저장 후 메시지 발행 코드를 찾아 실패 시나리오 3개를 적어보세요.
- outbox + CDC로 바꿨을 때 장애 전파 경로가 어떻게 줄어드는지 시퀀스 다이어그램으로 그려보세요.
- 소비자 멱등성 없이 운영했을 때와 비교해 월간 운영 비용(장애 대응 시간)을 추정해보세요.
💬 댓글