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)