TL;DR: Kafka를 처음 붙일 때 나는 Producer 옵션보다 Consumer의 manual ack가 더 먼저 눈에 들어왔고, 그 지점에서야 "메시지를 받는 것"보다 "언제 처리 완료로 간주할 것인가"가 더 중요한 질문일 수 있겠다고 생각하게 됐다.

Kafka를 공부하기 전에는 토픽을 만들고 Producer로 메시지를 보내는 쪽이 더 핵심이라고 생각했다. 그런데 이번에 실제 코드를 보면서 내 시선은 조금 다르게 움직였다. modules/kafka/.../KafkaConfig.kt에서 먼저 보인 것은 KafkaTemplate보ConcurrentKafkaListenerContainerFactory였고, 그 안에서도 ContainerProperties.AckMode.MANUAL 설정이 더 오래 남았다. 나는 이 지점이 단순한 설정값이 아니라, "우리는 메시지를 얼마나 조심스럽게 다루고 싶은가"에 대한 태도처럼 느껴졌다.
처음에는 "잘 받기만 하면 되는 것 아닌가"라고 생각했다
처음에는 Kafka를 붙인다는 말을 들으면 자연스럽게 이런 흐름을 떠올렸다.
Producer가 보낸다 -> Consumer가 받는다 -> 끝
그런데 실제 코드는 그렇게 단순하지 않았다. DemoKafkaConsumer는 메시지를 받은 직후 자동으로 끝내지 않고, Acknowledgment를 받아서 마지막에 직접 acknowledge()를 호출한다.
fun demoListener(
messages: List<ConsumerRecord<Any, Any>>, acknowledgment: Acknowledgment,) {
println(messages) acknowledgment.acknowledge()}
이 코드를 보면서 나는 "Kafka를 붙였다"는 사실보다 "언제 커밋할지를 애플리케이션이 결정하고 있다"는 사실이 더 중요하지 않을까 생각했다. 자동 커밋에 기대면 코드는 더 짧아질 수 있었을 텐데, 굳이 수동으로 넘긴 이유가 있다는 뜻처럼 보였기 때문이다.
내가 배운 것은 메시지 소비가 아니라 처리 완료의 기준이었다
KafkaConfig.kt를 보면 배치 리스너, polling 크기, heartbeat, session timeout 같은 값들이 함께 잡혀 있다. 처음에는 이 값들이 전부 튜닝 포인트처럼 보여서 조금 멀게 느껴졌다. 그런데 ackMode = MANUAL을 기준으로 다시 보니 그림이 조금 정리됐다.
내가 이해한 흐름은 대략 이렇다.
한 번에 여러 메시지를 읽는다
-> 애플리케이션이 직접 처리한다
-> 처리 성공이라고 판단한 뒤에만 ack 한다
나는 이 구조가 "Kafka는 비동기니까 빠르다"는 식의 설명보다 더 현실적이라고 느꼈다. 실제 서비스에서는 메시지를 읽었다는 사실보다, 그 메시지로 인한 부작용이 어디까지 반영됐는지가 더 중요하지 않을까 생각했기 때문이다. 예를 들어 집계 테이블을 갱신하거나, 별도 저장소에 처리 이력을 남기거나, 외부 시스템과 연결되는 순간에는 "받았음"과 "끝났음"이 같지 않을 수 있다.
그래서 나는 manual ack를 단순한 Kafka 문법이 아니라, 나중에 Outbox나 멱등 처리로 이어질 출발점처럼 보게 됐다. 아직 이 저장소에는 event_handled 같은 멱등 처리 저장소나 실제 product_metrics 집계가 들어와 있지 않다. 그래도 Consumer가 언제 ack할지를 스스로 결정하는 구조를 먼저 둔 것은, 앞으로 처리 성공의 기준을 더 엄격하게 가져가려는 방향과 닿아 있지 않을까 생각했다.
설정값보다 먼저 보였던 것은 실패를 다루는 태도였다
내가 이번에 가장 크게 배운 것은 Kafka 설정을 외우는 일이 아니었다. 오히려 다음 질문을 먼저 가져야 한다는 쪽에 가까웠다.
- 이 메시지는 언제 "처리 완료"라고 볼 수 있을까?
- 실패했다면 어디까지를 실패로 볼까?
- 다시 읽혀도 괜찮도록 만들려면 무엇이 더 필요할까?
예전에는 acks=all, idempotence=true, consumer group, partition key 같은 키워드를 먼저 익히면 Kafka를 이해할 수 있다고 생각했다. 지금은 그 키워드들이 결국 모두 "실패를 어떤 경계에서 감당할 것인가"라는 질문으로 다시 돌아오는 것처럼 느껴진다. manual ack는 그 질문을 코드 위로 끌어올리는 가장 작은 장치처럼 보였다.
아직 조금 조심스럽게 남겨두고 싶은 생각
물론 지금 구현만으로 Kafka 소비 전략을 다 이해했다고 말하기는 어렵다. 현재 코드는 DemoKafkaConsumer 수준이고, 실제 비즈니스 메시지를 처리하는 Consumer나 멱등성 저장소는 아직 없다. 그래서 나는 manual ack가 항상 정답이라고 쓰기보다, 적어도 "처리 완료의 기준을 코드에서 직접 붙잡아 보게 만드는 설정" 정도로 이해하고 있다.
아마 다음 단계에서는 이 감각이 더 선명해질 것 같다. 실제로 product_metrics를 갱신하거나, 선착순 쿠폰 발급처럼 중복 처리와 순서가 민감한 문제를 다루기 시작하면, 왜 자동 커밋보다 수동 ack 쪽이 더 마음에 걸렸는지 조금 더 분명해지지 않을까 싶다.
이번에 나는 Kafka를 "메시지 브로커를 붙이는 일"로 보기보다, "처리 완료를 어디서 선언할지 정하는 일"에 더 가깝게 보게 됐다. 적어도 지금의 나는, 그 출발점이 Consumer 코드의 마지막 줄에 있는 acknowledgment.acknowledge()였다고 생각한다.