2 min read

Kafka Outbox Pattern 실무 적용

트랜잭션 일관성과 이벤트 전달 신뢰성을 동시에 확보하는 Outbox 파이프라인 설계

Kafka Outbox Pattern 실무 적용 thumbnail

도입

이벤트 기반 아키텍처에서 가장 자주 깨지는 가정은 "DB 커밋과 메시지 발행이 동시에 성공할 것"이라는 기대다. 실제 운영에서는 둘 중 하나만 성공하는 경우가 반드시 생긴다. Outbox 패턴은 이 간극을 메우는 현실적인 방법이다.

이 글은 Kafka 기반 서비스에서 Outbox를 도입할 때 필요한 스키마 설계, relay 프로세스, 소비자 멱등성까지 한 번에 정리한다.

Kafka Outbox Pattern 실무 적용 커버
Wikimedia Commons 기반 무료 이미지

문제 정의

Outbox 없이 운영하면 다음 장애가 반복된다.

  • 주문 DB는 성공했는데 이벤트 발행 실패로 다운스트림 시스템이 업데이트되지 않는다.
  • 이벤트는 발행됐는데 DB 롤백이 발생해 "유령 이벤트"가 생긴다.
  • 재시도 과정에서 동일 이벤트가 여러 번 소비되어 상태가 틀어진다.
  • 장애 복구 시 어느 시점부터 재처리해야 하는지 기준이 없다.

핵심은 DB 트랜잭션 안에서 "상태 변경 + 이벤트 기록"을 원자적으로 처리하는 것이다.

핵심 개념

구성요소역할운영 포인트
Outbox Table발행 예정 이벤트 저장상태(PENDING/SENT/FAILED) 추적
Relay WorkerOutbox -> 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로 최적화했습니다.

댓글