2026. 5. 23.·base·
Kafka at-least-once-delivery로 인해 발생하는 멱등성 문제
kafka 트러블 슈팅
Kafka consumer는 메시지를 poll로 가져온 뒤, 처리가 끝났다고 판단되면 offset을 commit 한다.
여기서 중요한 점은 메시지를 읽었다고 해서 처리 완료가 아니라는 것이다.
Kafka의 기본 전달 방식은 at-least-once delivery로 메시지를 최소 한 번은 전달해주지만, 장애 상황에서는 같은 메시지가 두번 이상 전달될 수 있다.
예를 들어 알림 생성 이벤트를 처리한다고 하면
1. consumer가 Kafka에서 POST_PUBLISHED 이벤트를 poll
2. listener가 실행됨
3. DB에 알림 저장 성공
4. offset commit 전에 서버가 죽음
5. Kafka는 처리 완료를 확인하지 못함
6. 같은 offset의 메시지를 다시 전달
7. consumer가 같은 알림을 다시 insert 시도이때 DB에 알림 엔티티에 대한 유니크 제약조건이 있으면 중복 데이터 저장은 막을 수 있지만, 애플리케이션이 중복 insert를 예외로 처리하면 문제가 발생한다.
listener 안에서 예외가 밖으로 전파되면 Spring Kafka container는 그 레코드를 정상 처리 실패로 보기 때문에 해당 offset을 커밋하지 않고, 재시도하거나 DLT로 보내게 된다.
중복 메시지 재전달
-> duplicate key 예외 발생
-> listener 실패
-> offset commit 안 됨
-> 같은 메시지 또 재전달그 결과로 이렇게 이미 처리된 메시지가 계속 컨슈머로 재전송되고 계속 실패하면서 진행을 막을 수 있다.
DB저장과 Kafka offset commit이 서로 다른 시스템의 작업이라 하나의 트랜잭션으로 묶기 어렵다는 점에서 발생하는 문제라 이를 해결하기 위해선 같은 메시지가 여러 번 들어와도 결과가 한 번 처리한 것과 같도록 컨슈머 메서드를 멱등하게 만들어주어야한다.
위 케이스에선 유니크 제약조건과 upsert 방식을 같이 사용하면 문제를 해결할 수 잇다.
INSERT INTO notifications (
recipient_id,
source_event_id,
...
)
VALUES (...)
ON CONFLICT (recipient_id, source_event_id) DO NOTHING;이렇게 하면 같은 이벤트가 재전달되어도 예외 처리 없이 listener가 정상 종료되고 offset을 커밋할 수 있으므로 하나의 메시지를 무한 반복하는 문제가 발생하지 않는다.
이미 처리된 이벤트
-> insert 생략
-> 예외 없음
-> listener 정상 종료
-> offset commit 가능Related
Join the thread
Leave feedback, ask for clarification, or keep a focused discussion attached to this article.