Kafka 마스터 — 배치·병렬·에러·트랜잭션

2026-05-03확률과 통계 마스터 노트

카프카 마스터 노트 시리즈 7편. 배치 처리(receiveBatch)와 병렬 패턴(concatMap vs flatMap), 에러 핸들링 전략(재시도·격리·DLQ·파이프라인 분리), 멱등성 설계 원칙, Kafka Transaction과 정확히 한 번(Exactly-Once) 전달, isolation.level=read_committed의 의미, transactional.id 관리까지 — 운영 고급 패턴.

이 글은 카프카 마스터 노트 시리즈의 일곱 번째 편입니다. 6편까지 단순한 read·write였다면, 이번엔 운영 고급 패턴 — 배치·병렬·에러·트랜잭션.

at-least-once는 모든 분산 시스템의 기본. 그런데 정확히 한 번(exactly-once)은 어떻게? Kafka Transaction이 그 답. DLQ·재시도·멱등성도 함께 봅니다.

처음 이 단원이 어렵게 느껴지는 이유

처음 이 단원이 어렵게 느껴지는 이유는 두 가지예요. 첫째, at-least-once vs exactly-once 차이가 막연합니다. 트랜잭션이 어떻게 그걸 보장하나? 둘째, 에러 처리 패턴이 너무 많아 보입니다. retry·DLQ·circuit breaker·격리… 어느 게 결정적인가?

해결법은 한 가지예요. **"멱등성이 모든 답의 시작"**이라는 관점. 멱등 처리만 보장되면 at-least-once만 충분. 트랜잭션은 양방향 (read+write) 흐름에서만 진짜 필요. 이 그림이 잡히면 나머지는 채우기만 하면 됩니다.

배치 처리 — 처리량 ↑

receiveBatch (Reactor Kafka)

receiver.receiveBatch()    // ConsumerRecords 단위
    .concatMap(records ->
        Flux.fromIterable(records)
            .flatMap(record -> processAsync(record), 4)
            .doOnComplete(() -> records.iterator().next().receiverOffset().commit())
    )
    .subscribe();

장점 — 한 번에 N 메시지 처리. 처리량 ↑.

여기서 시험 함정이 하나 있어요. batch ack는 batch 단위. 한 메시지만 실패해도 전체 영향. 세밀 제어는 record 단위.

Producer 배치 (3편)

batch.size=16384
linger.ms=10

자동 배치. 처리량과 지연 균형.

병렬 처리 — concatMap vs flatMap

concatMap — 순서 보장

receiver.receive()
    .concatMap(record -> processAsync(record))
    .subscribe();

한 메시지 끝나야 다음. 같은 파티션 안 순서 보장.

flatMap — 병렬

receiver.receive()
    .flatMap(record -> processAsync(record), 16)   // 동시 16
    .subscribe();

순서 깨짐. 처리량 ↑.

groupBy — 파티션별 병렬 + 순서

receiver.receive()
    .groupBy(record -> record.partition())
    .flatMap(group ->
        group.concatMap(record -> processAsync(record))
    )
    .subscribe();

파티션 안 순서 + 파티션 사이 병렬. 운영 권장 패턴.

여기서 정말 중요한 시험 함정 — 병렬 처리 + 순서 보장 = groupBy + concatMap. 가장 흔한 실무 패턴.

에러 핸들링 5 전략

1. 재시도 (Retry)

일시적 에러 (네트워크 일시 끊김·DB 일시 락)는 재시도.

.retryWhen(Retry.backoff(3, Duration.ofSeconds(1))
    .maxBackoff(Duration.ofSeconds(10))
    .filter(t -> t instanceof TransientException))

지수 백오프 권장.

2. 격리 (Isolation)

한 메시지 처리 실패가 다른 메시지에 영향 안 가도록.

receiver.receive()
    .flatMap(record ->
        processAsync(record)
            .onErrorResume(e -> {
                log.error("Skip", e);
                record.receiverOffset().acknowledge();
                return Mono.empty();
            }),
        16
    )

3. Dead Letter Queue (DLQ)

처리 실패 메시지를 별도 토픽으로:

.onErrorResume(e ->
    sender.send(SenderRecord.create(
        new ProducerRecord<>("order-events.dlq", record.key(), record.value()),
        record.receiverOffset()
    ))
)

DLQ에서 수동 검토·복구.

여기서 정말 중요한 시험 함정 — DLQ는 운영 필수. 처리 불가 메시지가 컨슈머를 영원히 막으면 시스템 마비. DLQ로 격리.

4. 파이프라인 분리

오래 걸리는 작업은 별도 컨슈머로:

input-events → 빠른 컨슈머 → 검증·라우팅
            → slow-events → 느린 컨슈머 → 복잡 처리

5. Circuit Breaker

외부 시스템 죽으면 일시 중단:

.onErrorResume(CircuitBreakerOpenException.class, e -> {
    pauseConsumer();
    return Mono.empty();
})

Resilience4j 등 사용.

멱등성 — 분산 시스템의 핵심

같은 메시지 N번 처리해도 결과 같음.

// 비멱등
INSERT INTO orders VALUES (...)
// 같은 메시지 두 번 처리 → 두 번 삽입

// 멱등
INSERT INTO orders VALUES (...) ON CONFLICT (id) DO NOTHING
// 또는
UPSERT

