학습일지/Kafka

[Kafka] RetryableTopic, DLT

Merge Log 2025. 9. 23. 14:12

테스트를 위한 간단한 시나리오

  • 이메일 전송을 위한 서비스
  • 이메일 발송 토픽 : email.send
  • 이메일 정보 Producer
  • 이메일 발송 Consumer

외부 API 혹은 서버 장애 등 다양한 상황에서 작업이 실패할 수 있다

이 경우 실패에 대해서 재시도 처리를 해야하는 것이 일반적이다

참고로 토픽은 key-value 형태로 생성된다

이메일 전송 같은 경우 재시도 처리를 위해 아래와 같이 코드를 작성하였다

@Service
public class EmailSendConsumer {

    @KafkaListener(
            topics = "email.send",
            groupId = "email-send-group"
    )
    @RetryableTopic(
            attempts = "5",
            backoff = @Backoff(delay = 1000, multiplier = 2),
            dltTopicSuffix = ".dlt"
    )
    public void consume(String message) {
        System.out.println("Kafka로부터 받은 메시지: " + message);

        EmailSendMessage emailSendMessage = EmailSendMessage.fromJson(message);

        if (emailSendMessage.getTo().equals("fail@naver.com")) {
            System.out.println("잘못된 이메일 주소로 인한 발송 실패");
            throw new RuntimeException("잘못된 이메일 주소로 인한 발송 실패");
        }

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            throw new RuntimeException("이메일 발송 실패");
        }

        System.out.println("이메일 발송");
    }
}

그리고 실패 상황을 재현하기 위해 실패 조건을 추가하였다

재시도 정책은 실제 외부 API 의 지연시간 등을 고려하여 정책을 세워야 한다

현재는 테스트를 위해 5번의 재시도와 Exponential (지수적) 재시도 정책을 통해 재시도 로직을 처리하였다

Spring Kafka 에서 제공하는 재시도 처리

  • import org.springframework.kafka.annotation.RetryableTopic
  • Consumer 서버에서 재시도에 대해 아무런 코드를 작성하지 않더라도 자동으로 재시도를 처리한다 → 기본값으로 재시도(retry) 전략이 설정되어 있다
  • interval : 재시도를 하는 시간 간격 (ms)
    • interval=0 일 경우 실패하자마자 즉시 재시도 처리
  • maxAttempts : 최대 재시도 횟수
    • maxAttempts=9 재시도를 9번까지 시도
  • currentAttemps : 지금까지 시도한 횟수 (최소 시도 횟수 + 재시도 횟수)
    • currentAttemps=10 최소 시도를 1번 하고 재시도를 9번 시도

재시도를 여러번 했음에도 불구하고 처리에 실패하는 메시지는 어떻게 해야할까?

DLT (Dead Letter Topic)

  • 오류로 인해 처리할 수 없는 메시지를 임시로 저장하는 토픽
  • Kafka 에서는 재시도까지 실패한 메시지를 다른 토픽에 따로 저장해서 유실을 방지하고 후속 조치를 가능하게 만든다

DLT 를 사용함으로 얻는 이점

  • 실패한 메시지를 DLT 에 따로 저장하므로 실패한 메시지를 유실하지 않는다
  • 사후에 실패 원인 분석에 사용할 수 있다
  • 처리되지 못한 메시지 또한 수동으로 처리해줄 수 있다

사실 Spring Kafka 는 @RetryableTopic 을 사용하면 자동으로 DLT 토픽을 생성하고 메시지를 전송해준다.

  • 기본적으로 만드는 DLT 토픽 이름은 {기존 토픽명}-dlt 형태로 지어진다
  • 일관된 토픽이름을 위해 dlt 토픽 이름을 변경해줄 수 있다
@RetryableTopic(
            attempts = "5",
            backoff = @Backoff(delay = 1000, multiplier = 2),
            dltTopicSuffix = ".dlt"
    )

DLT 에 저장된 메시지 사후 처리 방식

  • DLT 에 저장된 메시지를 로그 시스템에 전송하여 장애 원인을 추적할 수 있도록 한다
  • DLT 에 메시지가 인식될 수 있도록 알림 발송을 한다
  • 관리자는 알림을 확인 후 로그 시스템의 로그를 확인하여 사후 처리를 한다

수동 처리 방식 예시들

  • 메시지를 원래 토픽으로 직접 보내기
    • 장애가 일시적이고 지금은 해결된 경우 (서버 장애)
  • 메시지 폐기
    • 영구적으로 처리할 수 없는 메시지 (탈퇴한 사용자, 형식 오류)
    • 단, 영구적으로 폐기하지 않고 혹시 모를 상황에 대비해 로그로 남겨둔다
  • 잘못된 메시지 내용이 Kafka 에 들어가지 않게 Product 의 검증 로직 보완
    • 원인 분석 후 검증 로직 보완

사후 처리를 위한 dlt 토픽을 소비하는 서비스를 작성한다

@Service
public class EmailSendDltConsumer {

    @KafkaListener(
            topics = "email.send.dlt",
            groupId = "email-send-dlt-group"
    )
    public void consume(String message) {
        // 1. 로그 시스템에 전송
        System.out.println("로그 시스템에 전송 : " + message);

        // 2. 알림 발송
        System.out.println("Slack 알림 발송 : " + message);
    }
}
  • email.send.dlt 라는 토픽을 소비
  • 첫번째로 로그 시스템에 전송
  • 두번째로 슬랙 알림 전송

테스트 코드