Architectureadvanced검토 2026.08

Transactional Outbox

비즈니스 변경과 발행할 이벤트를 같은 로컬 트랜잭션에 저장해 Dual Write 불일치를 막는다.

#outbox#dual-write#eventual-consistency#messaging

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 WriteDB 저장과 메시지 발행처럼 서로 다른 시스템을 연속 갱신하는 것
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

  1. Outbox가 해결하는 Dual Write 실패 두 가지는?
  2. Relay Publish 성공 후 상태 갱신 전에 죽으면 어떻게 되는가?
  3. 2PC와 비교한 장단점은?
  4. Event 순서를 어떻게 보장할 것인가?

Durable Outbox 운영 설계, Outbox Relay와 CDC, 멱등성·중복·순서, Kafka를 이어서 본다.

SOURCE REFERENCES

이 문서의 근거

본문은 Dev Atlas 안에서 완결되며, 검증이 필요할 때만 원문을 확인할 수 있습니다.

원문 출처 보기 1