DB 변경과 이벤트 발행을 안전하게 함께 처리하는 패턴이다. 실제 데이터 변경과 "이 이벤트를 내보내라"는 기록(outbox 행)을 같은 DB 트랜잭션에 넣고, 별도 워커가 outbox를 읽어 메시지 브로커로 보낸다.
풀려는 문제: 이중 쓰기
① DB에 주문 저장 (커밋 성공)
② 브로커에 "주문 생성됨" 발행 (네트워크 오류로 실패)
→ 주문은 있는데 아무도 모른다순서를 바꿔도 마찬가지다. 서로 다른 두 시스템은 한 트랜잭션으로 묶을 수 없으므로, 둘 중 하나만 성공하는 순간이 반드시 생긴다.
해결 구조
[한 트랜잭션]
INSERT INTO orders ...
INSERT INTO outbox (aggregate_id, type, payload) ...
↓ 둘 다 커밋되거나 둘 다 롤백
[릴레이 워커(message relay)] outbox 폴링(또는 CDC로 읽기) → 브로커 발행 → 처리 완료 표시- 원자성 덕분에 데이터 변경과 "할 일"이 함께 남는다. 스프링이라면 같은 @Transactional 안에서 두 행을 저장하면 된다
- 워커가 발행 후 완료 표시 전에 죽으면 같은 이벤트를 다시 보낸다. 그래서 소비자는 멱등해야 한다
- outbox 테이블을 CDC (변경 데이터 캡처)로 읽으면 폴링(polling) 없이 거의 실시간으로 내보낼 수 있다
언제 쓰나
주문·결제처럼 "DB에 반영됐다면 반드시 다른 서비스도 알아야 하는" 흐름에 쓴다. 이벤트 형태를 테이블 구조와 분리해 도메인 이벤트로 설계할 수 있다는 점이 CDC 단독 방식과 다르다. 애그리거트 단위로 이벤트를 내는 DDD(Domain-Driven Design) 설계와 잘 맞는다.