Kafka 메세지 처리에 대한 고민
배경
이커머스 기반 주문 처리 시스템에서 Kafka를 도입하며, 대량 메시지 처리 환경에서도 안정적인 소비와 중복 방지, 장애 복구 구조를 어떻게 구현할지에 대한 실전 고민이 있었습니다.
Kafka는 at-least-once 전달을 보장하지만, 이는 곧 중복 소비의 가능성이 있다는 뜻입니다. 실제 장애 상황에서 메시지가 두 번 처리되며 중복 주문이 발생하는 문제를 경험하게 되었고, 이를 계기로 구조 개선을 시작하게 되었습니다.
문제 상황 1 - 중복 메세지 소비
Kafka 컨슈머가 메시지를 처리 중 예외가 발생하면, 해당 오프셋이 커밋되지 않아 같은 메시지를 다시 소비합니다.
단순히 보면 메시지를 놓치지 않기 위한 설계지만, 이로 인해 실제 주문이 두 번 처리되는 문제가 발생했습니다.
대응 1 – 수동 커밋 도입
Kafka의 자동 커밋 기능은 메시지 처리 완료 여부와 무관하게 오프셋을 주기적으로 저장하므로, 장애 발생 시 처리되지 않은 메시지가 유실될 수 있습니다.
이를 방지하고자 수동 커밋 방식으로 전환하여, 메시지 처리 완료 후에만 명시적으로 오프셋을 커밋하도록 했습니다.
문제 상황 2 – 수동 커밋의 복잡성과 운영 부담 증가
수동 커밋 전환은 중복 소비를 막는 데 효과가 있었지만,
- 메시지 실패 시 재처리 여부 판단
- 트랜잭션 경계 내에서 커밋 타이밍 조절
- 처리 성공 여부에 따라 커밋 분기
등 다양한 예외 처리를 직접 구현해야 했습니다.
결과적으로 처리 로직이 복잡해지고 운영 부담이 증가했습니다.
초기 접근 – 메시지 생성 시 락으로 중복 차단
이 복잡성을 줄이기 위해, 메시지 생성 시점에서 Redisson 분산 락을 고유 키 기준으로 설정해 중복 메시지 자체를 아예 생성하지 않도록 설계했습니다. 하지만 테스트 중 처리 실패 시 락이 해제되지 않아, 이후 동일 메시지가 아예 생성되지 않는 문제가 발견되었습니다.
대응 2 – 소비 시점 락으로 구조 전환
구조를 수정하여 Kafka 메시지를 먼저 발행한 후, 소비 시점에 Redisson 락을 고유 키 기준으로 설정하고, 락을 획득한 컨슈머만 메시지를 처리하도록 했습니다.
이 방식은 다음을 가능하게 했습니다:
- 메시지는 항상 Kafka에 들어감 → 복구 가능성 확보
- 중복 소비는 락으로 제어 → 정합성 유지
- 처리 실패 시에도 이후 재시도 가능 → 운영 유연성 확보
문제 상황 3 – 실패 메시지 복구 구조 부재
메시지 처리 중 예외가 발생했을 때, 실패 메시지를 별도로 추적하거나 재처리할 수 있는 흐름이 부재했습니다.
대응 3 – DLQ(Dead Letter Queue) 도입
Kafka의 DLQ 구조를 활용해 처리 실패 메시지를 별도 토픽으로 분리 저장하고, 후속 수동 재처리나 관리자 확인을 통해 서비스 신뢰성과 품질을 유지할 수 있도록 개선했습니다.
결과 및 교훈
이 구조 개선 과정을 통해 메시지 처리의 정합성과 안정성을 확보할 수 있었고,
Kafka의 메시지 보장 방식과 오프셋 커밋 구조, Redisson 락의 적용 위치에 대한 이해도 함께 얻을 수 있었습니다.