이번 내용은 부하 테스트를 하면서 발견한 내용은 아니다. 실무에서 많이 문제가 된다는데, Kafka의 at-least-once 특성 상 발생할 수 있는 문제이다.
Kafka의 메시지 전달 보장 수준
- at-most-once: 메시지를 한 번 이하로 보낸다. (유실 가능)
- at-least-once: 메시지를 한 번 이상 보낸다. (중복 가능)
- exactly-once: 메시지를 정확히 한 번 보낸다. (복잡하고 비용이 크다.)
기본 설정은 대부분 at-least-once이고 메시지가 중복될 수 있지만 유실되진 않는다.
만약 Kafka 메시지를 수신한 후 처리 과정이 아래와 같다고 가정해보자.
- Redis 저장
- MongoDB 백업
- WebSocket 브로드캐스트
- Kafka offset 커밋
만약 MongoDB 백업 중 예외가 발생하면 Kafka는 offset을 커밋하지 않게 된다. 그러면 같은 메시지를 다시 전송하게 되는데 여기서 문제가 발생한다.
- 이미 Redis에 저장이 된 상태에서 메시지가 다시 처리된다.
- 같은 내용이 Redis에 또 저장될 수 있다.
- 사용자 입장에서 중복된 내용을 보게 될 수 있다.
이를 위해 중복을 방지하는 로직을 짜야한다.
중복 방지
내 채팅 시스템을 기준으로는 Redis에 저장하고 브로드캐스트를 하게 되어 Redis에 저장하는 부분에 중복 방지 로직이 필요하다.
1
2
3
4
5
6
7
String redisDedupKey = "chat:dedup:" + message.getMessageId();
Boolean isNew = redisTemplate.opsForValue().setIfAbsent(redisDedupKey, "1", Duration.ofMinutes(10));
if (!Boolean.TRUE.equals(isNew)) {
log.warn("중복 메시지 무시: {}", message.getMessageId());
return;
}
- 구현은 간단하다.
- 메시지 Id를 기준으로 이미 메시지가 처리되었는지에 대한 여부를 Redis에 따로 저장해둔다.
- TTL을 걸어 메모리 효율성도 챙긴다.
- 재시도를 해도 이미 처리된 메시지는 걸러지기 때문에 안정적으로 해결이 가능하다.
정리
Kafka의 at-least-once 보장은 메시지 유실은 막아주지만, 그에 따른 중복 처리 문제는 애플리케이션 레벨에서 반드시 대응해야 한다.
내가 테스트를 해보며 찾은 것은 아니지만, 이러한 부분은 실제 운영 환경에서 사용자 경험에 좋지 않기 때문에 사전에 구조적으로 방어하는 설계가 중요하다고 느꼈다.