카프카 마스터 노트 시리즈 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 워크플로우:
- 처리 실패 → DLQ로
- 운영자가 검토
- 코드 수정 후 DLQ → 재시도 토픽으로 이동
- 재처리
시험 직전 한 번 더 — 자주 헷갈리는 함정 모음
여기까지가 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 워크플로우 — 처리 실패 → 검토 → 수정 → 재처리
시리즈 다른 편
- 1편 — EDA·Kafka 기초·KRaft
- 2편 — Topic·Partition·Offset
- 3편 — Producer·Consumer 동작
- 4편 — Consumer Group·리밸런싱
- 5편 — Reactor Kafka
- 6편 — Cluster·HA·Best Practices
- 7편 — 배치·에러·트랜잭션 (현재 글)
- 8편 — Spring Kafka·테스트·보안
- 9편 — Spring Cloud Stream 기초
- 10편 — StreamBridge 동적 라우팅
- 11편 — Fan-Out / Fan-In
- 12편 — SCS Tips & Tricks
- 13편 — Saga 코레오그래피
- 14편 — Saga 오케스트레이터
- 15편 — Transactional Outbox
공식 문서: Kafka Transactions 에서 더 깊이.
다음 글(8편, 마지막)에서는 Spring Kafka·통합 테스트·보안 — 실무 통합과 운영 환경 보안까지 시리즈 마무리.