TestForge Blog
← 전체 포스트

Outbox Pattern 설계 가이드 - 이벤트 드리븐 시스템에서 데이터 정합성을 어떻게 지킬까

DB 업데이트와 메시지 발행을 함께 처리해야 할 때 dual write 문제는 거의 반드시 등장합니다. 이 글에서는 Outbox Pattern이 필요한 이유, 테이블 설계, 발행 워커 구조, 중복 처리, 재시도, 운영 포인트까지 실제 아키텍처 관점으로 설명합니다.

TestForge Team ·

왜 Dual Write가 문제인가

예를 들어 주문 서비스가 아래 두 작업을 해야 한다고 가정해봅시다.

  1. 주문을 DB에 저장
  2. OrderCreated 이벤트를 Kafka에 발행

겉으로는 단순하지만, 둘 중 하나만 성공하면 정합성이 깨집니다.

  • DB 저장 성공, 메시지 발행 실패
  • 메시지 발행 성공, DB 저장 실패

이 문제를 보통 dual write라고 부릅니다.

분산 트랜잭션으로 해결하지 않는 이유

이론적으로는 분산 트랜잭션이 떠오르지만, 실무에서는 아래 이유로 잘 쓰지 않습니다.

  • 시스템 간 결합도가 높아짐
  • 운영 복잡도가 커짐
  • 메시지 브로커와 DB를 강하게 묶기 어려움

그래서 이벤트 기반 마이크로서비스에서는 Outbox Pattern이 사실상 표준에 가깝습니다.

Outbox Pattern의 핵심 아이디어

핵심은 간단합니다.

  • 비즈니스 데이터 변경
  • 발행할 이벤트 데이터 기록

이 두 가지를 같은 DB 트랜잭션 안에서 처리합니다.

그리고 별도 워커가 Outbox 테이블을 읽어 메시지 브로커로 발행합니다.

기본 흐름

Application
 -> DB Transaction
    -> orders insert
    -> outbox insert
 -> Commit

Publisher Worker
 -> read unpublished outbox rows
 -> publish to broker
 -> mark as published

이 구조의 장점은 “적어도 이벤트를 잃어버리지는 않는다”는 점입니다.

Outbox 테이블은 어떻게 설계할까

보통 아래 필드를 둡니다.

  • id
  • aggregate_type
  • aggregate_id
  • event_type
  • payload
  • status
  • created_at
  • published_at
  • retry_count

예를 들면:

CREATE TABLE outbox_events (
  id BIGSERIAL PRIMARY KEY,
  aggregate_type VARCHAR(100) NOT NULL,
  aggregate_id VARCHAR(100) NOT NULL,
  event_type VARCHAR(100) NOT NULL,
  payload JSONB NOT NULL,
  status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
  retry_count INT NOT NULL DEFAULT 0,
  created_at TIMESTAMP NOT NULL DEFAULT NOW(),
  published_at TIMESTAMP
);

발행 워커는 어떤 방식으로 돌릴까

대표적으로 두 방식이 있습니다.

Polling 방식

  • 일정 주기로 PENDING 이벤트 조회
  • 브로커에 발행
  • 성공 시 PUBLISHED 업데이트

장점:

  • 단순하고 구현이 쉬움

단점:

  • 지연이 약간 생김
  • polling 부담 관리 필요

CDC 방식

  • Debezium 같은 도구로 Outbox 변경 감지
  • DB 변경 이벤트를 바로 브로커로 전달

장점:

  • 지연이 낮고 자동화가 높음

단점:

  • 운영 복잡도가 커짐

초기에는 Polling으로 시작하는 경우가 많고, 대규모 시스템에서 CDC를 고려합니다.

중복 발행은 반드시 생긴다고 생각해야 한다

Outbox를 써도 “정확히 한 번” 발행을 쉽게 보장할 수 있는 것은 아닙니다.

예:

  • 브로커 발행 성공
  • 상태 업데이트 전 워커 장애

이 경우 재시도 시 중복 발행이 가능합니다.

그래서 소비자는 아래를 전제로 설계해야 합니다.

  • 멱등성 키 사용
  • 중복 이벤트 무해화
  • 이미 처리한 이벤트 추적

즉, Outbox는 이벤트 유실을 줄여주지만 중복까지 없애주지는 않습니다.

장애 시 운영 포인트

운영에서 중요한 것은 아래입니다.

  • PENDING 적체량
  • 재시도 횟수 증가
  • 특정 이벤트 타입 발행 실패율
  • 소비자 처리 지연

Outbox는 애플리케이션 로직이 아니라 운영 파이프라인으로 봐야 합니다.

자주 하는 실수

  • Outbox 적재만 하고 발행 지표를 보지 않음
  • 재시도 정책이 없어 장애 시 무한 적체
  • payload 스키마 버전 관리 부재
  • 소비자 멱등성을 고려하지 않음
  • 대량 적체 시 일괄 재발행 전략이 없음

언제 특히 효과적인가

  • 주문/결제/배송 같이 상태 전이가 중요한 서비스
  • DB를 기준으로 진실 원천을 유지해야 하는 경우
  • 이벤트 유실이 허용되지 않는 도메인

반대로 실시간성이 절대적으로 중요하고, 운영 복잡도를 감수할 수 있다면 CDC 기반 고급 구조를 검토할 수 있습니다.

마무리

Outbox Pattern은 이벤트 드리븐 시스템에서 “메시지 발행을 트랜잭션 바깥에 두면서도 정합성을 최대한 지키는 방법”입니다.

완벽한 마법은 아니지만, dual write 문제를 실무적으로 가장 잘 다루는 패턴 중 하나입니다.
핵심은 Outbox를 도입하는 것 자체보다, 중복 처리, 재시도, 적체 관찰까지 포함해 운영 가능한 구조로 만드는 데 있습니다.