Outbox / Inbox 패턴을 “깊이(depth)” 단계별로 쌓아 올리는 방식으로 정리해봤다.
(이벤트 기반/마이크로서비스에서 “DB 트랜잭션 ↔ 메시지 발행”의 정합성을 맞추는 용도)
서비스가 DB에 상태 변경(예: 주문 생성)을 한 뒤, 곧바로 Kafka 같은 브로커로 이벤트를 발행하면
DB 커밋은 성공했는데 메시지 발행은 실패(또는 반대) 같은 “반쪽 성공”이 생길 수 있다.
Outbox는 이를 막기 위해,
즉 “DB가 진실(Source of Truth)이고, 이벤트는 DB에 먼저 적어둔 뒤 안전하게 발행”하는 구조.
브로커(Kafka)는 “최소 1회(at-least-once)” 전달이 흔해서, 컨슈머는 중복 메시지를 받을 수 있다.
Inbox는 컨슈머가 메시지를 처리할 때,
즉 “수신 측에서 중복 처리를 방지해서 정확히 한 번처럼 보이게” 만드는 구조.
Outbox 테이블
id(eventId, UUID 등), aggregate_type, aggregate_id, event_typepayload(JSON), status(PENDING/SENT/FAILED), created_at업무 트랜잭션 안에서 Outbox 레코드 저장
Outbox 퍼블리셔(폴링 워커)
status=PENDING 레코드를 조회 → Kafka 발행 → 성공 시 SENT로 업데이트재시도 정책(최소)
FAILED로 남기고 다음 폴링에서 재시도(또는 backoff)최소 구성에서는 “폴링 기반”이 가장 단순
Inbox 테이블(Processed Messages)
event_id(유니크), consumer_name, processed_at컨슘 시 처리 순서
event_id가 Inbox에 이미 있으면 → 종료(중복 무시)핵심은 “업무 처리”와 “처리완료 기록”을 같은 트랜잭션으로 묶는 것.
Inbox가 있으려면 eventId가 반드시 고유해야 하고, 컨슈머가 그걸 신뢰해야 한다.
eventId 생성 주체
퍼블리셔가 2개 이상 떠도 안전해야 함(이중 발행 방지)
방법(선택지)
SELECT … FOR UPDATE SKIP LOCKEDPENDING -> SENDING으로 CAS 업데이트 후 발행“Kafka 발행 성공”을 어디까지로 볼지
실패/타임아웃 시: 실제론 기록됐는데 타임아웃일 수 있어 → 중복 발행 가능성을 전제로 Inbox가 필요해짐(서로 보완 관계)
payload(JSON)의 필드가 바뀌면 컨슈머가 깨지기 쉬움
최소 방어:
event_type + version 넣기(OrderCreatedV1 같은 네이밍)Outbox에 FAILED가 쌓이면 “업무는 됐는데 이벤트가 안 나감” 상태
최소한 필요:
CDC 기반 Outbox (Debezium)
폴링 대신 DB binlog를 읽어 Kafka로 내보내 성능/지연 개선
Exactly-once처럼 보이게 만들기 조합
Idempotency Key를 도메인 명령에도 적용
리플레이 전략
대용량 최적화
order-server
OrderAfterCreateV1hub-server
OrderAfterCreateV1 컨슘 시 Inbox로 중복 제거HubRouteAfterCreateV1delivery-server
HubRouteAfterCreateV1 컨슘 시 Inbox