1. 주요 특징
신뢰성 (Reliability)
다양한 메커니즘을 제공하여 성능과 신뢰성 사이의 균형을 조절할 수 있습니다.
예시: 메시지 영속화(Persistence) - 디스크에 메시지를 저장하여 서버 재시작 시 데이터 손실을 방지하지만, 디스크 I/O로 인해 처리량이 감소할 수 있습니다.
유연한 라우팅 (Flexible Routing)
다양한 Exchange 타입을 지원하며, 각각 다른 라우팅 규칙을 제공합니다.
사용자 정의 Exchange를 플러그인 형태로 구현하여 확장할 수 있습니다.
다중 프로토콜 지원 (Multi-protocol)
- STOMP: HTTP와 유사한 텍스트 기반 프로토콜. 명령어와 헤더는 텍스트, 메시지 본문은 바이너리 또는 텍스트 가능. 파싱 비용이 상대적으로 높습니다.
- MQTT: 바이너리 프로토콜. 낮은 대역폭, 높은 지연 시간, 불안정한 네트워크 환경에서 IoT 센서나 모바일 디바이스에 최적화되어 있습니다.
- RabbitMQ Streams: 고처리량, 저지연의 스트리밍 데이터 처리를 위한 바이너리 프로토콜. 로그, 원격 측정 데이터 등 대규모 실시간 데이터 스트림에 적합합니다.
- HTTP/WebSocket: 브라우저 등 클라이언트가 WebSocket을 통해 STOMP나 MQTT 프로토콜을 사용할 수 있게 해줍니다.
- AMQP 0-9-1: Exchange, Queue, Binding 등 강력한 메시지 시맨틱을 정의하는 핵심 프로토콜입니다.
- AMQP 1.0: 국제 표준 프로토콜로, 서로 다른 메시지 미들웨어 간의 상호 운용성을 보장합니다.
관리 도구 (Management UI)
RabbitMQ 서버를 구동한 후, 서버 IP와 설정된 포트 번호로 접속하면 웹 기반 관리 인터페이스를 사용할 수 있습니다.
클러스터링 및 페더레이션
- 클러스터: 여러 RabbitMQ 서버를 하나의 논리적 브로커로 묶어 고가용성과 확장성을 제공합니다.
- 페더레이션 (Federation): 서로 다른 클러스터나 데이터 센터 간에 메시지를 안전하게 교환할 수 있도록 해줍니다.
2. 주요 역할
- 디커플링 (Decoupling): 서비스 간 직접적인 의존성을 제거합니다. 한 서비스의 변경이 다른 서비스에 영향을 주지 않도록 메시지를 통해 통신합니다.
- 비동기 처리 (Asynchronous): 주문 후 포인트 적립, 혜택 부여, 알림 발송 등 여러 후속 작업을 순차적으로 처리하지 않고 메시지 큐를 통해 동시에 처리하여 응답 시간을 단축합니다.
- 트래픽 완충 (Load Leveling / Traffic Shaping): 순간적으로 폭증하는 요청을 큐에 저장하고, Consumer가 처리 가능한 속도로 소비하도록 조절합니다. 예를 들어, 12306에서 "구매 요청이 접수되었습니다"라는 메시지를 보여주는 것이 이에 해당합니다. RabbitMQ의 QoS(Quality of Service) 메커니즘을 통해 Consumer의 처리 능력을 초과하지 않도록 제어할 수 있습니다.
3. 기본 아키텍처
메시지 흐름
- Producer는 Broker에 Connection을 생성하고, 그 안에 Channel을 엽니다.
- Producer는 RoutingKey와 함께 메시지를 특정 Exchange에 전송합니다.
- Exchange는 Binding 규칙에 따라 메시지를 적절한 Queue로 라우팅합니다.
- Queue에 메시지가 있으면 Broker는 설정에 따라 Consumer에게 Push(Push 모델)하거나, Consumer는 Pull(Pull 모델)하여 메시지를 가져옵니다.
핵심 구성 요소
- Virtual Host (vhost): 다중 사용자 및 보안을 위한 논리적 격리 단위입니다. 각 사용자는 자신의 vhost 내에서만 Exchange, Queue 등을 생성하고 관리할 수 있습니다.
- Broker: RabbitMQ 서버 자체를 의미합니다.
- Exchange:
- Direct: RoutingKey가 BindingKey와 완전히 일치하는 Queue로 메시지를 전달합니다.
- Topic: RoutingKey와 BindingKey를 패턴 매칭(와일드카드 사용)하여 메시지를 전달합니다.
- Fanout: RoutingKey를 무시하고 Exchange에 바인딩된 모든 Queue로 메시지를 브로드캐스트합니다.
- Headers: RoutingKey 대신 메시지 헤더 속성을 기반으로 라우팅합니다. 사용 빈도가 낮습니다.
- Queue: 메시지가 저장되는 버퍼 역할을 합니다.
- Consumer:
- Pull 모델: Consumer가 주기적으로 큐를 폴링하여 메시지를 가져옵니다. 지연 시간이 발생할 수 있습니다.
- Push 모델: Queue에 메시지가 도착하면 Broker가 Consumer에게 즉시 푸시합니다.
Spring Boot에서의 구현 흐름
@Configuration클래스에서TopicExchange와Queue를 빈으로 선언합니다.BindingBuilder.bind()를 사용하여 Queue와 Exchange를 바인딩합니다.RabbitAdmin이 애플리케이션 시작 시 모든 Exchange와 Queue를 RabbitMQ에 자동으로 생성합니다.- Producer는
RabbitTemplate.convertAndSend()메서드로 Exchange 이름과 RoutingKey를 지정하여 메시지를 전송합니다. - Consumer는
@RabbitListener(queues = "queue-name")어노테이션을 사용하여 특정 Queue를 리스닝합니다.
4. 핵심 메커니즘
메시지 영속화 (Message Persistence)
- Exchange 및 Queue 선언 시
durable=true를 설정합니다(기본값true). - 메시지 전송 시
delivery_mode속성을2(PERSISTENT)로 설정합니다.
메시지 확인 (Message Acknowledgement)
- 자동 확인 (Auto Ack): Broker가 Consumer에게 메시지를 전달한 즉시 Queue에서 삭제합니다. Consumer의 실제 처리 성공 여부는 고려하지 않습니다. 간단하지만 메시지 손실 위험이 있으며, 로그 수집 등 손실이 허용되는 경우에 적합합니다.
// autoAck = true, 소비 후 자동 확인
channel.basicConsume("my_queue", true, (consumerTag, delivery) -> {
String message = new String(delivery.getBody());
System.out.println("수신 메시지: " + message);
// 메서드 실행 완료 시 RabbitMQ가 자동으로 처리 완료로 간주
}, consumerTag -> {});
- 수동 확인 (Manual Ack): Consumer가 명시적으로 확인 응답을 보내야 합니다. 신뢰성이 높지만 구현이 복잡합니다.
// autoAck = false, 수동 확인 필요
channel.basicConsume("another_queue", false, (consumerTag, delivery) -> {
try {
String message = new String(delivery.getBody());
System.out.println("메시지 처리: " + message);
// 비즈니스 로직 성공 시 수동 확인
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
// multiple = false: 현 메시지만 확인
} catch (Exception e) {
// 처리 실패 시 거부 및 재큐잉
channel.basicNack(delivery.getEnvelope().getDeliveryTag(), false, true);
// multiple = false, requeue = true
}
}, consumerTag -> {});
참고: 수동 확인 모드에서 basicAck 호출을 누락하면 메시지는 'Unacked' 상태로 남아 RabbitMQ 메모리를 점유하게 됩니다. 이는 메모리 부족 문제를 유발할 수 있으므로 주의해야 합니다.
메시지 분배 (Message Distribution)
- 작업 큐 (Work Queue): 여러 Consumer가 하나의 Queue를 모니터링합니다.
- 라운드 로빈 (Round-robin): RabbitMQ 기본 분배 방식. Consumer의 처리 속도와 관계없이 메시지를 순차적으로 균등하게 분배합니다. 처리 속도가 느린 Consumer에 메시지가 쌓일 수 있습니다.
- 공정 분배 (Fair Dispatch):
channel.basicQos(1)설정을 통해 활성화됩니다. Consumer가 하나의 메시지를 처리하고 확인(Ack)한 후에만 다음 메시지를 받도록 합니다. 이는 '처리 능력에 따른 분배'를 가능하게 합니다. 반드시 수동 확인 모드(autoAck=false)와 함께 사용해야 합니다.
- 메시지 라우팅 (Message Routing): Exchange의 4가지 라우팅 방식(Direct, Topic, Fanout, Headers)에 따라 메시지가 Queue에 전달됩니다.
발행 확인 (Publisher Confirms)
- Confirm 메커니즘: Producer가 Exchange에 메시지를 전송했을 때, Broker가 성공적으로 수신하면
confirm()콜백을 호출합니다. Exchange가 메시지를 Queue로 라우팅하지 못하면returnedMessage()콜백이 호출됩니다. (라우팅 가능한 경우 콜백 없음) - ACK 트랜잭션: application.yml에 다음 설정을 추가하여 활성화합니다.
spring.rabbitmq.publisher-confirm-type: correlatedspring.rabbitmq.publisher-returns: truespring.rabbitmq.template.mandatory: true
데드 레터 큐 (Dead Letter Queue)
소비되지 못한 메시지(Dead Letter)가 이동하는 특수 큐입니다.
데드 레터가 되는 조건은 다음과 같습니다:
- 메시지가 거부(
basic.reject또는basic.nack)되고requeue=false인 경우 - 메시지 TTL(Time-To-Live)이 만료된 경우
- Queue의 최대 길이를 초과하여 가장 오래된 메시지가 제거될 경우
데드 레터는 데드 레터 Exchange(DLX)로 전달된 후, 해당 Exchange에 바인딩된 데드 레터 Queue(DLQ)에 저장됩니다. 이를 통해 모니터링이나 재처리 로직을 구현할 수 있습니다.
@Configuration
public class DeadLetterConfig {
@Bean
public DirectExchange dlxExchange() {
return new DirectExchange("dlx_exchange");
}
@Bean
public Queue dlxQueue() {
return new Queue("dlx_queue");
}
@Bean
public Binding dlxBinding() {
return BindingBuilder.bind(dlxQueue()).to(dlxExchange()).with("dead_routing_key");
}
// 정상 Queue를 선언할 때 데드 레터 Exchange와 바인딩
@Bean
public Queue normalQueue() {
return QueueBuilder.durable("normal_queue")
.deadLetterExchange("dlx_exchange")
.deadLetterRoutingKey("dead_routing_key")
.ttl(30000) // 메시지 TTL (30초)
.maxLength(1000) // Queue 최대 길이
.build();
}
}
지연 큐 (Delayed Queue)
설정된 지연 시간 후에 메시지를 Consumer에게 전달하는 패턴입니다.
TTL + Dead Letter 조합으로 구현합니다.
- 핵심 아이디어: 먼저 메시지를 Consumer가 없는 Queue(지연 큐)에 보내고, TTL이 만료되어 데드 레터가 되면 이를 실제 Consumer가 모니터링하는 Queue(DLQ)로 이동시키는 것입니다.
- Queue에 TTL을 설정하면 메시지가 만료 즉시 폐기됩니다. 개별 메시지에 TTL을 설정하면 메시지가 Consumer에게 전달되기 직전에 만료 여부가 확인되므로, 큐에 쌓여 있는 만료된 메시지도 일시적으로 남아있을 수 있습니다. 두 값이 모두 설정된 경우 더 작은 값이 우선합니다.
@Configuration
public class DelayQueueSpringConfig {
@Bean
public DirectExchange delayExchange() {
return new DirectExchange("order.delay.exchange");
}
@Bean
public DirectExchange dlxExchange() {
return new DirectExchange("order.dlx.exchange");
}
@Bean
public Queue dlxQueue() {
return new Queue("order.dlx.queue");
}
@Bean
public Binding dlxBinding() {
return BindingBuilder.bind(dlxQueue()).to(dlxExchange()).with("order.timeout");
}
@Bean
public Queue delayQueue() {
return QueueBuilder.durable("order.delay.queue")
.deadLetterExchange("order.dlx.exchange")
.deadLetterRoutingKey("order.timeout")
.ttl(30 * 60 * 1000) // 30분
.build();
}
@Bean
public Binding delayBinding() {
return BindingBuilder.bind(delayQueue()).to(delayExchange()).with("order.delay");
}
}
지연 메시지 전송
@Service
public class DelayMessageService {
@Autowired
private RabbitTemplate rabbitTemplate;
public void sendDelayMessage(String orderId) {
rabbitTemplate.convertAndSend("order.delay.exchange", "order.delay", orderId);
}
}
지연 메시지 소비 (DLQ 리스닝)
@Component
public class TimeoutOrderListener {
@RabbitListener(queues = "order.dlx.queue")
public void processTimeoutOrder(String orderId) {
System.out.println("주문 시간 초과 체크: " + orderId);
// 주문 상태 확인 및 처리 로직
}
}
5. 실무 문제 및 해결 방안
메시지 손실 (Message Loss)
메시지는 Producer -> Broker -> Consumer로 전달되는 과정에서 두 번의 네트워크 전송을 거치며 손실될 수 있습니다.
- Producer -> Broker: Broker가 다운되거나 재시작될 때 메시지가 손실될 수 있음.
해결 방법: Publisher Confirms 메커니즘 사용. - Broker 내 Queue: Queue가 메시지를 영속화하지 않으면 Broker 재시작 시 데이터가 손실됨.
해결 방법: 메시지 영속화(Persistence) 설정. - Broker -> Consumer: Consumer가 메시지를 처리하기 전에 장애가 발생하면 메시지가 손실될 수 있음.
해결 방법: 수동 확인(Manual Ack) 메커니즘 사용. Consumer가 메시지를 성공적으로 처리한 후에만 Ack를 보내도록 합니다.
메시지 적체 (Message Accumulation)
Queue의 길이 제한을 초과하면 메모리 릭(Leak)이 발생하거나, 브로커 프로세스가 종료될 수 있습니다.
해결 방법: Queue의 최대 길이(maxLength)를 설정하고, 데드 레터 큐를 활용하여 초과 메시지를 안전하게 처리합니다.
메시지 처리 실패 (Message Processing Failure)
Consumer가 처리에 실패한 메시지는 데드 레터 큐로 이동시킵니다.
추가 처리: 데드 레터 큐를 리스닝하는 별도의 Consumer가 해당 메시지를 데이터베이스에 저장하여 관리자가 수동으로 개입하거나, 재처리 로직을 구현할 수 있습니다.
channel.basicConsume("dlx_queue", false, (consumerTag, delivery) -> {
String deadMsg = new String(delivery.getBody());
Map<String, Object> headers = delivery.getProperties().getHeaders();
// RabbitMQ가 자동으로 추가한 데드 레터 관련 헤더 정보
String reason = (String) headers.get("x-death");
System.err.println("데드 레터 메시지 수신: " + deadMsg);
System.err.println("사유: " + reason);
// 경고 알림 전송 또는 DB에 저장하여 관리자 개입 유도
sendAlert(deadMsg);
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}, consumerTag -> {});
참고 자료