Blog
Anki
RabbitMQ

RabbitMQ는 Exactly Once 전달을 보장하나요?

꼬리 질문
  • RabbitMQ가 제공하는 전달 보장 수준은 무엇인가요?
  • Exactly Once를 구현하기 위한 방법들을 설명해주세요.

답변 보기

RabbitMQ는 기본적으로 Exactly Once 전달을 보장하지 않습니다.

  • 기본 보장 수준: At Least Once (최소 한 번) 전달 보장 📦
  • 분산 시스템의 한계: 네트워크 장애, 브로커 재시작, 컨슈머 크래시로 인한 메시지 중복이나 손실 가능
  • 트랜잭션 기능의 제약:
    • 단일 큐에서만 원자성 보장
    • 여러 큐 관련 시 원자성 보장 불가능
    • 브로커 장애 시 트랜잭션 효과가 일부만 적용될 수 있음

꼬리질문: RabbitMQ가 제공하는 전달 보장 수준은 무엇인가요?

  • At Most Once: 메시지가 최대 한 번만 전달 (손실 가능) ⚠️
  • At Least Once: 메시지가 최소 한 번은 전달 (중복 가능) ✅
  • Exactly Once: 제한적 시나리오에서만 가능 ❌

꼬리질문: Exactly Once를 구현하기 위한 방법들을 설명해주세요.

  • 멱등성(Idempotency) 구현:
    // 주문 ID로 중복 체크
    if (!orderRepository.exists(orderId)) {
        processOrder(order);
        channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
    }
  • 중복 제거(Deduplication) 테이블:
    String messageId = delivery.getProperties().getMessageId();
    if (!processedMessageRepository.exists(messageId)) {
        processOrder(order);
        processedMessageRepository.save(messageId);
    }
  • 트랜잭션 활용:
    @Transactional
    public void handleMessage(Delivery delivery) {
        orderService.save(order);
        channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
    }

출처: RabbitMQ 공식 문서 (opens in a new tab), 분산 시스템 Exactly Once 불가능성 (opens in a new tab)

같은 큐에 여러 컨슈머가 연결되어 있을 때 메시지 전달 방식을 설명해주세요

꼬리 질문
  • Direct Exchange에서 같은 routing key로 바인딩된 큐는 어떻게 동작하나요?
  • 여러 컨슈머가 같은 메시지를 받으려면 어떻게 해야 하나요?

답변 보기

같은 큐에 여러 컨슈머가 연결되면 Round-Robin 방식으로 메시지가 분배됩니다.

  • Round-Robin 분배: 메시지가 컨슈머들에게 순차적으로 분배 🔄
  • 하나의 메시지 = 하나의 컨슈머: 각 메시지는 오직 하나의 컨슈머에게만 전달
  • 로드 밸런싱 효과: 여러 컨슈머가 작업을 분담하여 처리 성능 향상
// 같은 큐에 여러 컨슈머 연결 예시
String queueName = "task_queue";
channel.queueDeclare(queueName, true, false, false, null);
 
// 컨슈머 1
channel.basicConsume(queueName, false, deliverCallback1, consumerTag -> {});
// 컨슈머 2  
channel.basicConsume(queueName, false, deliverCallback2, consumerTag -> {});
 
// 메시지 전달 순서:
// 메시지1 → 컨슈머1
// 메시지2 → 컨슈머2
// 메시지3 → 컨슈머1

꼬리질문: Direct Exchange에서 같은 routing key로 바인딩된 큐는 어떻게 동작하나요?

  • Exchange 레벨: 같은 routing key로 바인딩된 모든 큐에 메시지 복사 📤
  • 큐 레벨: 각 큐 내에서는 컨슈머들이 Round-Robin으로 분배
// 같은 routing key, 다른 큐에 바인딩
channel.queueBind("queue1", "direct_exchange", "orders");
channel.queueBind("queue2", "direct_exchange", "orders");
// 결과: 메시지가 queue1과 queue2 모두에 복사됨

꼬리질문: 여러 컨슈머가 같은 메시지를 받으려면 어떻게 해야 하나요?

  • 방법 1: 각 컨슈머마다 별도 큐 생성
    channel.queueDeclare("email_queue", true, false, false, null);
    channel.queueDeclare("sms_queue", true, false, false, null);
    channel.queueBind("email_queue", "user_events", "signup");
    channel.queueBind("sms_queue", "user_events", "signup");
  • 방법 2: Fanout Exchange 사용 (routing key 무시)
    channel.exchangeDeclare("broadcast", "fanout");
  • 방법 3: 자동 생성 임시 큐
    String queueName = channel.queueDeclare().getQueue();
    channel.queueBind(queueName, "events", "");

출처: RabbitMQ Consumers 가이드 (opens in a new tab), RabbitMQ Queues 가이드 (opens in a new tab)

