2 min read
Kafka Outbox Pattern 실무 적용
트랜잭션 일관성과 이벤트 전달 신뢰성을 동시에 확보하는 Outbox 파이프라인 설계

도입
이벤트 기반 아키텍처에서 가장 자주 깨지는 가정은 "DB 커밋과 메시지 발행이 동시에 성공할 것"이라는 기대다. 실제 운영에서는 둘 중 하나만 성공하는 경우가 반드시 생긴다. Outbox 패턴은 이 간극을 메우는 현실적인 방법이다.
이 글은 Kafka 기반 서비스에서 Outbox를 도입할 때 필요한 스키마 설계, relay 프로세스, 소비자 멱등성까지 한 번에 정리한다.

문제 정의
Outbox 없이 운영하면 다음 장애가 반복된다.
- 주문 DB는 성공했는데 이벤트 발행 실패로 다운스트림 시스템이 업데이트되지 않는다.
- 이벤트는 발행됐는데 DB 롤백이 발생해 "유령 이벤트"가 생긴다.
- 재시도 과정에서 동일 이벤트가 여러 번 소비되어 상태가 틀어진다.
- 장애 복구 시 어느 시점부터 재처리해야 하는지 기준이 없다.
핵심은 DB 트랜잭션 안에서 "상태 변경 + 이벤트 기록"을 원자적으로 처리하는 것이다.
핵심 개념
| 구성요소 | 역할 | 운영 포인트 |
|---|---|---|
| Outbox Table | 발행 예정 이벤트 저장 | 상태(PENDING/SENT/FAILED) 추적 |
| Relay Worker | Outbox -> Kafka 전달 | 재시도/백오프/배치 크기 제어 |
| Consumer Idempotency | 중복 소비 방지 | 이벤트 키 기반 처리 기록 |
| Replay Strategy | 장애 복구 | 오프셋 + outbox 상태 동기화 |
코드 예시 1: 트랜잭션 내 Outbox 기록
import { db } from "@/lib/db";
export async function placeOrder(command: {
orderId: string;
userId: string;
amount: number;
}) {
await db.transaction(async (tx) => {
await tx.query(
`INSERT INTO orders (id, user_id, amount, status)
VALUES ($1, $2, $3, 'PLACED')`,
[command.orderId, command.userId, command.amount],
);
await tx.query(
`INSERT INTO outbox_events (event_id, topic, aggregate_id, payload, status)
VALUES ($1, 'order.placed', $2, $3::jsonb, 'PENDING')`,
[
crypto.randomUUID(),
command.orderId,
JSON.stringify({
orderId: command.orderId,
userId: command.userId,
amount: command.amount,
}),
],
);
});
}
코드 예시 2: Relay Worker(배치 발행 + 상태 전이)
export async function relayOutboxBatch(limit = 100) {
const rows = await db.query(
`SELECT event_id, topic, payload
FROM outbox_events
WHERE status = 'PENDING'
ORDER BY created_at ASC
LIMIT $1
FOR UPDATE SKIP LOCKED`,
[limit],
);
for (const row of rows) {
try {
await kafkaProducer.send({
topic: row.topic,
messages: [{ key: row.event_id, value: JSON.stringify(row.payload) }],
});
await db.query(
`UPDATE outbox_events SET status = 'SENT', sent_at = now() WHERE event_id = $1`,
[row.event_id],
);
} catch {
await db.query(
`UPDATE outbox_events
SET status = 'FAILED', retry_count = retry_count + 1
WHERE event_id = $1`,
[row.event_id],
);
}
}
}
아키텍처 흐름
Mermaid diagram rendering...
트레이드오프
- Outbox는 데이터 정합성을 높이지만 테이블/워커 운영 비용이 추가된다.
- 즉시성(near real-time)이 중요하면 relay 주기와 배치 크기 튜닝이 필요하다.
- 소비자 멱등성까지 함께 설계하지 않으면 중복 이벤트 문제는 남는다.
정리
Outbox 패턴은 완벽한 이론보다 불완전한 현실에 강한 전략이다. 상태 변경과 이벤트 기록을 같은 트랜잭션에 묶고, relay/consumer를 운영 기준으로 관리하면 장애 시 복구 가능성이 크게 올라간다.
이미지 출처
- Cover: source link
- License: CC BY-SA 3.0 / Author: BalticServers.com
- Note: Wikimedia Commons 무료 라이선스 이미지를 다운로드 후 1600px 기준 JPG로 최적화했습니다.