카프카 ack-mode에서 offset 처리
Batch
개념
- Spring Kafka의 기본 ack# offset 처리 중 발생한 문제
특징
장점
- offset commit 횟수가 줄어들어 네트워크 오버헤드가 감소한다.
- 개발자가 커밋 타이밍을 신경 쓸 필요 없음
단점
- 배치 처리 중 애플리케이션이 중단되면
- 배치의 일부 메시지만 처리되고 offset은 commit되지 않습니다
- 재시작 시 이미 처리된 메시지들도 다시 처리됩니다
- 이는 정상적인 상황으로, at-least-once 보장을 위한 동작입니다
Record
개념
- 각 레코드(메시지)가 처리될 때마다 개별적으로 offset을 커밋하는 모드
- 배치 내의 각 레코드 처리가 완료되면 즉시 해당 레코드까지의 offset을 커밋
- 리스너 컨테이너가 각 레코드 처리 완료를 감지하면 자동으로 커밋하므로 별도의
acknowledge()호출 불필요
특징
장점
- 메시지 손실 위험을 최소화 (각 레코드별로 커밋)
- 애플리케이션 중단 시 재처리해야 할 메시지 수가 적음
- 정확한 처리 지점까지만 커밋되어 중복 처리 최소화
단점
- offset commit 횟수가 많아져 네트워크 오버헤드 증가
- Kafka 브로커에 부하 증가 (빈번한 커밋 요청)
- 처리량(throughput) 감소 가능성
사용 시나리오
- 메시지 처리 실패 시 재처리 비용이 높은 경우
- 정확한 offset 관리가 중요한 비즈니스 로직
- 높은 안정성이 요구되는 시스템
@KafkaListener(topics = "critical-orders")
public void processCriticalOrder(OrderEvent order) {
// 각 레코드 처리 완료 시 자동으로 offset 커밋
validateOrder(order);
saveToDatabase(order);
sendNotification(order);
// 처리 완료 → 자동 커밋
}manual
개념
- acknowledge()가 여러 번 호출되어도, 실제 커밋은 다음 poll()에서 일괄로 처리된다.
acknowledge()호출 즉시가 아니라, 다음 poll() 시점에 batch로 커밋
MANUAL 모드에서의 커밋
acknowledge()가 호출되면 오프셋이 큐에 적재된다.- 리스너 스레드가 배치 처리를 끝내고 다음 poll()을 호출하기 직전에, 큐에 쌓인 모든 오프셋을 한꺼번에
commitSync()(또는commitAsync())로 브로커에 전송한다. - 현재 배치가 완전히 끝난 뒤에야 실제 커밋이 일어나며, 그 시점이
다음 poll()이다.
특징
// 1. acknowledge() 호출
acknowledgment.acknowledge(); // 큐에 저장됨
// 2. Spring Kafka 내부에서 처리
// - 리스너 컨테이너가 ack 요청을 내부 큐에 적재
// - 현재 배치의 모든 레코드가 처리될 때까지 대기
// 3. 다음 poll() 시점에 일괄 커밋
// - 큐에 쌓인 모든 ack들을 commitSync() 또는 commitAsync()로 처리manual_immediate
개념
- 애플리케이션 코드에서
Acknowledgment.acknowledge()메서드가 호출되는 즉시 오프셋을 커밋하는 모드 - 개발자가 메시지 처리의 성공/실패를 직접 제어할 수 있게 해주는 수동 커밋 방식
특징
acknowledge()메서드가 호출되면 바로 오프셋이 커밋된다.- 다음 poll()까지 기다리지 않고 현재 처리한 메시지의 오프셋까지 즉시 커밋된다.
- 리스터 쓰레드에서 커밋을 해야 한다.
- 커밋 작업은 반드시 Consumer 스레드(리스너 스레드)에서만 실행되어야 한다.
@KafkaListener(topics = "order-topic")
public void processOrder(OrderEvent order, Acknowledgment ack) {
// Case 1: 동기 처리 (리스너 스레드)
if (order.isSimple()) {
processSimpleOrder(order);
ack.acknowledge(); // ✅ 즉시 커밋
return;
}
// Case 2: 비동기 처리 (다른 스레드)
orderProcessingService.processAsync(order)
.thenRun(() -> {
ack.acknowledge(); // ❌ 큐에 저장, 나중에 커밋
});
}offset 처리 중 발생한 문제
MANUAL 모드에서 acknowledge() 호출하지 않아도 컨슈머가 계속 진행하는 현상
현상 설명
MANUAL 모드에서 acknowledge()를 호출하지 않아도 컨슈머가 계속해서 다음 메시지들을 처리하는 상황이 발생합니다.
원인 분석
Spring Kafka에서는 **메시지 읽기(polling)**와 offset commit이 완전히 별개의 과정으로 동작하기 때문입니다.
메시지 처리 위치 vs Commit된 Offset의 차이
1. 컨슈머의 현재 처리 위치 (Current Position)
- Kafka 컨슈머가 현재 읽어서 처리하고 있는 메시지의 offset
acknowledge()를 호출하지 않아도 계속 증가- 컨슈머는 poll()을 통해 계속 다음 배치의 메시지들을 가져옴
2. 브로커에 Commit된 Offset (Committed Offset)
- 실제로 Kafka 브로커에 저장된 “처리 완료” 지점
acknowledge()호출 시에만 업데이트됨- 서버 재시작 시 이 지점부터 다시 시작
동작 예시
토픽의 메시지: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
시나리오:
1. 메시지 1, 2, 3 처리 → acknowledge() 호출 → offset 3 커밋
2. 메시지 4, 5, 6 처리 → acknowledge() 호출 안함
3. 메시지 7, 8, 9 처리 → acknowledge() 호출 안함
결과:
- 컨슈머 현재 위치: offset 9까지 처리 완료
- 브로커 저장된 offset: 3 (마지막 커밋 지점)
- 서버 재시작 시: offset 4부터 다시 시작 (4,5,6,7,8,9 중복 처리)