Java에서 RabbitMQ 큐를 구분하는 방법을 설명해주세요

꼬리 질문
  • 같은 이름의 큐를 다른 속성으로 선언하면 어떻게 되나요?
  • 서버 생성 큐는 언제 사용하나요?

답변 보기

Java에서는 큐 이름(queue name)을 통해 같은 큐인지 다른 큐인지 구분합니다.

  • 큐 이름이 식별자: queueDeclare() 메서드의 첫 번째 매개변수가 고유 식별자
  • 동일성 판별: 큐 이름 + 모든 속성이 동일해야 같은 큐로 인식
  • 속성 불일치 시: PRECONDITION_FAILED 예외 발생
// 같은 큐 사용 (여러 컨슈머가 Round-Robin 분배)
channel.queueDeclare("order_queue", true, false, false, null);
 
// 다른 큐 사용 (각각 독립적)
channel.queueDeclare("email_queue", true, false, false, null);
channel.queueDeclare("sms_queue", true, false, false, null);
 
// 서버 자동 생성 큐 (고유 이름 자동 생성)
String randomQueueName = channel.queueDeclare().getQueue();
// 예: amq.gen-JzTY20BRgKO-HjmUJj0wLg

꼬리질문: 같은 이름의 큐를 다른 속성으로 선언하면 어떻게 되나요?

  • 에러 발생: PRECONDITION_FAILED (코드 406) 채널 레벨 예외
  • 기존 큐 유지: 이미 존재하는 큐의 속성은 변경되지 않음
  • 해결 방법: 다른 이름의 큐를 사용하거나 기존 큐 삭제 후 재생성
// 첫 번째 선언
channel.queueDeclare("test_queue", true, false, false, null);
 
// 두 번째 선언 (다른 durable 값) - 에러 발생!
channel.queueDeclare("test_queue", false, false, false, null);
// IOException: PRECONDITION_FAILED

꼬리질문: 서버 생성 큐는 언제 사용하나요?

  • 임시 큐가 필요한 경우: 일회성 작업이나 임시 통신
  • 독점적 사용: 특정 컨슈머만 사용하는 큐
  • 자동 정리: 연결 종료 시 자동 삭제되는 큐
// 서버 생성 큐 활용 예시
String tempQueue = channel.queueDeclare().getQueue();
channel.queueBind(tempQueue, "logs", "");
 
// RPC 패턴에서 응답 큐로 활용
String replyQueue = channel.queueDeclare().getQueue();
AMQP.BasicProperties props = new AMQP.BasicProperties.Builder()
    .replyTo(replyQueue)
    .build();

출처: RabbitMQ Java API 가이드 (opens in a new tab), RabbitMQ Queues 가이드 (opens in a new tab)

RabbitMQ에서 같은 큐와 Exchange를 조합하는 패턴들을 설명해주세요

꼬리 질문
  • 로그 시스템에서 여러 심각도 레벨을 하나의 큐로 받는 방법은?
  • 브로드캐스팅 패턴은 어떻게 구현하나요?

답변 보기

RabbitMQ에서는 큐와 Exchange를 다양하게 조합하여 유연한 메시징 패턴을 구현할 수 있습니다.

  • 패턴 1: 같은 큐 + 여러 Routing Key (필터링) 🔍
  • 패턴 2: 같은 큐 + 여러 Exchange (멀티소스 수집) 📥
  • 패턴 3: 같은 Exchange + 여러 큐 (브로드캐스팅) 📡
  • 패턴 4: 복합 바인딩 (멀티 라우팅) 🔀

꼬리질문: 로그 시스템에서 여러 심각도 레벨을 하나의 큐로 받는 방법은?

  • 같은 큐를 여러 routing key로 바인딩:
// 로그 필터링 패턴
channel.exchangeDeclare("log_exchange", "direct");
String logQueue = "application_logs";
channel.queueDeclare(logQueue, true, false, false, null);
 
// 같은 큐를 여러 routing key로 바인딩
channel.queueBind(logQueue, "log_exchange", "error");
channel.queueBind(logQueue, "log_exchange", "warning");
channel.queueBind(logQueue, "log_exchange", "info");
 
// 하나의 컨슈머가 모든 로그 레벨 처리
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
    String message = new String(delivery.getBody(), "UTF-8");
    String severity = delivery.getEnvelope().getRoutingKey();
    System.out.println("로그 [" + severity + "]: " + message);
};

멀티소스 수집 패턴:

// 주문 처리 큐가 웹/모바일/API 주문을 모두 받기
channel.exchangeDeclare("web_orders", "direct");
channel.exchangeDeclare("mobile_orders", "direct");
channel.exchangeDeclare("api_orders", "direct");
 
