백엔드 데이터 인프라 88편 — Kafka Message Delivery Semantics (at-most·at-least·exactly-once)

2026-05-17백엔드 데이터 인프라

백엔드 데이터 인프라 88편. Kafka Message Delivery Semantics — at-most-once·at-least-once·exactly-once 3가지 의미 보장의 정확한 차이, Idempotent Producer + Transactions 로 풀어낸 EOS, isolation.level read_committed, 실무 선택 가이드까지 풀어쓴 학습 노트.

📚 백엔드 데이터 인프라 · 88편 — Kafka Message Delivery Semantics (at-most·at-least·exactly-once)

이 글은 백엔드 데이터 인프라 시리즈 130편 중 88편이에요. 87편 까지 Consumer 설계를 풀었다면, 이번 88편은 Kafka 의 가장 중요한 보장 개념인 Message Delivery Semantics(메시지 전달 보장)를 다뤄요. 시스템 신뢰성과 데이터 정확성의 핵심.

Delivery Semantics가 어렵게 느껴지는 이유

at-most-once, at-least-once, exactly-once 라는 단어가 직관적으로 잘 분리되지 않아요.

첫째, "exactly-once 가 가능한가?" 자체가 분산 시스템에서 논쟁의 영역이에요. 많은 시스템이 "exactly-once 보장"이라고 주장하지만 세부 조건이 다 달라요.

둘째, Producer 측과 Consumer 측의 의미가 달라요. publish 시점 보장과 consume 시점 보장은 별도 영역.

셋째, EOS (Exactly-Once Semantics, 정확히 한 번 보장) 메커니즘이 복잡해요. Idempotent Producer, Transactions, Consumer isolation.level 의 3가지 조합으로 굴러가요.

이번 글에서 3가지 의미, 각각의 구현 패턴, EOS 메커니즘, 실무 선택까지 정리해요.

3가지 의미 보장

At-Most-Once

"메시지가 손실될 수 있지만 절대 중복 전달 안 됨"

  • Producer 가 retry 안 함
  • Consumer 가 process 전 commit
  • 메시지가 한 번 또는 0번

언제 — 로그·메트릭 같은 손실 OK + 중복 X 데이터. 매우 드묾.

At-Least-Once (Kafka 기본)

"메시지가 절대 손실 안 되지만 중복 전달 가능"

  • Producer 가 retry 함 (실패 시 재시도)
  • Consumer 가 process 후 commit
  • 메시지가 한 번 이상

언제 — 대부분의 환경. 중복은 멱등성으로 처리 가 일반적.

Exactly-Once

"메시지가 정확히 한 번"

  • 손실 X + 중복 X
  • 가장 강한 보장이지만 비용 큼

언제 — 결제·금융·법적 데이터. Kafka Streams 의 기본 모드.

Publishing 측 — Producer 보장

At-Most-Once

acks=0
retries=0

Producer 가 한 번 쏘고 잊어버려요(fire-and-forget). 실패하면 그냥 잃어요.

At-Least-Once (예전 기본)

acks=all
retries=2147483647
enable.idempotence=false

실패 시 retry → 성공했는데 ACK 못 받았으면 중복 전송이 일어나요.

Exactly-Once — Idempotent Producer

enable.idempotence=true       # Kafka 3.0+ 기본

86편에서 본 메커니즘:

  • Producer 가 Producer ID (PID, 프로듀서 고유 ID) 받음
  • 각 메시지 sequence number 부여
  • Broker 가 (PID, sequence) 추적 → 중복 거부

Producer 가 같은 메시지 두 번 보내도 broker 에 한 번만 저장돼요. publishing 단계의 exactly-once.

자동 활성 조건 (3.0+ 기본):

  • acks=all
  • retries > 0
  • max.in.flight.requests.per.connection ≤ 5

Consuming 측 — Consumer 보장

At-Most-Once

