18-8. Outbox Pattern
DB 저장은 성공했는데 이벤트는 왜 사라졌을까?
지난 글에서는 이벤트 중복 처리(Idempotency) 를 알아보았습니다.
이벤트 기반 아키텍처에서는
같은 이벤트가
여러 번
전달될 수 있기 때문에
Consumer는
멱등하게
구현해야 한다는 것도
살펴보았습니다.
그런데 이번에는
반대의 문제가
생깁니다.
"이벤트가 두 번 오는 것이 아니라, 아예 안 왔습니다."
"주문은 저장되었는데 배송이 시작되지 않았습니다."
"결제는 완료됐는데 포인트가 적립되지 않았습니다."
이 문제를 해결하기 위해 등장한 패턴이 바로 Outbox Pattern입니다.
이번 글에서는 이벤트 기반 시스템에서 가장 중요한 패턴 중 하나인 Outbox Pattern을 알아보겠습니다.
먼저 상황을 보자
쇼핑몰입니다.
사용자가
주문을
생성합니다.
OrderService는
다음 작업을
수행합니다.
주문 저장
↓
OrderCreated Event 발행
아주
단순해
보입니다.
코드도
다음처럼
작성할 수 있습니다.
@Transactional
public void createOrder() {
orderRepository.save(order);
kafkaTemplate.send(
"order",
new OrderCreated(...)
);
}
문제 없어
보입니다.
그런데 정말 안전할까?
다음 상황을
생각해 보겠습니다.
주문 저장
성공
입니다.
그런데
바로 다음 줄에서
Kafka 장애
가
발생했습니다.
결과는
어떻게 될까요?
DB
주문 있음
Kafka
이벤트 없음
입니다.
배송 시스템은
이벤트를
받지 못합니다.
포인트도
적립되지 않습니다.
통계도
갱신되지 않습니다.
이것이 Dual Write 문제
DB와
Message Broker는
서로
다른 시스템입니다.
다음 두 작업을
수행해야 합니다.
DB 저장
그리고
이벤트 발행
입니다.
이 두 작업은
원자적(Atomic)
으로
처리되지 않습니다.
즉
하나는 성공
하나는 실패
할 수 있습니다.
이것을
Dual Write Problem
이라고 합니다.
왜 Transaction으로 해결하지 않을까?
많은 개발자가
생각합니다.
@Transactional이면
되지 않을까?
안 됩니다.
Spring의
Transaction은
보통
DB만
보장합니다.
Kafka는
DB Transaction의
범위 밖입니다.
즉
DB Commit
≠
Kafka 성공
입니다.
Outbox Pattern의 아이디어
Outbox Pattern은
아주 단순한
아이디어에서
시작합니다.
이벤트도
DB에
같이 저장하자.
입니다.
즉
주문을
저장할 때
다음도
함께
저장합니다.
ORDER
+
OUTBOX_EVENT
입니다.
둘 다
같은
Transaction에서
Commit합니다.
Outbox 테이블
예를 들어
다음과 같습니다.
OUTBOX_EVENT
id
event_type
payload
status
created_at
입니다.
주문 생성
Transaction에서
ORDER 저장
↓
OUTBOX_EVENT 저장
↓
Commit
합니다.
이제
DB에는
이벤트가
안전하게
남아 있습니다.
누가 Kafka로 보낼까?
여기서
새로운
컴포넌트가
등장합니다.
Outbox Publisher
입니다.
이 컴포넌트는
주기적으로
OUTBOX_EVENT를
조회합니다.
status
=
READY
인
데이터를
찾습니다.
그리고
Kafka에
전송합니다.
Kafka
↓
성공
하면
상태를
변경합니다.
SENT
입니다.
전체 흐름
주문 저장
↓
Outbox 저장
↓
Commit
↓
Publisher 조회
↓
Kafka 전송
↓
SENT
입니다.
Kafka가 죽어 있다면?
문제 없습니다.
Publisher가
전송을
실패합니다.
하지만
Outbox에는
이벤트가
남아 있습니다.
Kafka가
다시
살아나면
Publisher가
재시도합니다.
결국
이벤트는
전송됩니다.
즉
이벤트
유실
방지
가
가능합니다.
Outbox Pattern의 핵심
핵심은
다음 한 줄입니다.
DB 저장과
이벤트 저장은
같은 Transaction
입니다.
Kafka 전송은
나중에
해도
됩니다.
Payload는 무엇을 저장할까?
Outbox에는
보통
이벤트를
그대로
저장합니다.
예를 들어
{
"eventId": "evt-1001",
"orderId": 100,
"memberId": 5
}
입니다.
JSON으로
저장하는
경우가
가장 많습니다.
Publisher가 중간에 죽으면?
예를 들어
Kafka 전송
직후
Publisher가
죽었습니다.
상태는
아직
READY입니다.
다시
시작하면
같은 이벤트를
또
전송합니다.
문제가
생길까요?
아닙니다.
지난 글에서
배운
멱등성
이
바로
필요한 이유입니다.
Consumer는
중복 이벤트를
무시해야 합니다.
Polling 방식
가장 많이
사용하는
방식입니다.
5초마다
Outbox 조회
입니다.
새로운
이벤트가
있으면
전송합니다.
구현이
간단합니다.
CDC 방식
더 발전된
방법도
있습니다.
바로
CDC(Change Data Capture)
입니다.
DB의
변경 로그를
감지합니다.
새로운
Outbox 데이터가
생기면
즉시
Kafka로
전송합니다.
대표적으로
Debezium
을
많이
사용합니다.
Polling보다
지연이
적습니다.
Outbox Pattern의 장점
첫 번째
이벤트가
유실되지 않습니다.
두 번째
DB와
이벤트의
정합성을
높일 수 있습니다.
세 번째
재시도가
쉽습니다.
네 번째
장애가
발생해도
복구가
가능합니다.
단점도 있다
Outbox
테이블을
관리해야 합니다.
Publisher도
운영해야 합니다.
이미
전송한
데이터를
삭제하거나
아카이빙하는
정책도
필요합니다.
구조가
조금
복잡해집니다.
실무에서는 언제 사용할까?
다음과 같은
기능에서
매우 많이
사용합니다.
주문
결제
회원가입
배송
포인트
즉
이벤트가
절대
유실되면
안 되는
업무입니다.
Outbox Pattern이 필요한 이유
다음 한 문장으로
정리할 수 있습니다.
DB는
성공했는데
이벤트는
실패할 수 있다.
Outbox Pattern은
이 문제를
해결하기 위한
패턴입니다.
실무 체크리스트
① DB 저장과 이벤트 저장을 같은 Transaction으로 처리하는가?
② Outbox 테이블이 있는가?
③ Publisher가 재시도하는가?
④ 전송 완료 상태를 관리하는가?
⑤ Consumer는 멱등한가?
⑥ 오래된 Outbox 데이터를 정리하는 정책이 있는가?
Outbox Pattern vs 기존 방식
항목기존 방식Outbox Pattern
| DB 저장 | 성공 | 성공 |
| 이벤트 발행 | 즉시 | Outbox를 통해 |
| Kafka 장애 | 이벤트 유실 가능 | 재시도 가능 |
| 정합성 | 낮음 | 높음 |
| 운영 복잡도 | 낮음 | 다소 높음 |
정리
이벤트 기반 시스템에서는 DB 저장과 이벤트 발행이 서로 다른 시스템에서 수행되기 때문에 Dual Write 문제가 발생할 수 있습니다.
Outbox Pattern은 비즈니스 데이터와 이벤트를 하나의 DB Transaction으로 함께 저장하고, 이후 별도의 Publisher가 이를 Broker(Kafka, RabbitMQ 등)로 전달하는 방식입니다.
이를 통해 이벤트 유실을 방지하고, 장애가 발생해도 안전하게 재시도할 수 있습니다.
다만 Publisher 운영, Outbox 테이블 관리, 데이터 정리 정책 등 추가적인 운영 요소가 필요하며, Consumer는 중복 전송에 대비해 멱등성(Idempotency) 을 반드시 보장해야 합니다.
다음 글 예고
다음 글에서는 18-9. Saga Pattern을 알아보겠습니다.
MSA에서는 하나의 Transaction으로 여러 서비스를 묶을 수 없습니다. Saga Pattern이 분산 트랜잭션을 어떻게 해결하는지, Choreography 방식과 Orchestration 방식의 차이까지 실무 사례와 함께 자세히 살펴보겠습니다.
'language > java' 카테고리의 다른 글
| Dead Letter Queue(DLQ) (0) | 2026.08.03 |
|---|---|
| Saga Pattern (0) | 2026.08.03 |
| 이벤트 중복 처리(Idempotency) (0) | 2026.08.03 |
| 이벤트 설계 방법 (0) | 2026.08.03 |
| Kafka와 RabbitMQ의 차이 (0) | 2026.08.03 |
댓글