
RabbitMQ는 분산 시스템에서 비동기 메시징을 통해 서비스 간 결합도를 낮추고 성능과 확장성을 보장하는 데 탁월한 메시지 브로커다. 하지만 메시지를 소비하는 도중 예외가 발생할 수 있고 이런 상황에서의 적절한 대응은 매우 중요하다고 생각해서 여러 방안을 고민해보았다. 과연 단순히 메시지를 다시 큐에 넣는 것만으로 충분할까?
이 글에서는 RabbitMQ에서 메시지 처리 중 예외가 발생했을 때 어떻게 안전하게 재처리하고 무한 루프나 장애로 이어지지 않도록 하는 구조를 실습과 함께 정리해 보았다.
RabbitMQ에서 메시지를 소비한 후 클라이언트는 브로커에게 메시지를 정상적으로 처리했는지를 알려야 한다.
basicAck: 메시지를 정상적으로 처리했음을 브로커에 알리고 큐에서 제거한다.basicNack: 메시지 처리 실패를 알린다. requeue=true 설정 시 다시 큐에 들어간다.basicReject: 단일 메시지를 거절한다. requeue에 따라 처리 경로가 달라진다.channel.basicAck(deliveryTag, false); // 정상 처리
channel.basicNack(deliveryTag, false, true); // 실패 후 재큐잉
channel.basicReject(deliveryTag, false); // 실패 후 제거
가장 직관적인 방식은 requeue=true 옵션을 사용하여 메시지를 다시 원래 큐에 넣는 것이다. 그러나 메시지를 처리할 수 없는 상태(예: 잘못된 데이터, 외부 시스템 오류)가 지속된다면 해당 메시지는 무한히 재처리되며 장애를 유발할 수 있다.
[ 큐 ] → [ 소비자 ] → (예외 발생) → [ requeue ] → 다시 [ 큐 ]
↑ ↓
무한 반복
메시지 처리에 실패한 경우, 별도의 재시도 큐에 잠시 보관했다가 다시 원래 큐로 되돌린다. 이때 TTL(Time-To-Live)을 이용하여 메시지를 잠시 지연시킨 후 다시 처리한다.
재시도 횟수를 초과하거나 처리할 수 없는 메시지를 Dead Letter Queue(DLQ)로 분리하여 후속 처리를 할 수 있다.
[ original.queue ]
|
(fail)
↓
[ retry.queue (TTL) ]
|
(TTL 만료)
↓
[ original.queue ]
|
(재시도 n회 초과)
↓
[ DLQ ]
이 구조를 통해 다음과 같은 이점을 얻을 수 있다.
그림으로 표현하면 아래와 같다.

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);
}
private void sendToDLQ(Message message) {
rabbitTemplate.send("dlq.exchange", "dlq.routing.key", message);
}
RabbitMQ에서의 메시지 재처리는 단순히 requeue 하는 방식보다는 실패 상황에 따라 적절한 재처리 및 격리 전략을 마련하는 것이 중요하다고 생각한다. Retry Queue, TTL, DLQ를 조합하면 예외 상황에서도 안정적인 서비스 운영이 가능하다.
Spring AMQP를 활용하면 이러한 전략을 코드로 명확하게 구현할 수 있으며 운영 상황에 따라 retry count, delay 시간 등을 유연하게 조절할 수 있다.