Transactional Outbox
비즈니스 변경과 발행할 이벤트를 같은 로컬 트랜잭션에 저장해 Dual Write 불일치를 막는다.
이 문서의 목차
Overview
Transactional Outbox는 주문 Row와 “주문 생성 이벤트” Row를 같은 DB Transaction으로 Commit한다. DB Commit 뒤 별도 Relay가 Outbox를 Broker로 전달한다. 핵심은 DB와 Kafka를 하나의 분산 Transaction으로 묶는 것이 아니라 Dual Write 지점을 DB 하나로 줄이는 것이다.
한 문장 설명: 업무 데이터와 보낼 편지를 같은 금고에 넣고, 배달부가 나중에 편지를 전달하는 패턴이다.
이 문서의 범위: 여기서는 Dual Write를 하나의 Local Transaction으로 줄이는 원자성에 집중한다. Claim Lease, Retry, 장애 복구, 관측과 Cleanup은 Durable Outbox 운영 설계에서 이어서 다룬다.
핵심 용어
| 용어 | 뜻 |
|---|---|
| Dual Write | DB 저장과 메시지 발행처럼 서로 다른 시스템을 연속 갱신하는 것 |
| Outbox Row | 발행할 Event ID, Aggregate, Payload, 상태를 저장한 행 |
| Relay | 미발행 Row를 찾아 Broker에 전달하는 별도 Process |
| At-least-once | 손실을 피하는 대신 같은 이벤트가 중복 전달될 수 있는 보장 |
왜 필요한가
DB Commit 후 Broker 전송이 실패하면 주문은 생겼지만 이벤트는 사라진다. 반대로 전송 후 DB Rollback이면 존재하지 않는 주문 이벤트가 나간다. Retry만으로는 두 결과를 원자적으로 만들 수 없다. Outbox는 로컬 Transaction의 원자성을 이용해 “업무 변경은 있는데 발행 기록은 없음”을 막는다.
핵심 원리와 내부 동작
sequenceDiagram; participant A as Application; participant D as Database; participant R as Relay; participant K as Broker; A->>D: BEGIN; A->>D: INSERT order; A->>D: INSERT outbox; A->>D: COMMIT; R->>D: 미발행 Row 조회; R->>K: publish(eventId); K-->>R: ack; R->>D: published 처리
Relay가 Publish하고 상태 변경 전에 죽으면 중복이 생긴다. 따라서 Outbox는 보통 손실 회피와 At-least-once 전달을 제공하며 Consumer 멱등성이 짝이다. Event ID, Aggregate ID, Type, Schema Version, 발생 시각을 명시한다.
Example
BEGIN;
UPDATE orders SET status = 'PAID' WHERE id = 42;
INSERT INTO outbox(event_id, aggregate_id, event_type, payload, status)
VALUES ('evt-7', '42', 'OrderPaid', '{"orderId":42,"schemaVersion":1}', 'NEW');
COMMIT;Spring에서는 같은 @Transactional Service 안에서 두 Repository 저장을 수행한다. Kafka 전송을 그 Transaction 안에서 기다리지 않는다.
실무에서 발생하는 문제와 Trade-off
Outbox Table 증가, Relay Lag, Poison Event, 중복, 순서 역전, Schema 변경이 새 운영 대상이 된다. 즉시 강한 일관성이 아니라 지연된 최종 일관성이다. 2PC보다 결합도가 낮고 장애 복구가 쉽지만, Consumer는 “한 번만 온다”를 가정할 수 없다.
흔한 오해
- DB와 Kafka가 하나의 Transaction이 되는 패턴이 아니다.
- Outbox Row 저장 성공은 Event 소비 완료를 뜻하지 않는다.
published=true만 두면 영원히 실패한 Event와 중복 경쟁을 충분히 다루지 못한다.
Production Considerations
Relay Lag, 가장 오래된 NEW Row 나이, Publish 실패율, 재시도 횟수, Table 크기를 관찰한다. Claim Lease와 Backoff, DLQ/격리, 보관·삭제 정책은 Durable Outbox 운영 설계의 상태 수명주기로 구체화한다. 개인정보는 Payload 최소화와 암호화·삭제 정책을 적용한다.
다른 사람에게 설명한다면
30초: “DB와 메시지 Broker에 동시에 쓰면 한쪽만 성공할 수 있습니다. Outbox는 업무 데이터와 이벤트를 같은 DB Transaction에 기록하고 Relay가 나중에 발행합니다. 손실은 막지만 중복 가능성이 있어 Consumer 멱등성이 필요합니다.”
2분: Dual Write의 두 실패 순서를 예로 들고, Local Transaction → Relay → At-least-once → Inbox/Dedup까지 연결해 설명한다. 끝에는 Relay Lag과 Cleanup도 운영해야 한다고 덧붙인다.
Interview Questions
- Outbox가 해결하는 Dual Write 실패 두 가지는?
- Relay Publish 성공 후 상태 갱신 전에 죽으면 어떻게 되는가?
- 2PC와 비교한 장단점은?
- Event 순서를 어떻게 보장할 것인가?
Related Topics
Durable Outbox 운영 설계, Outbox Relay와 CDC, 멱등성·중복·순서, Kafka를 이어서 본다.
SOURCE REFERENCES
이 문서의 근거
본문은 Dev Atlas 안에서 완결되며, 검증이 필요할 때만 원문을 확인할 수 있습니다.