본문 바로가기
Spring/ETC

RabbitMq 나홀로 학습

by YoonJong 2025. 11. 17.
728x90
반응형

쇼핑몰 개인 프로젝트에 RabbitMQ를 도입하며 가졌던 의문점과 그 해답을 Q&A 형식으로 정리했다.

 

 

Q. Connection 1개, Channel 6개, Queue 1개? 이게 무슨 말인가?

  • Connection (1개): 내 Spring Boot 애플리케이션과 RabbitMQ 서버가 연결된 하나의 물리적인 TCP 연결이다.
  • Channel (6개): 그 1개의 연결(Connection) 위에서 메시지를 주고받기 위해 만들어진 가상의 통로다. (큐 6개가 아니다!) 스프링이 효율을 위해 여러 개를 만들어 관리한다.
  • Queue (1개): order.notification.queue 같은 실제 메시지 저장소(창고)다.

Q. 'Exchanges' 탭은 뭔가?

  • '교환기'는 메시지 우체국(분류 센터)이다.
  • Producer는 메시지를 큐에 직접 보내는 게 아니라, Exchange에 먼저 보낸다. 그럼 Exchange가 Type(direct, topic, fanout) 규칙에 따라 어떤 큐로 보낼지 분류(라우팅)해준다.
  • amq.*로 시작하는 건 RabbitMQ 기본 교환기이니 무시해도 된다. 우리가 만든 order.exchange만 보면 된다.

Q. 'Nodes'는 3개인데 'Queues'는 왜 1개인가?

  • Nodes (3개): RabbitMQ를 실행하는 서버(컴퓨터) 3대가 한 팀(클러스터)을 이뤘다는 뜻이다.
  • Queues (1개): 그 3대의 서버가 힘을 합쳐 1개의 큐(작업)를 공동으로 관리하고 있다는 뜻이다.
  • 3대의 서버가 1개의 중요한 큐를 함께 지키고 있는 상태라고 보면 된다.

Q. 큐가 1개면 위험하지 않나?

  • 위험할 수 있다. 2가지 측면에서.
    1. 성능 병목: 모든 종류의 메시지(주문 생성, 주문 취소, 배송 알림...)를 큐 1개로 처리하면, 느린 작업 1개가 뒤의 모든 작업을 막아버릴 수 있다.
    2. 장애 격리: 잘못된 메시지(독이 든 메시지) 1개가 큐를 막아버리면 시스템 전체가 멈춘다.

Q. 처리 속도를 2배로 만들고 싶다. "주문 생성 큐"를 2개 만들면 되나?

  • 아니다. 그건 잘못된 접근이다.
  • "같은 종류의 작업"을 더 빨리 처리하고 싶을 때는, 큐(Queue)를 2개로 만드는 게 아니라 소비자(Consumer)를 2개로 늘려야 한다.

비유: 마트 계산대(Queue)에 줄(Message)이 길다.

  • 잘못된 생각: 줄을 2개로 나눈다. (Queue 2개)
  • 올바른 생각: 줄은 1개로 두고, 계산원(Consumer)을 2명으로 늘린다.

Q. 그럼 @RabbitListener의 concurrency = "2"는 뭔가?

  • @RabbitListener(queues = "...", concurrency = "2")
  • 이게 바로 "계산원을 2명 둔다"는 뜻이다.
  • ORDER_NOTIFICATION_QUEUE라는 1개의 큐를 동시에 처리할 소비자(스레드)의 개수를 2개로 하겠다는 의미다. 2개의 스레드가 큐의 메시지를 병렬로 나눠서 처리하므로 속도가 빨라진다.

Q. 그럼 "주문 큐"를 2개 이상 쓰는 경우는 절대 없나?

  • 있다. 하지만 "속도" 때문이 아니라 "분리"가 목적일 때다.
    1. 우선순위 분리: "VIP 주문 큐" / "일반 주문 큐"
    2. 장애 격리: "웹 주문 큐" / "모바일 앱 주문 큐" (모바일 앱 버그가 웹 주문까지 마비시키는 것을 방지)
    3. 데이터 지역성: "한국 주문 큐" / "미국 주문 큐"

