RabbitMQ 메시지 재처리, 제대로 이해하기

Lord·2025년 4월 10일

Problem Solving Skills

목록 보기
11/17
post-thumbnail

RabbitMQ는 분산 시스템에서 비동기 메시징을 통해 서비스 간 결합도를 낮추고 성능과 확장성을 보장하는 데 탁월한 메시지 브로커다. 하지만 메시지를 소비하는 도중 예외가 발생할 수 있고 이런 상황에서의 적절한 대응은 매우 중요하다고 생각해서 여러 방안을 고민해보았다. 과연 단순히 메시지를 다시 큐에 넣는 것만으로 충분할까?

이 글에서는 RabbitMQ에서 메시지 처리 중 예외가 발생했을 때 어떻게 안전하게 재처리하고 무한 루프나 장애로 이어지지 않도록 하는 구조를 실습과 함께 정리해 보았다.


기본 개념 정리

ack / nack / reject

RabbitMQ에서 메시지를 소비한 후 클라이언트는 브로커에게 메시지를 정상적으로 처리했는지를 알려야 한다.

  • basicAck: 메시지를 정상적으로 처리했음을 브로커에 알리고 큐에서 제거한다.
  • basicNack: 메시지 처리 실패를 알린다. requeue=true 설정 시 다시 큐에 들어간다.
  • basicReject: 단일 메시지를 거절한다. requeue에 따라 처리 경로가 달라진다.
channel.basicAck(deliveryTag, false); // 정상 처리
channel.basicNack(deliveryTag, false, true); // 실패 후 재큐잉
channel.basicReject(deliveryTag, false); // 실패 후 제거

단순 재처리의 문제: 무한 루프

가장 직관적인 방식은 requeue=true 옵션을 사용하여 메시지를 다시 원래 큐에 넣는 것이다. 그러나 메시지를 처리할 수 없는 상태(예: 잘못된 데이터, 외부 시스템 오류)가 지속된다면 해당 메시지는 무한히 재처리되며 장애를 유발할 수 있다.

메시지 무한 루프 흐름도

[ 큐 ] → [ 소비자 ] → (예외 발생) → [ requeue ] → 다시 [ 큐 ]
                             ↑                     ↓
                           무한 반복

안정적인 재처리 전략

Retry Queue + Delay Queue + TTL

메시지 처리에 실패한 경우, 별도의 재시도 큐에 잠시 보관했다가 다시 원래 큐로 되돌린다. 이때 TTL(Time-To-Live)을 이용하여 메시지를 잠시 지연시킨 후 다시 처리한다.

DLQ(Dead Letter Queue)

재시도 횟수를 초과하거나 처리할 수 없는 메시지를 Dead Letter Queue(DLQ)로 분리하여 후속 처리를 할 수 있다.


전체 아키텍처 구성도

[ original.queue ]
       |
     (fail)
       ↓
[ retry.queue (TTL) ]
       |
     (TTL 만료)
       ↓
[ original.queue ]
       |
     (재시도 n회 초과)
       ↓
     [ DLQ ]

이 구조를 통해 다음과 같은 이점을 얻을 수 있다.

  • 일시적인 문제 발생 시 자동 복구 가능
  • 영구 실패 메시지는 DLQ로 분리해 장애 확산 방지
  • 재시도 사이에 시간 간격을 두어 시스템 보호

그림으로 표현하면 아래와 같다.

Spring AMQP로 구현하기

application.yml 설정 예시

spring:
  rabbitmq:
    listener:
      simple:
        default-requeue-rejected: false
        acknowledge-mode: manual

메시지 소비 코드

@RabbitListener(queues = "my.queue")
public void handleMessage(Message message, Channel channel) throws IOException {
    long tag = message.getMessageProperties().getDeliveryTag();
    try {
        // 비즈니스 로직 수행
        channel.basicAck(tag, false);
    } catch (Exception e) {
        int retryCount = (int) message.getMessageProperties().getHeaders()
                                      .getOrDefault("x-retry-count", 0);

        if (retryCount >= 3) {
            // DLQ로 전송
            sendToDLQ(message);
            channel.basicAck(tag, false);
        } else {
            // retry-count 증가 후 재큐잉
            resendToRetryQueue(message, retryCount + 1);
            channel.basicAck(tag, false);
        }
    }
}

메시지 재큐잉 함수

private void resendToRetryQueue(Message message, int retryCount) {
    MessageProperties props = message.getMessageProperties();
    props.setHeader("x-retry-count", retryCount);
    rabbitTemplate.send("retry.exchange", "retry.routing.key", message);
}

DLQ 전송 함수

private void sendToDLQ(Message message) {
    rabbitTemplate.send("dlq.exchange", "dlq.routing.key", message);
}

RabbitMQ에서의 메시지 재처리는 단순히 requeue 하는 방식보다는 실패 상황에 따라 적절한 재처리 및 격리 전략을 마련하는 것이 중요하다고 생각한다. Retry Queue, TTL, DLQ를 조합하면 예외 상황에서도 안정적인 서비스 운영이 가능하다.

Spring AMQP를 활용하면 이러한 전략을 코드로 명확하게 구현할 수 있으며 운영 상황에 따라 retry count, delay 시간 등을 유연하게 조절할 수 있다.

profile
다재다능한 Backend 개발자에 도전하는 개발자

0개의 댓글