본문으로 건너뛰기

Outbox 패턴으로 Kafka Exactly-Once에 가까워지기

· 약 7분
Johny Cho
Back End Engineer @ NHN


이번 포스트에서는 Debezium(CDC)을 사용하지 않고,
Outbox 패턴을 활용해 effectively exactly-once를 달성하는 구조를 정리해보려고 합니다.

분산 시스템에서
“메시지를 정확히 한 번 처리한다(Exactly-Once)” 는 요구사항은 매우 흔하지만,
이를 end-to-end로 완벽하게 보장하는 것은 현실적으로 거의 불가능합니다.

Kafka, DB, 외부 API는 각각 독립적인 시스템이며
이들을 하나의 트랜잭션으로 묶을 수 없기 때문입니다.

그래서 실무에서는 보통 다음을 목표로 합니다.

중복은 허용하되,
최종 비즈니스 결과(effect)는 정확히 한 번만 반영되도록 설계한다.

1. 문제 배경

DB update → Kafka produce

이 구조에서는 다음 문제가 발생할 수 있습니다.

  • DB는 반영됐지만 Kafka 발행 실패 → 이벤트 유실
  • Kafka는 발행됐지만 DB 트랜잭션 롤백 → 잘못된 이벤트 전파
  • 네트워크 장애로 성공/실패 여부를 알 수 없음

➡️ DB와 Kafka 사이의 정합성이 깨짐


2. Outbox 패턴 개요

Outbox 패턴의 핵심 아이디어는 단순합니다.

이벤트 발행을 DB 트랜잭션의 일부로 만든다.

즉,

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

같은 트랜잭션 안에서 처리합니다.

Kafka 발행은 이후 **비동기 Relay(배치/스케줄러)**가 담당합니다.


3. 전체 아키텍처 흐름

1) Producer (Application)

  • 비즈니스 로직 수행
  • Outbox 테이블에 이벤트 INSERT
  • 하나의 DB 트랜잭션으로 커밋

이 단계에서:

  • Kafka 장애와 무관하게 이벤트는 DB에 안전하게 저장된다
  • 애플리케이션 장애가 발생해도 이벤트 유실은 없다

2) Outbox Relay (폴링 기반)

  • Outbox 테이블에서 PENDING 상태 이벤트 조회
  • Kafka로 메시지 발행
  • 성공 시 SENT 상태로 변경
  • 실패 시 재시도

이 과정에서:

  • 네트워크 오류, Kafka 장애로 발행 실패 가능
  • 타임아웃으로 인한 중복 발행 가능성 존재
  • 대신 최소 한 번(at-least-once) 발행은 보장

3) Consumer

  • Kafka 메시지 소비
  • eventId 기준으로 이미 처리된 이벤트인지 확인
  • 처음 처리하는 이벤트만 비즈니스 로직 수행
  • 처리 이력 DB에 기록

메시지는 중복 소비될 수 있지만,
DB 멱등 처리로 비즈니스 결과는 정확히 한 번만 반영됩니다.


4. Outbox 테이블 예시

  • id (PK)
  • event_id (전역 유일)
  • payload
  • status (PENDING, SENT)
  • created_at

실무에서는 Outbox row를 즉시 DELETE하기보다
SENT 상태로 남긴 뒤 TTL 또는 배치로 정리하는 방식이 더 안전합니다.
(감사 로그, 재처리, 리플레이 대응)


5. Consumer 멱등 처리 방식

Consumer는 반드시 중복 처리를 방지해야 합니다.

대표적인 방법:

  • processed_event 테이블에 event_id를 PK로 저장
  • 또는 비즈니스 테이블에 event_id UNIQUE 제약 추가

이렇게 하면:

  • 동일 메시지가 여러 번 와도
  • DB 반영은 단 한 번만 발생

6. 이 구조가 보장하는 수준

구간보장 수준
DB → OutboxExactly-once
Outbox → KafkaAt-least-once
Kafka → ConsumerAt-least-once
최종 비즈니스 결과Effectively exactly-once

즉,

전달은 최소 한 번,
결과는 정확히 한 번


7. 왜 “완전한 Exactly-Once”가 아닐까?

  • Kafka 발행은 중복될 수 있다
  • Consumer는 재처리될 수 있다
  • 네트워크 장애로 성공 여부를 확정할 수 없다

따라서 물리적인 의미의 Exactly-once는 아닙니다.

하지만,

  • eventId 기반 멱등 처리
  • 유니크 제약
  • 처리 이력 관리

를 통해 비즈니스 관점에서는 Exactly-once와 동일한 효과를 얻게됩니다.


8. 주의사항

  1. eventId 설계가 가장 중요

    • 재시도/재발행에도 동일해야 함
    • 전역 유일성 필수
  2. 중복 발행은 정상 동작

    • Consumer 멱등 처리는 선택이 아니라 필수
  3. 외부 API 포함 시

    • Idempotency-Key 지원 여부가 핵심
    • 없으면 호출 로그 + 재시도/보상 트랜잭션 필요

마치며

분산 시스템에서 물리적으로 정확한 Exactly-once를 end-to-end로 보장하는 것은 현실적으로 불가능합니다.

대신 실무에서는 이벤트 발행 과정에서의 유실을 막고, 중복 전달은 허용하되 비즈니스 결과는 한 번만 반영되도록 설계하는 것이 더 중요합니다.

이를 위해 Outbox 패턴으로 이벤트 발행을 최소 한 번 보장하고, Consumer에서 eventId 기반 멱등 처리를 적용함으로써 end-to-end로는 결과가 정확히 한 번만 반영되도록 설계할 수 있습니다.

Exactly-once는 설정의 문제가 아니라, 어디에서 중복을 허용하고 어디에서 막을 것인지에 대한 설계의 문제입니다.

Loading comments...