Q. publisher-confirm-type: correlated와 publisher-returns: true가 뭔가?

  • 메시지 유실을 막는 2중 안전장치다.
  • publisher-confirm-type: correlated (송장번호)
    • 뜻: 내 메시지가 Broker(RabbitMQ 서버)에 안전하게 도착했는지(ACK/NACK) 회신을 받겠다.
    • 이유: 네트워크 문제로 메시지가 서버에 도착도 못 하고 사라지는 것을 방지한다.
  • publisher-returns: true (주소 불명 반송)
    • 뜻: Broker까지는 잘 갔는데, 주소(Routing Key)가 틀려서 Queue로 배달이 안 되면, 메시지를 나한테 반송해라.
    • 이유: 주소 오타 등으로 메시지가 엉뚱한 곳으로 가거나 버려지는 것을 방지한다.

Q. 이런 실패는 어디서 확인하나?

  • 설정만 켠다고 끝나는 게 아니다. 실패를 받는 '콜백' 코드를 RabbitTemplate에 직접 등록해야 한다.
// RabbitMQConfig.java (일부)
@Slf4j
@Configuration
public class RabbitMQConfig {

    @Bean
    public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory, 
                                           MessageConverter messageConverter) {
        
        RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
        rabbitTemplate.setMessageConverter(messageConverter);

        // [1. Confirm 콜백: Broker 도착 실패 확인]
        rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
            if (!ack) {
                log.error("[Confirm 실패] ID={}, 원인={}", correlationData.getId(), cause);
                // [조치] DB에 '발행 실패'로 저장하고 나중에 재시도
            }
        });

        // [2. Return 콜백: Queue 라우팅 실패(반송) 확인]
        rabbitTemplate.setReturnsCallback((returned) -> {
            log.error("[Return 반송] 메시지={}, Exchange={}, 라우팅키={}, 사유={}",
                    new String(returned.getMessage().getBody()),
                    returned.getExchange(),
                    returned.getRoutingKey(),
                    returned.getReplyText()
            );
            // [조치] 99% 개발자 실수. 즉시 알림 보내서 코드/설정 고쳐야 함
        });

        return rabbitTemplate;
    }
}

 

Q. 실패한 메시지(DLQ)는 UI에서 직접 볼 수 있나?

  • 있다. 그게 DLQ를 쓰는 핵심 이유 중 하나다.
  • UI의 Queues 탭에서 dead.letter.queue(설정한 DLQ 이름)를 클릭한다.
  • 하단의 Get messages 패널에서 Get Message(s) 버튼을 누르면 실패한 메시지 내용을 볼 수 있다.
  • Payload: "무엇이" 실패했는지 (메시지 원본)
  • Headers > x-death: "왜" 실패했는지 ( reason: "rejected" 등)

Q. (경고) UI에서 메시지 조회 시 'Requeue' 옵션은 어디 있나?

  • 실패한 메시지를 조회할 때 조회만 하고 큐에서 삭제해버리면 안 된다.
  • Get messages 패널의 Ack Mode 드롭다운에서 Nack message requeue true를 선택해야 한다.
  • 이게 "메시지를 확인만 하고 다시 큐에 되돌려놓는다(Requeue: Yes)"는 뜻이다.
  • Automatic ack를 선택하면 메시지가 큐에서 삭제되니 절대 주의해야 한다.

Q. ERLANG_COOKIE는 뭐고 어떻게 만드나?

  • 클러스터를 구성할 노드(서버)들끼리 공유하는 "비밀 접속 암호"다.
  • 모든 노드가 이 값이 똑같아야 서로를 신뢰하고 팀을 이룬다.
  • 터미널에서 아래 명령어로 생성한다. (아무 랜덤 문자열이나 상관없다)
