메시지 신뢰성 보장을 위한 확인 메커니즘(ACK, NACK)
소비자가 큐에서 메시지를 가져간 직후에 죽으면, 아직 처리하지 못한 그 메시지는 어떻게 되는 걸까?
ACK와 NACK — 메시지 신뢰성 보장의 핵심
왜 메시지 확인이 필요한가
RabbitMQ에서 소비자가 큐로부터 메시지를 가져왔다고 해서 처리가 완료된 것은 아닙니다. 메시지를 가져온 직후 소비자에 장애가 발생하면, 아직 처리되지 않은 메시지가 사라질 수 있습니다.
이러한 메시지 손실을 방지 하기 위해 RabbitMQ는 ACK/NACK 메커니즘을 제공합니다. 소비자가 메시지 처리 결과를 브로커에 명시적으로 알려줌으로써, 브로커는 메시지를 안전하게 삭제할지 또는 재전달할지를 판단할 수 있습니다.
**핵심 원리 **: 브로커는 소비자의 확인 신호를 받기 전까지 메시지를 큐에서 삭제하지 않습니다. 이를 통해 장애 상황에서도 메시지가 유실되지 않도록 보장합니다.
ACK와 NACK란
| 신호 | 의미 | 용도 |
|---|---|---|
| ACK (Acknowledgement) | 정상 처리 확인 | 소비자가 메시지를 성공적으로 처리했음을 브로커에 알림 |
| NACK (Negative Acknowledgement) | 처리 실패 알림 | 메시지 처리에 실패했음을 브로커에 알리고, 재전달 또는 폐기를 요청 |
- ACK 를 수신한 브로커는 해당 메시지를 큐에서 안전하게 삭제합니다.
- NACK 를 수신한 브로커는 설정에 따라 메시지를 큐에 재적재(
requeue=true)하거나 폐기(requeue=false)합니다.
Acknowledgement Mode 비교
RabbitMQ에서는 소비자가 메시지 처리 결과를 브로커에 알리는 방식을 두 가지로 나눕니다.
| 비교 항목 | Auto-ack | Manual-ack |
|---|---|---|
| ACK 전송 시점 | 메시지 수신 즉시 자동 전송 | 비즈니스 로직 처리 후 명시적 전송 |
| ** 신뢰성** | 낮음 (처리 중 오류 시 메시지 손실 가능) | 높음 (처리 완료 후에만 삭제) |
| ** 성능** | 높음 (별도 확인 작업 없음) | 상대적으로 낮음 (확인 신호 전송 오버헤드) |
| ** 적합한 시나리오** | 로그 수집 등 손실 허용 데이터 | 결제, 주문 등 손실 불가 데이터 |
| ** 장애 시 동작** | 메시지 유실 가능 | 메시지 재전달 가능 |
Auto-ack 모드
메시지를 수신하는 즉시 자동으로 ACK가 전송됩니다. 별도의 확인 작업이 없어 처리 속도는 빠르지만, ** 비즈니스 로직 처리 중 오류가 발생해도 브로커는 이미 메시지를 처리 완료로 간주 **합니다.
Auto-ack는 로그 메시지, 통계 데이터 등 ** 재처리가 필요 없는 비중요 데이터 **에 적합합니다.
Manual-ack 모드
소비자가 비즈니스 로직을 성공적으로 처리한 후에만 명시적으로 ACK를 전송합니다. ** 메시지 처리가 확실히 끝난 뒤에만 큐에서 삭제 **되므로 신뢰성이 높고, 장애 발생 시 메시지를 재처리하여 데이터 손실을 방지할 수 있습니다.
Manual-ack는 결제, 주문 등 ** 데이터 손실이 허용되지 않는 비즈니스 **에 적합합니다.
코드 예시: Manual-ack 모드
import com.rabbitmq.client.*;
public class AckNackExample {
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
try (Connection connection = factory.newConnection();
Channel channel = connection.createChannel()) {
// Manual-ack 모드 설정: autoAck = false
channel.basicConsume("queueName", false, (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
long deliveryTag = delivery.getEnvelope().getDeliveryTag();
boolean isProcessed = doSomething(message);
if (isProcessed) {
// 처리 성공: ACK 전송 → 브로커가 메시지를 큐에서 삭제
channel.basicAck(deliveryTag, false);
} else {
// 처리 실패: NACK 전송 → requeue=true로 큐에 재적재
channel.basicNack(deliveryTag, false, true);
}
}, consumerTag -> {});
}
}
}
위 코드의 핵심은 basicConsume의 두 번째 파라미터 autoAck를 false로 설정하여 Manual-ack 모드를 활성화하는 것입니다. 이후 처리 결과에 따라 basicAck 또는 basicNack를 명시적으로 호출합니다.
basicAck / basicNack 파라미터 상세
basicAck
channel.basicAck(long deliveryTag, boolean multiple);
| 파라미터 | 타입 | 설명 |
|---|---|---|
deliveryTag | long | 메시지의 고유 식별자. 브로커가 어떤 메시지에 대한 ACK인지 식별 |
multiple | boolean | true: 해당 deliveryTag 이하의 모든 미확인 메시지 를 일괄 ACK. false: 해당 메시지만 ACK |
basicNack
channel.basicNack(long deliveryTag, boolean multiple, boolean requeue);
| 파라미터 | 타입 | 설명 |
|---|---|---|
deliveryTag | long | 메시지의 고유 식별자 |
multiple | boolean | true: 해당 deliveryTag 이하의 모든 미확인 메시지를 일괄 NACK. false: 해당 메시지만 NACK |
requeue | boolean | true: 메시지를 큐에 재적재하여 다른 소비자가 처리. false: 메시지를 폐기 (DLX가 설정되어 있으면 DLQ로 이동) |
**주의 **:
requeue=true로 설정하면 동일한 메시지가 무한 반복 처리될 수 있습니다. 이를 방지하려면 재시도 횟수를 제한하거나,requeue=false와 함께 DLX(Dead Letter Exchange)를 활용하는 것이 좋습니다.
요약
| 핵심 개념 | 설명 |
|---|---|
| ACK | 메시지 정상 처리 확인 → 브로커가 큐에서 삭제 |
| NACK | 메시지 처리 실패 알림 → 재적재 또는 폐기 |
| Auto-ack | 수신 즉시 자동 확인 (빠르지만 손실 위험) |
| Manual-ack | 처리 완료 후 명시적 확인 (신뢰성 높음) |
| multiple | 여러 메시지 일괄 확인으로 네트워크 효율 향상 |
| requeue | 실패 메시지 재처리 여부 결정 |
주의할 점
requeue=true의 무한 루프 함정
basicNack에서 requeue=true로 설정하면 실패한 메시지가 큐에 다시 들어갑니다. 같은 소비자가 다시 꺼내서 또 실패하면? 무한 반복입니다. 반드시 재시도 횟수를 제한하거나, requeue=false와 DLX를 조합해서 실패 메시지를 별도 큐로 보내야 합니다.
ACK를 보내지 않으면 메모리가 터진다
Manual-ack 모드에서 ACK도 NACK도 보내지 않으면, 브로커는 해당 메시지를 "처리 중"으로 간주하고 큐에서 삭제하지 않습니다. unacked 메시지가 쌓이면 브로커 메모리가 고갈됩니다. prefetch count를 설정해서 한 번에 처리할 메시지 수를 제한하는 것이 필수입니다.
Prefetch Count — 소비자 과부하 방지
Prefetch Count(basicQos)는 ** 소비자가 ACK를 보내기 전까지 브로커가 전달할 수 있는 최대 미확인 메시지 수 **를 제한하는 설정입니다.
// 한 번에 최대 10개의 메시지만 전달받도록 제한
channel.basicQos(10);
| Prefetch Count | 동작 | 적합한 상황 |
|---|---|---|
0 (기본값) | 제한 없음 — 브로커가 가능한 모든 메시지를 한꺼번에 전달 | 거의 사용하지 않음 (메모리 위험) |
1 | 메시지 하나씩 전달 — ACK 후 다음 메시지 수신 | 처리 시간이 긴 작업, 공정한 분배 |
10~50 | 적당한 배치 단위로 전달 | 일반적인 실무 환경 |
Prefetch Count를 설정하지 않으면 브로커가 큐의 메시지를 한꺼번에 소비자에게 밀어넣어, 소비자 메모리가 고갈되거나 다른 소비자가 놀게 됩니다. Manual-ack 모드에서는 ** 반드시
basicQos를 함께 설정 **해야 합니다.
신뢰성이 중요한 시스템에서는 Manual-ack 모드를 기본으로 사용 하고, 처리 실패 시 requeue와 DLX를 조합하여 메시지 유실 없는 안정적인 처리 파이프라인을 구축하는 것을 권장합니다.
**공식 문서 참고 **: Consumer Acknowledgements and Publisher Confirms