1. messages = consumer.poll()
2. consumer.commitSync()         ← 먼저 commit
3. process(messages)             ← 그 후 처리

3단계에서 죽으면 다음 consumer 가 이미 commit 된 다음 메시지부터 시작해요. 처리 못 한 메시지가 손실돼요.

At-Least-Once (기본)

1. messages = consumer.poll()
2. process(messages)             ← 먼저 처리
3. consumer.commitSync()         ← 그 후 commit

3단계 전에 죽으면 다음 consumer 가 commit 안 된 처음부터 다시 읽어요. 중복 처리가 발생해요.

대부분 환경에서는 멱등성으로 중복 처리를 무해화해요:

  • DB INSERT 가 primary key 기반 — 두 번 처리해도 같은 결과
  • Counter 증가 = INCR key:N 이 아닌 SET key:N value 같은 멱등 패턴

Exactly-Once — Transactions

여기가 가장 복잡한 자리예요. Producer + Consumer + 두 topic 사이가 모두 atomic 해야 해요.

시나리오 — Kafka Streams 흐름

Topic A → Stream Processor → Topic B
              │
              └─ offset commit to Kafka

3가지가 함께 성공하거나 함께 실패해야 해요:

  1. Topic B 에 결과 write
  2. Topic A 의 offset advance
  3. Stream Processor state update

Transactional Producer

producer.initTransactions();
producer.beginTransaction();
producer.send(new ProducerRecord<>("topic-B", ...));
producer.sendOffsetsToTransaction(offsets, consumerGroupMetadata);
producer.commitTransaction();

commitTransaction() 이 atomic 마커예요. 실패 시 abortTransaction().

Consumer 측 — isolation.level

기본값은 read_uncommitted — 진행 중이거나 실패한 transaction 메시지도 보여요.

EOS 환경 = read_committed:

isolation.level=read_committed

이렇게 두면 commit 된 transaction 메시지만 보여요. Abort 된 transaction 메시지는 consumer 에게 안 보여요.

Kafka Streams 의 EOS

props.put(StreamsConfig.PROCESSING_GUARANTEE_CONFIG, StreamsConfig.EXACTLY_ONCE_V2);

한 줄로 EOS 가 켜져요. 내부적으로 transactions + idempotent + read_committed 가 모두 자동으로 설정돼요.

EOS의 한계 — Kafka 외부 시스템

여기가 정말 중요해요. Kafka 안에서는 EOS 풀 보장이 가능해요. 하지만 Kafka → 외부 시스템(DB·HTTP API·파일)으로 데이터 sink 할 때는 얘기가 달라요:

  • DB INSERT 후 Kafka offset commit 사이에 죽으면 DB 에는 들어갔는데 offset 은 안 올라가요. 다음 consumer 가 DB 에 또 INSERT 해서 중복이 생겨요.

해법 패턴:

1. Idempotent External Operations

DB INSERT 가 primary key 기반 upsert 면 중복 처리해도 같은 결과. 가장 단순해요.

2. Two-Phase Commit (XA, 2단계 커밋 표준)

DB 와 Kafka 사이를 2PC 로 묶어요. 극히 드물어요. 대부분 시스템이 2PC 를 지원하지 않아요.

3. Transactional Outbox + CDC (Change Data Capture, 변경 데이터 캡처)

DB 트랜잭션 안에서 outbox 테이블에 메시지를 저장해요. 별도 프로세스(Debezium·Kafka Connect)가 outbox 테이블을 읽어 Kafka 로 publish 해요. DB 와 Kafka 가 함께 atomic.

4. Consumer 가 Offset 도 Output Store 에 저장

Kafka Connect HDFS(Hadoop Distributed File System) connector 가 좋은 예. 데이터와 offset 을 같은 HDFS 파일에 함께 저장해요. 같은 곳에 있으니 atomic.

정리 — 보장 수준 선택