String orderQueue = "order_processing";
channel.queueDeclare(orderQueue, true, false, false, null);
 
// 같은 큐를 여러 Exchange에 바인딩
channel.queueBind(orderQueue, "web_orders", "new_order");
channel.queueBind(orderQueue, "mobile_orders", "new_order");
channel.queueBind(orderQueue, "api_orders", "new_order");

꼬리질문: 브로드캐스팅 패턴은 어떻게 구현하나요?

  • 같은 Exchange에 여러 큐 바인딩:
// 사용자 이벤트를 여러 서비스가 각각 처리
channel.exchangeDeclare("user_events", "direct");
 
// 여러 서비스별 큐 선언
channel.queueDeclare("email_service_queue", true, false, false, null);
channel.queueDeclare("analytics_queue", true, false, false, null);
channel.queueDeclare("notification_queue", true, false, false, null);
 
// 같은 Exchange에 여러 큐 바인딩
String routingKey = "user_signup";
channel.queueBind("email_service_queue", "user_events", routingKey);
channel.queueBind("analytics_queue", "user_events", routingKey);
channel.queueBind("notification_queue", "user_events", routingKey);
 
// 메시지 발행 시 모든 큐로 전달
channel.basicPublish("user_events", routingKey, null, 
    "새 사용자 가입: user123".getBytes());

Spring Boot 구현 예시:

@Configuration
public class RabbitConfig {
    @Bean
    public Declarables orderBindings(DirectExchange exchange, Queue queue) {
        return new Declarables(
            BindingBuilder.bind(queue).to(exchange).with("web.order"),
            BindingBuilder.bind(queue).to(exchange).with("mobile.order"),
            BindingBuilder.bind(queue).to(exchange).with("api.order")
        );
    }
}

출처: RabbitMQ Routing 튜토리얼 (opens in a new tab), CloudAMQP Exchange 가이드 (opens in a new tab)

Round-Robin 분배와 Exactly Once 처리의 차이점을 설명해주세요

꼬리 질문
  • 컨슈머 장애 시 메시지는 어떻게 처리되나요?
  • ACK 타이밍 문제는 무엇인가요?

답변 보기

Round-Robin 분배와 Exactly Once는 완전히 다른 개념입니다. Round-Robin만으로는 Exactly Once를 보장할 수 없습니다.

  • Round-Robin: 여러 컨슈머 간 메시지 분배 방식 (Load Balancing) ⚖️
  • Exactly Once: 메시지가 정확히 한 번만 처리되는 보장 (Processing Guarantee) 🎯
  • 핵심 차이: 분배 ≠ 처리 보장
// Round-Robin 동작 예시
// 3개 메시지, 3개 컨슈머
// 메시지1 → 컨슈머A ✅
// 메시지2 → 컨슈머B ✅  
// 메시지3 → 컨슈머C ✅
// 하지만 처리 중 장애 시 중복 처리 가능!

꼬리질문: 컨슈머 장애 시 메시지는 어떻게 처리되나요?

  • 재전송 메커니즘: ACK를 받지 못한 메시지는 다른 컨슈머에게 재전송
// 컨슈머 장애 시나리오
channel.basicConsume("order_queue", false, (consumerTag, delivery) -> {
    String order = new String(delivery.getBody(), "UTF-8");
    
    processOrder(order);  // ✅ 주문 처리 완료
    
    // 💥 여기서 컨슈머 크래시 → ACK 못 보냄
    channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}, consumerTag -> {});
 
// 결과:
// 1. 주문은 이미 처리됨
// 2. ACK를 못 받은 RabbitMQ가 메시지를 다른 컨슈머에게 재전송
// 3. 같은 주문이 중복 처리됨 ❌

꼬리질문: ACK 타이밍 문제는 무엇인가요?

  • 처리와 ACK 사이의 시간차: 메시지 처리 완료와 ACK 전송 사이에 장애 발생 가능
  • 네트워크 지연: ACK가 브로커에 도달하지 못하는 경우

Exactly Once 구현 방법:

// 방법 1: 멱등성 구현
if (!orderRepository.exists(orderId)) {
    processOrder(order);
    channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
} else {
    // 이미 처리된 주문 - 그냥 ACK
    channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
 
// 방법 2: 트랜잭션 활용
@Transactional
public void handleMessage(Delivery delivery) {
    orderService.save(order);  // DB 작업과
    channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);  // ACK를 함께
}
 
// 방법 3: 중복 제거 테이블
String messageId = delivery.getProperties().getMessageId();
if (!processedMessageRepository.exists(messageId)) {
    processOrder(order);
    processedMessageRepository.save(messageId);
    channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}

출처: RabbitMQ 신뢰성 가이드 (opens in a new tab), Exactly Once 불가능성 분석 (opens in a new tab)