Outbox Pattern 설계 가이드 - 이벤트 드리븐 시스템에서 데이터 정합성을 어떻게 지킬까
DB 업데이트와 메시지 발행을 함께 처리해야 할 때 dual write 문제는 거의 반드시 등장합니다. 이 글에서는 Outbox Pattern이 필요한 이유, 테이블 설계, 발행 워커 구조, 중복 처리, 재시도, 운영 포인트까지 실제 아키텍처 관점으로 설명합니다.
왜 Dual Write가 문제인가
예를 들어 주문 서비스가 아래 두 작업을 해야 한다고 가정해봅시다.
- 주문을 DB에 저장
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 테이블은 어떻게 설계할까
보통 아래 필드를 둡니다.
idaggregate_typeaggregate_idevent_typepayloadstatuscreated_atpublished_atretry_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를 도입하는 것 자체보다, 중복 처리, 재시도, 적체 관찰까지 포함해 운영 가능한 구조로 만드는 데 있습니다.