|

Outbox pattern은 왜 메시지 유실을 줄여줄까

Outbox pattern 메시지 유실 방지 구조 대표 이미지
Outbox pattern은 비즈니스 데이터와 발행할 이벤트를 같은 트랜잭션에 저장한다.

Outbox pattern은 이벤트 기반 아키텍처에서 자주 등장하는 패턴입니다. 핵심 문제는 DB 저장은 성공했는데 메시지 발행이 실패하면 어떻게 할 것인가입니다.

기준은 비즈니스 데이터 변경과 발행할 이벤트 기록을 같은 트랜잭션에 묶는 것입니다. 메시지 broker로 바로 보내는 대신 outbox table에 먼저 남깁니다.


Outbox pattern 처리 흐름

메시지 유실을 줄이는 흐름 카드
Outbox pattern은 비즈니스 데이터와 발행할 이벤트를 같은 트랜잭션에 저장한다.

문제는 DB commit과 메시지 발행 사이에 있다

주문을 저장한 뒤 주문 생성 이벤트를 발행한다고 생각해 보겠습니다. DB commit은 성공했는데 broker publish 직전에 서버가 죽으면 주문은 생겼지만 이벤트는 사라집니다.

Outbox table에 먼저 기록한다

microservices.io의 Transactional outbox 설명은 메시지를 DB 트랜잭션 안에 저장한 뒤 별도 relay가 메시지를 발행하는 방식으로 이 문제를 다룹니다.

BEGIN;
INSERT INTO orders(id, status) VALUES (42, 'CREATED');
INSERT INTO outbox_events(id, type, payload, status)
VALUES (1001, 'OrderCreated', '{"orderId":42}', 'READY');
COMMIT;

publisher가 outbox를 발행한다

별도 publisher는 READY 상태의 outbox event를 읽어 broker로 발행합니다. 성공하면 상태를 PUBLISHED로 바꾸거나 발행 시각을 남깁니다.

  • polling publisher는 주기적으로 outbox table을 조회한다
  • CDC는 DB 변경 로그를 읽어 broker로 전달한다
  • 일시적 broker 장애는 retry로 처리한다
  • 소비자는 중복 메시지를 견딜 수 있어야 한다

정리

Outbox pattern은 메시지를 정확히 한 번만 보내주는 마법이 아닙니다. 대신 DB 변경과 이벤트 기록을 같은 트랜잭션에 넣어 유실 위험을 줄이고, publisher 재시도와 idempotent consumer로 운영 안정성을 높이는 구조입니다.


Outbox pattern 장애 지점 카드
이번 글에서 실제로 판단해야 할 기준을 카드로 정리했다.

Outbox pattern이 필요한 실패 시나리오

주문을 저장한 뒤 Kafka나 RabbitMQ로 OrderCreated 메시지를 보내는 코드를 생각해 보겠습니다. DB commit은 성공했는데 메시지 발행 직전에 서버가 죽으면 주문 데이터는 남지만 다른 서비스는 주문이 생긴 사실을 모릅니다.

1. 주문 DB 저장 성공
2. DB transaction commit 성공
3. 메시지 브로커로 OrderCreated 발행 시도
4. 발행 직전 서버 장애

결과: DB에는 주문이 있지만 메시지는 없다.

반대로 메시지를 먼저 보내고 DB commit이 실패하면 더 위험할 수도 있습니다. 다른 서비스는 주문이 생겼다고 믿는데, 실제 주문 데이터는 없습니다. Outbox pattern은 이 두 작업을 억지로 하나의 분산 트랜잭션으로 묶기보다, DB 안에 발행할 메시지를 같이 남기는 방식으로 문제를 줄입니다.

Outbox 테이블에는 무엇을 넣을까

  • event_id: 중복 발행과 추적을 위한 고유 id
  • aggregate_type / aggregate_id: 어떤 도메인 객체에서 나온 이벤트인지
  • event_type: OrderCreated, PaymentApproved 같은 이벤트 이름
  • payload: 소비자가 처리할 최소 데이터
  • status 또는 published_at: 발행 여부와 재시도 판단 기준
  • created_at / retry_count: 지연과 장애 분석을 위한 정보
CREATE TABLE outbox_event (
    event_id VARCHAR(64) PRIMARY KEY,
    aggregate_type VARCHAR(50) NOT NULL,
    aggregate_id VARCHAR(64) NOT NULL,
    event_type VARCHAR(80) NOT NULL,
    payload JSON NOT NULL,
    published_at TIMESTAMP NULL,
    retry_count INT NOT NULL DEFAULT 0,
    created_at TIMESTAMP NOT NULL
);

중요한 점은 outbox_event 저장이 주문 저장과 같은 DB transaction 안에서 일어난다는 것입니다. 그러면 주문만 있고 이벤트 기록은 없는 상태를 줄일 수 있습니다.

polling publisher와 CDC는 무엇이 다를까

  • polling publisher: 애플리케이션 배치나 워커가 outbox 테이블을 주기적으로 조회해서 발행한다
  • CDC: Debezium 같은 변경 데이터 캡처 도구가 DB 변경 로그를 읽어 브로커로 전달한다
  • polling은 이해하기 쉽지만 조회 주기와 잠금, 재시도 설계를 신경 써야 한다
  • CDC는 지연이 적고 확장성이 좋을 수 있지만 운영 복잡도가 올라간다

중복 메시지를 없애는 패턴은 아니다

Outbox pattern은 메시지 유실을 줄여주지만 중복 발행 가능성을 완전히 없애지는 않습니다. 발행은 성공했는데 published_at 업데이트 전에 장애가 나면 같은 이벤트를 다시 보낼 수 있습니다.

그래서 소비자 쪽에서는 idempotent consumer가 필요합니다. event_id를 처리 이력에 남기거나, 비즈니스 키 기준으로 이미 처리한 이벤트인지 확인해야 합니다.

도입 전에 확인할 체크리스트

  • 이벤트 유실이 실제 장애로 이어지는 업무인가
  • 소비자가 중복 메시지를 견딜 수 있는가
  • outbox 테이블이 무한히 커지지 않도록 보관 정책이 있는가
  • 실패한 이벤트를 재처리하고 관찰할 운영 화면이나 로그가 있는가
  • polling과 CDC 중 현재 팀 운영 수준에 맞는 방식은 무엇인가

관련해서 같이 보면 좋은 글

REST API 멱등성이란 무엇인가도 함께 보면 이 글의 기준을 더 쉽게 연결할 수 있습니다.

JPA fetch join과 Pageable을 같이 쓰면 왜 위험할까도 함께 보면 이 글의 기준을 더 쉽게 연결할 수 있습니다.

함께보면 좋은 글