본문 바로가기
language/java

Outbox Pattern

by 죄니안죄니 2026. 8. 3.
반응형

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

댓글