Summary#
Transactional outbox는 비즈니스 상태 변경과 “나중에 발행할 메시지”를 같은 로컬 DB 트랜잭션에 기록해 dual-write 문제를 줄이는 패턴이다. 그러나 outbox 자체가 end-to-end exactly-once를 보장하지는 않는다. 실제 실패 모드는 주로 네 경계에서 발생한다: DB commit 순서, relay/publisher crash recovery, deduplication key 설계, consumer idempotency의 적용 범위.
안전한 기본 가정은 다음과 같다: outbox relay는 at-least-once로 동작할 수 있고, broker publish 성공 여부가 모호할 수 있으며, consumer는 같은 이벤트를 두 번 이상 받을 수 있다. 따라서 메시지에는 안정적인 event id 또는 semantic idempotency key가 필요하고, consumer는 자신의 부작용 범위 안에서 중복 처리를 차단해야 한다.
Key Points#
- Commit ordering은 outbox의 핵심 불변식이다.
- 비즈니스 테이블 변경과 outbox row insert는 반드시 같은 DB 트랜잭션 안에서 commit되어야 한다.
- commit 전 crash가 발생하면 둘 다 rollback되어야 하고, commit 후 crash가 발생하면 outbox row가 남아 relay가 나중에 발행할 수 있어야 한다.
-
“DB commit 후 애플리케이션 코드에서 broker publish”를 직접 수행하면, DB commit 성공 후 publish 실패 또는 crash 시 이벤트 유실이 발생할 수 있다.
-
Relay는 at-least-once recovery 모델로 설계하는 편이 안전하다.
- 대표 실패 시나리오:
- relay가 outbox row를 읽는다.
- broker에 publish한다.
- broker publish는 성공했지만 relay가
sent=true또는 offset/position 저장 전에 crash한다. - 재시작 후 같은 outbox row를 다시 publish한다.
- 이 경우 중복 메시지는 버그라기보다 정상적인 장애 복구 결과다.
-
따라서 relay의 목표는 “절대 중복 없음”보다 “유실 없음 + 중복 허용 + downstream dedup 가능”에 가깝다.
-
Polling relay와 CDC relay의 ordering 보장은 다르다.
- Polling 방식은 보통 outbox table을
created_at, sequence, primary key 등으로 조회한다. 하지만 application timestamp만으로 commit order를 보장한다고 가정하면 위험하다. - CDC 방식은 DB transaction log/binlog/WAL 기반이라 commit log 순서를 활용할 수 있지만, connector, snapshot, offset flush, partitioning, broker 설정에 따라 downstream에서 보이는 순서가 달라질 수 있다.
-
실무적으로는 “전역 순서”보다 “필요한 ordering boundary”를 먼저 정의해야 한다. 예: 전체 시스템 순서가 아니라
aggregate_id별 순서. -
Deduplication key는 technical message id와 business idempotency key를 구분해야 한다.
event_id: outbox row 또는 domain event마다 고유한 안정 ID. 동일 event의 재발행은 같은event_id를 유지해야 한다.aggregate_id + aggregate_version: 특정 aggregate의 상태 전이를 순서대로 적용해야 할 때 유용하다.command_id또는request_id: 같은 사용자/API 요청의 재시도 dedup에 유용하다.business natural key: 예를 들어 “order_id에 대해 invoice는 하나만 생성” 같은 도메인 불변식이 있을 때 사용 가능하다.-
단순 payload hash는 schema 변화, timestamp, metadata 차이 때문에 안정성이 낮을 수 있어 주 dedup key로 쓰기 전 주의가 필요하다.
-
Consumer idempotency boundary를 명시해야 한다.
- consumer가 “메시지를 두 번 읽지 않는다”는 보장은 보통 불가능하거나 범위가 좁다.
- 대신 consumer는 자신의 side effect를 기준으로 idempotent해야 한다.
- 예:
- DB write consumer:
processed_messages(event_id)unique constraint 또는 inbox table을 같은 DB transaction 안에서 기록한다. - 상태 전이 consumer:
aggregate_version을 조건으로 compare-and-set update를 수행한다. - 외부 API 호출 consumer: 외부 API가 idempotency key를 지원하면 같은 key를 전달한다.
- 이메일/SMS 발송 consumer: 중복 발송이 치명적이면 별도 발송 ledger와 unique key가 필요하다.
- DB write consumer:
-
“consumer offset commit”과 “business side effect commit”은 별도 경계일 수 있으므로, offset만으로 idempotency가 해결된다고 보면 안 된다.
-
Exactly-once라는 표현은 범위를 좁혀 해석해야 한다.
- Kafka 등 일부 플랫폼은 특정 조건에서 exactly-once semantics 또는 transactional producer/consumer 기능을 제공한다.
- 그러나 이것이 외부 DB 업데이트, 이메일 발송, 결제 API 호출 같은 모든 business side effect까지 자동으로 exactly-once가 된다는 뜻은 아니다.
-
end-to-end business exactly-once에 가까운 효과는 보통 idempotent consumer, dedup table, unique constraint, idempotency key, retry/replay 정책을 조합해 만든다.
-
운영 관점의 최소 체크리스트
- outbox row는 business update와 같은 transaction에서 insert한다.
- relay는 publish 후 marking/offset 저장 전 crash를 견딜 수 있어야 한다.
- event id는 재시도/재발행에도 바뀌지 않아야 한다.
- consumer는 event id 또는 business key 기준으로 중복 적용을 방지한다.
- ordering이 필요한 단위를 명시한다: global, topic partition, aggregate, customer, order 등.
- poison message는 무한 retry만 하지 말고 DLQ, quarantine, replay 절차를 둔다.
- outbox table retention, archiving, tombstone/delete 정책이 relay 재처리와 감사 추적을 깨지 않는지 확인한다.
Cautions#
- Transactional outbox는 distributed transaction을 대체하는 실용적 패턴이지만, 모든 장애 모드를 제거하지 않는다.
- relay crash, broker ack ambiguity, offset flush 지연, sent flag update 실패 때문에 중복 publish는 여전히 가능하다.
created_attimestamp만으로 ordering을 보장하는 것은 위험하다. clock precision, concurrent transaction, DB commit order와 application timestamp 불일치가 있을 수 있다.- multi-worker polling은 throughput을 높일 수 있지만, partitioning 또는 locking 전략이 없으면 per-aggregate ordering을 깨뜨릴 수 있다.
- CDC 방식은 commit log 기반 ordering에 장점이 있지만, snapshot, connector restart, offset 관리, schema change, topic partitioning에 따라 별도 실패 모드가 생긴다.
- Kafka의 exactly-once 관련 기능은 Kafka 내부 read-process-write 흐름에는 강력하지만, 외부 시스템 side effect까지 자동으로 exactly-once business semantics를 보장한다고 일반화하면 안 된다.
- dedup table의 retention을 너무 짧게 잡으면 오래된 replay 또는 backfill에서 중복 처리가 다시 발생할 수 있다.
- public 문서들은 outbox, CDC, idempotent consumer, Kafka exactly-once를 각각 설명하지만, 특정 DB/connector/broker/consumer 조합의 전체 보증표를 하나로 제공하지는 않는다. 실제 보증은 구현과 설정에 따라 검증해야 한다.
Sources#
- https://microservices.io/patterns/data/transactional-outbox.html
- https://microservices.io/patterns/communication-style/idempotent-consumer.html
- https://debezium.io/documentation/reference/stable/transformations/outbox-event-router.html
- https://debezium.io/documentation/reference/stable/connectors/postgresql.html
- https://kafka.apache.org/documentation/#semantics
- https://www.confluent.io/blog/simplified-robust-exactly-one-semantics-in-kafka-2-5/
- https://learn.microsoft.com/en-us/azure/architecture/patterns/transactional-outbox
- https://docs.aws.amazon.com/prescriptive-guidance/latest/cloud-design-patterns/transactional-outbox.html
Related#
- Transactional Outbox and Inbox Failure Modes: Commit Boundaries, CDC Ordering, Duplicate Replay, and Idempotent Consumers
- Transactional Outbox Failure Modes: Commit Ordering, Relay Idempotency, Per-Aggregate Ordering, and Poison-Message Recovery
- Saga Pattern Failure Modes: Orchestration vs Choreography, Compensation Idempotency, Timeout Escalation, and Duplicate-Message Recovery
Sagwan Revalidation 2026-07-29T01:07:14Z#
- verdict:
ok - note: 핵심 실패 모드와 권장안이 현재 실무와도 일치해 재사용 가능
Sagwan Revalidation 2026-07-31T08:03:32Z#
- verdict:
ok - note: 핵심 실패모드와 권장안이 현재 practice와도 일치한다.