시나리오 권장
로그·메트릭 (손실 OK) At-most-once (acks=0)
대부분 일반 데이터 At-least-once + 멱등성
Kafka 내부 Stream Processing EOS v2 (Kafka Streams)
외부 DB sink + 정확성 결정적 Transactional Outbox + at-least-once
결제·금융 Transactional Outbox + 강한 멱등성

기본 권장은 at-least-once + 멱등성. 대부분 환경에 충분하고 운영이 단순해요.

EOS 는 진짜 필요한 자리에만 써요. 메커니즘이 복잡하고 성능 부담이 약 20% 정도.

한계·실무 함정

1. EOS 가 "모든 상황 exactly-once" 가 아님

위에서 강조한 자리. Kafka 내부만 보장돼요. 외부 시스템은 별도 패턴.

2. Transaction timeout

기본값은 transaction.timeout.ms=60000 (60초). transaction 처리가 더 길어지면 abort 돼요. 큰 batch 환경이면 늘려야 해요.

3. read_committed 의 지연 시간

EOS consumer 는 transaction commit 까지 대기해요. Last Stable Offset(LSO, 안정 오프셋) 까지만 읽어서 지연 시간이 약간 늘어요.

4. Idempotent Producer 활성화 자동 효과

enable.idempotence=true (3.0+ 기본) 가 acks=all 과 retries 까지 자동 설정해요. 명시적으로 안 박아도 안전.

5. Old client + EOS

EOS 는 Kafka 2.0+ 가 필요해요. Old client 와는 호환되지 않아요.

시험 직전 한 번 더 — Kafka Delivery Semantics 함정 압축 노트

  • 3가지 의미 = at-most-once · at-least-once · exactly-once
  • At-most-once = 손실 가능, 중복 X — 로그·메트릭만
  • At-least-once = 손실 X, 중복 가능 — 대부분 기본
  • Exactly-once = 손실 X, 중복 X — 가장 강한 보장
  • Publishing 보장 vs Consuming 보장 별도
  • Producer at-most-once = acks=0 + retries=0
  • Producer at-least-once = acks=all + retries (예전 기본)
  • Producer exactly-once (Publishing) = Idempotent Producer (enable.idempotence=true)
  • Kafka 3.0+ = idempotent 기본값
  • 자동 활성 조건 = acks=all + retries > 0 + max.in.flight ≤ 5
  • Consumer at-most-once = poll → commit → process (실패 시 손실)
  • Consumer at-least-once = poll → process → commit (실패 시 중복) — 기본
  • 대부분 = at-least-once + 멱등성으로 중복 처리 무해화
  • Consumer exactly-once = Transactions + isolation.level=read_committed
  • Transactional Producer = initTransactions·beginTransaction·sendOffsetsToTransaction·commitTransaction
  • isolation.level=read_committed = commit 된 transaction 메시지만 보임
  • Kafka Streams EOS = PROCESSING_GUARANTEE_CONFIG=EXACTLY_ONCE_V2 한 줄
  • EOS 한계 = Kafka 내부만, 외부 시스템 별도
  • 외부 sink 패턴 — Idempotent ops · Two-Phase Commit · Transactional Outbox · Offset 도 output store 에
  • Transactional Outbox = DB 트랜잭션에 outbox 테이블 → Debezium 으로 Kafka publish
  • 기본 권장 = at-least-once + 멱등성
  • EOS = 결제·금융·정확성 결정적 자리만
  • EOS 비용 ~20% 성능 부담
  • 함정 — transaction.timeout.ms 기본 60초, 큰 batch 면 늘림
  • 함정 — read_committed 는 LSO 까지만 읽음, 약간 지연
  • 함정 — Old client + EOS 호환 X (2.0+ 필요)

공식 문서: Kafka Design — Message Delivery Semantics 에서 자세한 사양을 확인할 수 있어요.

시리즈 다른 편 (앞뒤 글 모음)

이전 글:

다음 글:

※ 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

답글 남기기

error: Content is protected !!