inblog logo
|
vlogue
    DBJava & Spring BootJava & JPA & HibernateSystem/Network

    RabbitMQ 토폴로지 관리는 버겁다.

    문제를 삼아 문제를 만드는 사람
    김주혁's avatar
    김주혁
    Sep 29, 2026
    RabbitMQ 토폴로지 관리는 버겁다.
    RabbitMQ의 exchange, queue, binding 이름을 각 서비스 코드에 문자열로 박아 두면, 인프라와 애플리케이션이 어긋날 확률이 높습니다.
    @RabbitListener(queues = "delivery.dev.queue") public void onMessage(DeliveryEvent event) { ... }
    서비스가 두세 개일 때는 이래도 괜찮습니다. 그런데 stage가 local / dev / prod로 늘고, DLQ와 추가 큐가 붙고, 누군가 큐 이름을 살짝 바꾸는 순간부터 숨은 그림 찾기를 시작하게 됩니다.
    그래서 토폴로지를 한 곳에서 선언하기로 했습니다.
     

    먼저 Terraform으로 모아봤습니다.

    파편화된 정보를 모으는 가장 확실한 방법은 리소스 공급원을 하나로 정하는 것입니다. exchange, queue, binding은 전부 Terraform으로 선언하고, 그 외 누구도 토폴로지를 만들지 않기로 했습니다.
    infra-terraform rabbitmq/<env>/topology - exchange / queue / binding 선언 - Terraform state 관리
    이름도 규칙으로 고정했습니다.
    {service}.{stage}.event -> 서비스 exchange {service}.{stage}.dlx -> dead letter exchange {service}.{stage}.queue -> main queue {service}.{stage}.queue.dlq -> main DLQ {service}.{stage}.{path}.queue -> 추가 queue {service}.{stage}.{path}.queue.dlq -> 추가 queue의 DLQ
    인프라 쪽은 깔끔해졌습니다.
     
    그런데 서비스 코드를 열어 보니 여전히 "delivery.dev.queue"가 그대로 박혀 있었습니다. 선언을 한 곳에 모았을 뿐, 그 결과를 서비스가 알 방법은 없었던 겁니다.
     
    문자열이 Terraform 코드와 서비스 코드, 두 군데에 있는 상황은 그대로였습니다.
     

    그래서 나는 계약서를 뽑았습니다.

    서비스가 Terraform 코드를 읽게 할 수는 없으니, Terraform이 적용을 마친 결과를 서비스가 읽기 쉬운 모양으로 바꿔 주기로 했습니다. 이걸 계약서(contract)라고 부르겠습니다.
    기준은 코드가 아니라 state입니다. terraform apply가 끝난 뒤 terraform show -json을 뽑고, 거기서 rabbitmq_exchange, rabbitmq_queue, rabbitmq_binding 세 타입만 읽습니다. 어떤 리소스가 어느 서비스의 무엇인지는 1단계에서 정한 이름 규칙으로 판단합니다.
    delivery.dev.event -> service=delivery, stage=dev delivery.dev.queue -> resources.dev.queue delivery.dev.device.mqtt.queue -> alias=mqtt -> resources.dev.mqttQueue
    그러면 서비스 하나당 이런 JSON이 나옵니다.(예제임)
    { "service": "order", "vhost": "/", "stages": ["dev", "local", "prod"], "routingKeys": ["order.cancelled", "order.created"], "queueRoutingKeys": { "main": ["order.cancelled", "order.created"], "payment": ["order.payment.approved", "order.payment.failed"] }, "resources": { "dev": { "exchange": "order.dev.event", "dlx": "order.dev.dlx", "queue": "order.dev.queue", "dlq": "order.dev.queue.dlq", "paymentQueue": "order.dev.payment.queue", "paymentDlq": "order.dev.payment.queue.dlq" } } }
    필드
    의미
    routingKeys
    main queue의 routing key. 구 스키마 호환용이며 queueRoutingKeys.main과 같아야 합니다.
    queueRoutingKeys.<alias>
    queue alias별 routing key (main 포함)
    resources.<stage>.queue
    서비스가 기본으로 구독할 main queue
    resources.<stage>.<alias>Queue
    alias로 꺼내 쓰는 추가 queue
    이름은 한 곳에서 나오게 됐습니다. 근데 이 JSON 파일을 서비스에 어떻게 전달하죠? 복사해서 각 저장소에 커밋하면, 다시 "어느 쪽이 최신이지?" 문제로 돌아갑니다.
     

    그래서 Maven 아티팩트로 뿌렸습니다.

    서비스 개발자에게 가장 익숙한 공유 방법은 결국 버전이 붙은 의존성입니다. 그래서 계약서를 Maven 저장소(여기서는 Reposilite)에 JSON 아티팩트로 올리기로 했습니다.
    임시 Gradle workspace를 하나 만들고, contract/*.json을 자동으로 찾아 서비스마다 publication을 만듭니다. 서비스를 하나 추가해도 publish 설정은 건드릴 필요가 없습니다.
    contract/order.json -> com.example.rabbitmq.contract:order-topology:<version>@json
    근데 이제 토폴로지 하나 바꾸려면 init → plan → apply → show → 추출 → publish를 매번 손으로 돌려야 했습니다. 순서를 하나 빼먹거나, RabbitMQ 접속 정보를 로컬에 들고 다니는 것도 귀찮고 번거롭고 아무튼 그랬습니다.
     

    그래서 Ansible로 묶었습니다.

    RabbitMQ 클러스터는 원래 Ansible로 배포하고 있었습니다. 그래서 같은 role에 Terraform 실행과 계약서 추출, publish를 얹었습니다. 단, 역할은 분명히 나눴습니다. Ansible은 실행만 하고, 토폴로지는 절대 직접 선언하지 않습니다. Ansible 변수에 큐 이름이 하나라도 들어가는 순간 리소스 공급원이 두 개가 되니까요.
     
    근데 큐 이름 하나가 규칙을 살짝 벗어나자 계약서가 조용히 비어 버렸습니다. Terraform apply도, publish도 전부 성공인데 말이죠.
     
    그래서 검증과 fail-fast를 넣었습니다.
    원인은 이름 규칙이었습니다. order 서비스의 main queue를 order.dev.queue가 아니라 order.dev.payment.queue로 override하면 이렇게 됩니다.
    # 이렇게 하면 계약서가 조용히 비어 버린다 module "order_stage" { for_each = local.stages source = "../../modules/rabbitmq/topology" vhost = "/" domain = "order" stage = each.key queue_name = "order.${each.key}.payment.queue" # main queue 이름 override routing_keys = local.order_routing_keys }
    extractor 입장에서 이 큐는 main이 아니라 alias가 payment인 추가 큐입니다. 그래서 resources.dev.queue가 빠지고, routingKeys와 queueRoutingKeys.main은 빈 배열이 되고, 서비스는 빈 큐 이름을 받습니다. 에러는 어디서도 나지 않습니다.
    먼저 Terraform을 규칙대로 고쳤습니다. main queue는 표준 이름으로 두고, 별도 용도의 큐는 추가 큐로 따로 선언합니다.
    module "order_stage" { for_each = local.stages source = "../../modules/rabbitmq/topology" vhost = "/" domain = "order" stage = each.key routing_keys = local.order_main_routing_keys # main queue는 표준 이름 } module "order_payment_queue" { for_each = local.stages source = "../../modules/rabbitmq/extra-queue" vhost = "/" exchange = module.order_stage[each.key].exchange_name name = "order.${each.key}.payment.queue" routing_keys = local.order_payment_routing_keys }
    하지만 휴먼은 언제나 에러를 만들어냅니다. 그래서 더 방어적으로 만들었습니다.
    • publish 전에 계약서를 검사합니다. 하나라도 걸리면 publish를 멈춥니다.
    • 서비스 쪽에서 계약서를 읽을 때 값이 비어 있으면 빈 문자열을 돌려주지 않고 기동 시점에 바로 터뜨립니다. 빈 큐 이름은 메시지가 안 들어오는 시점에야 티가 나지만, 기동 실패는 배포하자마자 보입니다.
    notion image
    사람은 왼쪽 위의 Terraform 코드만 고치고, 가운데 네 단계는 Ansible playbook 한 번으로 돌면 끝입니다.
     

    뭐가 바뀐거냐면

    상황
    전
    후
    큐 이름이 있는 곳
    Terraform 코드 + 서비스마다 문자열
    Terraform 한 곳
    서비스가 이름을 아는 방법
    관리 화면 보고 복사
    계약서 의존성
    큐 이름 변경
    서비스 코드 grep 후 수정
    Terraform 수정 → 계약서 버전 업 → 서비스 의존성 버전 업
    routing key 확인
    Terraform 코드나 관리 화면 뒤지기
    queueRoutingKeys에서 바로 확인
    토폴로지 반영
    명령 여러 개를 손으로
    playbook 한 번, 클러스터 재시작 없이
    이름 규칙 위반
    메시지가 안 들어올 때 발견
    publish 전 검증 또는 서비스 기동 시 실패
    어떤 버전의 토폴로지를 쓰는지
    알 수 없음
    의존성 버전으로 명시
    제일 크게 바뀐 건 마지막 줄입니다. 이제 "이 서비스는 어떤 토폴로지 기준으로 배포됐지?"라는 질문에 build.gradle 한 줄로 답할 수 있습니다.
     

    마치며

    만들다보니 점점 복잡해졌는데, 선언을 모으니 전달이 문제였고, 전달을 풀자 실행이 번거로웠고, 실행을 묶고 나니 조용한 실패가 남았습니다.
    그래도 만들고 나서..는 굉장히 효율적으로 사용할 수 있게 됐습니다.
    누가 관리해주지않고 혼자서, 리소스들을 일일히 관리하게 되는 일은 너무 버거우니까요 같이 일하는 사람들도 만족하면서 쓰는걸 보니 나쁘진 않습니다.
    문제점도 명확한데,
    • 서비스마다 Gradle 보일러플레이트가 붙는다는게 가장 치명적, Gradle이 지저분해짐
    • 큐 이름을 바꾸거나 지울 때 배포 순서를 사람이 챙겨야 함..
    • 항상 최신 계약서를 받으니 예전 커밋을 다시 빌드해도 같은 결과가 나오지 않아서 재현이 어려움..
    • 메시지 내용(payload)은 보장하지 못하는데 이건 사내 라이브러리로 DTO 공유 중이라 해결…
    • 선언이 한 저장소에 모여 있어 서비스가 늘면 병목이 될 수 있음..
    • 결국 번거로움..
    • 가이드 없으면 처음보고 어떻게 돌아가는지 알 수 없음..
    제일 고민했던건 Contract를 풀어주는 PropertyProvider같은걸 만들었으면(이것도 복잡해지는 일이지만) 쓰는건 더 편하지 않았을까 싶네요…
    이렇게 바꾼이후로 큐나 이름이나.. 이런걸로 장애난적은 없습니다만, 귀찮은 일을 해결하기 위해 또 다른 귀찮은 일을 만든 느낌인건 지울 수 없네요
    Share article

    vlogue

    RSS·Powered by Inblog