# 32바이트 길이의 랜덤 문자열 생성
$ openssl rand -base64 32
wN/S/P8+vPqA8p9s8r9/QzFq8p8/wN/S/P8+vPqA8p9s8r9/QzFq

 

Q. 클러스터링을 위해 docker-compose.yml을 수정했다. 그냥 docker-compose up -d만 하면 되나?

  • 아니다. docker-compose.yml은 3개의 서버를 띄우는 역할만 한다.
  • 띄운 후에는 "너희는 이제부터 한 팀이야"라고 묶어주는 join_cluster 명령을 수동으로 실행해야 한다.
# 1. 모든 컨테이너 시작
docker-compose up -d

# 2. rabbitmq2 노드를 rabbitmq1에 조인
docker-compose exec rabbitmq2 rabbitmqctl stop_app
docker-compose exec rabbitmq2 rabbitmqctl join_cluster rabbit@rabbitmq1
docker-compose exec rabbitmq2 rabbitmqctl start_app

# 3. rabbitmq3 노드를 rabbitmq1에 조인
docker-compose exec rabbitmq3 rabbitmqctl stop_app
docker-compose exec rabbitmq3 rabbitmqctl join_cluster rabbit@rabbitmq1
docker-compose exec rabbitmq3 rabbitmqctl start_app

Q. 클러스터링이 잘 됐는지 어떻게 확인하나?

  • 아래 명령어를 실행한다.
  • Running Nodes에 rabbit@rabbitmq1, rabbit@rabbitmq2, rabbit@rabbitmq3가 모두 보이고, Network Partitions가 (none)이면 성공이다.

 

docker-compose exec rabbitmq1 rabbitmqctl cluster_status
➜  shoppingmall_project git:(main) ✗ docker-compose exec rabbitmq1 rabbitmqctl cluster_status
WARN[0000] /Users/r00416/Library/Mobile Documents/com~apple~CloudDocs/Desktop/project/study/shoppingmall_project/docker-compose.yml: the attribute `version` is obsolete, it will be ignored, please remove it to avoid potential confusion
Cluster status of node rabbit@rabbitmq1 ...
Basics

Cluster name: rabbit@rabbitmq1
Total CPU cores available cluster-wide: 36

Disk Nodes

rabbit@rabbitmq1
rabbit@rabbitmq2
rabbit@rabbitmq3

Running Nodes

rabbit@rabbitmq1
rabbit@rabbitmq2
rabbit@rabbitmq3

Versions

rabbit@rabbitmq1: RabbitMQ 3.12.14 on Erlang 25.3.2.15
rabbit@rabbitmq2: RabbitMQ 3.12.14 on Erlang 25.3.2.15
rabbit@rabbitmq3: RabbitMQ 3.12.14 on Erlang 25.3.2.15

CPU Cores

Node: rabbit@rabbitmq1, available CPU cores: 12
Node: rabbit@rabbitmq2, available CPU cores: 12
Node: rabbit@rabbitmq3, available CPU cores: 12

Maintenance status

Node: rabbit@rabbitmq1, status: not under maintenance
Node: rabbit@rabbitmq2, status: not under maintenance
Node: rabbit@rabbitmq3, status: not under maintenance

Alarms

(none)

Network Partitions

(none)

Listeners