여기서 정말 중요한 시험 함정 — at-least-once + 멱등 처리 = 사실상 exactly-once. 트랜잭션 없이도 정확성 확보. 비싼 트랜잭션 회피.

멱등 처리 패턴

패턴 설명
자연 멱등 UPSERT, SET (값 같으면 결과 같음)
메시지 ID 추적 DB에 처리한 ID 저장, 재시도 시 무시
Outbox + 멱등 발행 DB 트랜잭션과 발행 분리 (9~15편)

Kafka Transaction — 정확히 한 번

문제 — Read-Process-Write 흐름

1. Consumer poll
2. process
3. Producer send (새 메시지)
4. Consumer commit offset

만약 3 성공 후 4 실패 → 다음 poll에서 재처리 → 중복 send!

Transaction 해결

beginTransaction()
  send(...)
  sendOffsetsToTransaction(offsets, groupId)
commitTransaction()

→ send와 offset commit이 원자적
→ 둘 다 성공 또는 둘 다 실패

Kafka Transaction 코드

producer.initTransactions();

try {
    producer.beginTransaction();
    producer.send(new ProducerRecord<>(...));
    producer.send(new ProducerRecord<>(...));

    Map<TopicPartition, OffsetAndMetadata> offsets = ...;
    producer.sendOffsetsToTransaction(offsets, consumer.groupMetadata());

    producer.commitTransaction();
} catch (Exception e) {
    producer.abortTransaction();
}

필수 설정

# Producer
transactional.id=order-tx-1   # 고유
enable.idempotence=true       # 자동 활성화
acks=all                      # 자동

# Consumer
isolation.level=read_committed   # 커밋된 트랜잭션만

여기서 정말 중요한 시험 함정 — isolation.level=read_committed 필수. 기본은 read_uncommitted(트랜잭션 진행 중도 보임). 컨슈머 측에서 read_committed로 바꿔야 정확히 한 번.

transactional.id 관리

- 인스턴스마다 고유 ID
- 같은 인스턴스 재시작 시 같은 ID 유지
- ID로 좀비 인스턴스 fence

운영 — Kubernetes pod 이름 + 영구 식별자 결합.

Reactor Kafka에서 트랜잭션

sender.transactionManager().begin()
    .thenMany(receiver.receive()
        .flatMap(record ->
            sender.send(...)
                .then(record.receiverOffset().commit())
        )
    )
    .onErrorResume(e -> sender.transactionManager().abort())
    .doFinally(sig -> sender.transactionManager().commit())
    .subscribe();

Reactor Kafka는 트랜잭션 미지원 (일부 버전). 이때 Outbox Pattern 필요 (9~15편).

At-most / At-least / Exactly-Once 정리

보장 설명 구현
At-most-once 손실 가능, 중복 X Commit 먼저
At-least-once 손실 X, 중복 가능 처리 후 Commit + 멱등
Exactly-once 손실·중복 X Transaction 또는 멱등

여기서 시험 함정이 하나 있어요. 대부분 케이스 = At-least-once + 멱등. Transaction은 비용 큼. 정말 필요할 때만.

DLQ 운영 패턴

order-events           ← 정상 토픽
order-events.dlq       ← DLQ (처리 실패)
order-events.dlq.retry ← 재시도 가능

DLQ 워크플로우:

  1. 처리 실패 → DLQ로
  2. 운영자가 검토
  3. 코드 수정 후 DLQ → 재시도 토픽으로 이동
  4. 재처리

시험 직전 한 번 더 — 자주 헷갈리는 함정 모음

여기까지가 7편의 핵심입니다. 시험 직전 또는 실무에서 헷갈릴 때 다시 펼쳐 볼 수 있게 압축 노트로 마무리할게요.

  • 배치 — receiveBatch() 처리량 ↑, batch 단위 ack
  • Producer 배치 — batch.size + linger.ms
  • 병렬 — concatMap (순서) / flatMap (병렬) / groupBy+concatMap (운영 권장)
  • 에러 5 전략 — 재시도 / 격리 / DLQ / 파이프라인 분리 / Circuit Breaker
  • 재시도 = 지수 백오프 (Retry.backoff)
  • DLQ는 운영 필수 — 처리 불가 메시지 격리
  • 멱등성 = 분산 시스템 핵심
  • at-least-once + 멱등 = 사실상 exactly-once
  • 멱등 패턴 — UPSERT / 메시지 ID 추적 / Outbox
  • Kafka Transaction = read-process-write 원자성
  • 필수 설정 — transactional.id (고유) / isolation.level=read_committed
  • 기본 isolation은 read_uncommitted (트랜잭션 중도 보임)
  • 컨슈머에 read_committed 필수
  • sendOffsetsToTransaction() = offset과 send 원자적
  • transactional.id 관리 — 인스턴스마다 고유, 재시작 시 유지
  • 좀비 fence
  • Reactor Kafka 트랜잭션 일부 미지원 → Outbox Pattern (9~15편)
  • 보장 3 단계 — at-most / at-least / exactly
  • 대부분 = at-least + 멱등
  • Transaction은 비용 큼, 정말 필요할 때만
  • DLQ 워크플로우 — 처리 실패 → 검토 → 수정 → 재처리

시리즈 다른 편

공식 문서: Kafka Transactions 에서 더 깊이.

다음 글(8편, 마지막)에서는 Spring Kafka·통합 테스트·보안 — 실무 통합과 운영 환경 보안까지 시리즈 마무리.

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

답글 남기기

error: Content is protected !!