Node: rabbit@rabbitmq1, interface: [::], port: 15672, protocol: http, purpose: HTTP API
Node: rabbit@rabbitmq1, interface: [::], port: 15692, protocol: http/prometheus, purpose: Prometheus exporter API over HTTP
Node: rabbit@rabbitmq1, interface: [::], port: 25672, protocol: clustering, purpose: inter-node and CLI tool communication
Node: rabbit@rabbitmq1, interface: [::], port: 5672, protocol: amqp, purpose: AMQP 0-9-1 and AMQP 1.0
Node: rabbit@rabbitmq2, interface: [::], port: 15672, protocol: http, purpose: HTTP API
Node: rabbit@rabbitmq2, interface: [::], port: 15692, protocol: http/prometheus, purpose: Prometheus exporter API over HTTP
Node: rabbit@rabbitmq2, interface: [::], port: 25672, protocol: clustering, purpose: inter-node and CLI tool communication
Node: rabbit@rabbitmq2, interface: [::], port: 5672, protocol: amqp, purpose: AMQP 0-9-1 and AMQP 1.0
Node: rabbit@rabbitmq3, interface: [::], port: 15672, protocol: http, purpose: HTTP API
Node: rabbit@rabbitmq3, interface: [::], port: 15692, protocol: http/prometheus, purpose: Prometheus exporter API over HTTP
Node: rabbit@rabbitmq3, interface: [::], port: 25672, protocol: clustering, purpose: inter-node and CLI tool communication
Node: rabbit@rabbitmq3, interface: [::], port: 5672, protocol: amqp, purpose: AMQP 0-9-1 and AMQP 1.0

Feature flags

Flag: classic_mirrored_queue_version, state: enabled
Flag: classic_queue_type_delivery_support, state: enabled
Flag: direct_exchange_routing_v2, state: enabled
Flag: drop_unroutable_metric, state: enabled
Flag: empty_basic_get_metric, state: enabled
Flag: feature_flags_v2, state: enabled
Flag: implicit_default_bindings, state: enabled
Flag: listener_records_in_ets, state: enabled
Flag: maintenance_mode_status, state: enabled
Flag: quorum_queue, state: enabled
Flag: restart_streams, state: enabled
Flag: stream_queue, state: enabled
Flag: stream_sac_coordinator_unblock_group, state: enabled
Flag: stream_single_active_consumer, state: enabled
Flag: tracking_records_in_ets, state: enabled
Flag: user_limits, state: enabled
Flag: virtual_host_metadata, state: enabled

 

Q. 왜 클러스터는 2대나 4대 (짝수)로 안 하나?

  • "스플릿 브레인(Split-Brain)" 때문이다.
  • 클러스터는 "과반수(Quorum)"의 동의로 작동한다.
  • 4대 (짝수): 네트워크가 끊겨 2:2로 쪼개지면, 양쪽 다 과반수(3표) 확보에 실패해 클러스터 전체가 멈춘다.
  • 3대 (홀수): 2:1로 쪼개져도, 2표를 가진 그룹이 과반수를 확보해 서비스가 멈추지 않는다.
  • 그래서 3대가 4대보다 안정적이다.

Q. 클러스터링은 성공했는데, 왜 Master/Slave가 안 보이나?

  • cluster_status는 서버(노드) 상태만 보여준다.
  • "Master/Slave"(정확히는 Leader/Follower)는 큐(Queue) 하나하나에 적용되는 개념이다.
  • 즉, "A 큐의 대장은 rabbit1", "B 큐의 대장은 rabbit2"처럼 큐마다 다르다.

Q. 그럼 큐를 클러스터링하려면(Leader/Follower) 어떻게 하나?

  • "쿼럼 큐(Quorum Queue)"로 생성해야 한다. 이래야 큐의 데이터가 3개 노드에 복제된다.

 

⚠️ (매우 중요) 쿼럼 큐로 바꿨는데, 그냥 프로젝트 재시작하면 되나?

  • 절대 안 된다. 100% 에러 난다.
  • 이유: RabbitMQ는 "이미 '클래식' 타입으로 존재하는 큐의 속성을 '쿼럼' 타입으로 변경할 수 없어!"라며 시작을 막는다.
  • 올바른 순서:
    1. Management UI에서 기존 order.notification.queue를 수동으로 삭제한다.
    2. Spring Boot 프로젝트를 재시작한다.
  • 이렇게 해야 새 설정(쿼럼 큐)으로 큐가 정상 생성되며, UI에서 Leader/Follower 상태를 확인할 수 있다.

728x90
반응형

댓글