
"RabbitMQ를 사용해 보셨나요? 그렇다면 AMQP가 무엇인지 아시나요?"
안녕하세요, 백엔드 개발자(지망생) 머랭입니다.
많은 개발자가 RabbitMQ를 사용하지만, 그 근간이 되는 AMQP의 깊은 곳까지는 들여다보지 못하곤 합니다.
AMQP는 크게 0-9-1 버전과 1.0 버전으로 나뉩니다. 언뜻 보면 1.0이 0-9-1의 상위 호환 버전처럼 느껴지지만, 실제로는 지향점이 완전히 다른 프로토콜입니다.
- AMQP 0-9-1은 Exchange, Queue, Binding 등 브로커 내부의 동작 모델을 구체적으로 명시했습니다.
- AMQP 1.0은 브로커 내부 구현을 각 벤더에게 맡기고, 전송 규약에 집중하는 추상적인 접근을 택했습니다.
RabbitMQ는 현대 개발 시장에서 가장 유명한 AMQP 0-9-1 구현체입니다.
RabbitMQ 4.0이 출시되며 AMQP 1.0을 플러그인 형태로 지원하기 시작했지만, 여전히 전 세계 수많은 메시징 시스템은 AMQP 0-9-1 위에서 동작하고 있습니다.
이번 포스팅에서는 현대 개발 시장에서 활발하게 사용되는 AMQP 0-9-1 프로토콜의 공식 문서와 논문을 분석하며 얻은 본질적인 동작 원리를 공유하고자 합니다.
단순히 "어떤 라이브러리를 써서 어떻게 보낸다"는 방법론을 넘어, AMQP 0-9-1 공식 문서를 탐구하며 얻은 지식을 바탕으로 "AMQP는 왜 그렇게 동작하는지"에 대한 본질적인 답을 찾아보고자 합니다.
라이브러리 메서드 하나로 메시지를 보낼 수 있는 편리한 시대에, 수백 페이지의 명세서를 읽으며 내부 구조를 탐구하는 이유는 명확합니다.
도구를 사용하는 것을 넘어, 기술의 본질을 이해하고 통제하는 힘을 가질 수 있습니다.
AMQP는 월스트리트의 투자 은행 JPMorgan에서 시작되었습니다.
기존의 상용 메시징 미들웨어는 메시지 형식이 제각각이었고, 특정 벤더에 종속적이라는 단점이 있었습니다.
ex) IBM의 메시징 미들웨어가 전송한 메시지를 Microsoft의 Consumer가 처리할 수 없었습니다.
이를 가능하게 하려면 메시지 형식 변환 브릿지(어댑터)를 개발해야 했습니다.
AMQP 개발자들은 메시징 기술이 TCP/IP처럼 누구나 사용할 수 있는 공용어가 되기를 원했습니다.
특정 벤더의 기술이나 프로토콜에 얽매이지 않고, 이기종 시스템 간에도 메시지를 신뢰성 있게 교환할 수 있는 개방형 표준을 만드는 것이 AMQP의 목표였습니다.

Broker는 복잡한 메시지 라우팅, Queue 관리를 비롯해 메시지 전송의 모든 책임을 집니다.
Consumer가 메시지를 잘 받았는지 확인될 때까지 Broker는 메시지를 보관하고 관리하며, 처리가 완료되면 Queue에서 메시지를 제거합니다.
메시지를 최소한 한 번 전달합니다.
메시지 유실을 방지하지만, 두 번 전달되는 것은 막을 수 없어 멱등한 비즈니스 로직을 작성해야 합니다.
Consumer가 데이터를 가져오기 위해 대기하거나 주기적으로 확인할 필요가 없습니다.
Broker는 새로운 메시지가 Queue에 도착하면 연결된 Consumer에게 즉시 전달합니다.
이 과정에서, prefetch 메커니즘을 통해 Consumer가 처리 가능한 메시지 양을 Broker에 알림으로써, 무분별한 Push로 인한 시스템 마비를 방지합니다

Frame은 AMQP 통신에서 네트워크를 타고 흐르는 데이터의 최소 단위입니다.
TCP는 데이터 경계가 없는 스트림 방식이기 때문에, 어디서부터 어디까지가 하나의 데이터 단위인지 구분하기 위해 모든 데이터를 Frame으로 포장해 주고받습니다.
Frame은 네 가지로 분류됩니다.

AMQP는 기능들을 Class로 묶고, 그 안의 동작을 Method로 정의합니다.
Method Frame은 Peer간 명령을 주고받기 위해 사용되는 Frame입니다.
“test-queue라는 이름의 큐를 생성해라” 혹은 “A Exchange를 제거해라”와 같은 명령 데이터를 주고받기 위해 사용됩니다.
개인적으로 CPU instruction set architecture와 비슷한 개념이라고 느꼈습니다.


A message is the atomic unit of processing of the middleware routing and queuing system. Messages carry a content, which consists of a content header, holding a set of properties, and a content body, holding an opaque block of binary data.
메시지는 미들웨어 라우팅 및 대기열 시스템의 처리를 위한 원자 단위입니다. 메시지는 콘텐츠 헤더, 속성 세트, 불투명한 이진 데이터 블록을 포함하는 콘텐츠 본문으로 구성된 콘텐츠를 전달합니다.
AMQP 0-9-1 공식 문서 중..
메시지는 AMQP 시스템에서 데이터 이동의 최소 단위입니다.
논리적인 최소 단위이며, 실제로는 Frame이 데이터 이동의 최소 단위입니다.
메시지는 Content Header와 Content Body라는 두 계층으로 설계되었습니다.
메시지를 전달하는 주체는 Content Header 영역의 데이터만 사용해 메시지를 전달합니다.
Content Header: 메타데이터 영역으로, 메시지의 속성(Properties)이 담겨 있습니다.
Content Body: 실제로 애플리케이션이 전달하고자 하는 비즈니스 데이터가 담겨 있습니다.

AMQP에서 통신은 물리적 연결인 Connection과 논리적 통로인 Channel의 이중 구조로 이루어집니다.
Channel은 메시지를 Frame 단위로 분해하여 전송하는 실질적인 데이터 스트리밍의 주체입니다.
Connection으로 전송되는 메시지는 여러 개의 Frame으로 나뉘어집니다. (메시지 분할은 Channel이 수행)
덕분에 멀티스레드 환경에서 여러 메시지를 동시에 전송할 수 있습니다.
단일 TCP 연결은 대역폭 한계가 존재하므로, 여러 개의 Connection을 생성해 Connection 풀을 구성할 수 있습니다.
만약 메시지가 Frame 단위로 나뉘어지지 않는다면?
한 스레드가 Connection을 독점하고 자신이 보낼 모든 메시지를 보낸 후 Connection을 반납합니다.
그 과정에서 다른 스레드는 Blocking됩니다.
결과적으로, 처리량이 낮아집니다
Routing Key는 Producer가 메시지를 발행할 때, 메시지와 함께 전송하는 문자열입니다.
Exchange는 Routing Key를 사용해 메시지를 어떤 Queue로 전달할지 결정합니다.
Producer는 메시지가 정확히 어떤 Queue로 전달되어야 하는지에 관심가지지 않고 Routing Key만 Exchange에 전달합니다.
덕분에 Broker는 단순한 1:1 전달을 넘어 하나의 메시지를 여러 Queue에 전달하거나 특정 조건에 맞는 곳으로만 보내는 복잡한 라우팅을 로직을 수행할 수 있습니다.
대소문자를 구분하므로, 설계 시 참고해야 합니다.
Point-To-Point 방식을 사용하려면 Routing Key를 Queue의 이름으로 설정합니다.
Pub-Sub 방식을 사용하려면 데이터의 성격을 나타내는 계층적 값을 사용합니다.
예:
order.new,payment.success
255 octet 크기의 문자열 형식으로 이루어집니다.
octet: 8비트 크기의 데이터 단위로, 과거 1 바이트가 8비트이지 않은 경우가 있어 생긴 단위입니다.

Producer는 메시지를 생성하고 Broker를 향해 발행하는 클라이언트 애플리케이션입니다.
Producer는 Queue에 메시지를 직접 발행하지 않고, 메시지와 Routing Key를 Exchange에게 전송합니다.
메시지를 Queue에 전달하는 과정은 Broker에서 이루어집니다.
높은 처리량을 위해 Producer → Broker 간 메시지 수신응답(ACK/NACK)은 이루어지지 않습니다.
RabbitMQ는 확장 기능으로 이 기능을 제공합니다. 신뢰성이 중요한 경우 사용할 수 있습니다.
RabbitMQ 3.0 이상에서는 다중 Consumer 에 대한 메시지 소비 체크 오버헤드로 인해 Immediate 플래그를 지원하지 않습니다.
AMQP의 핵심인 Broker는 메시지의 수신, 라우팅, 보관 및 전달을 총괄하는 시스템입니다.

Exchange는 Producer로부터 수신한 메시지를 하나 이상의 Queue 혹은 Exchange로 전달하기 위한 라우팅 엔진입니다.
라우팅 규칙에 따라 하나의 메시지를 여러 Queue 혹은 Exchange에 동시에 전달할 수 있습니다.
메시지의 Content Body에는 관여하지 않으며, 수신한 데이터를 변경 없이 그대로 전달합니다.
가장 중요한 속성 세 가지에 대해 설명하겠습니다.
order.*)이 Routing Key(예: order.new)에 매칭되는 바인딩의 Queue 혹은 Exchange로 메시지를 전달합니다.
Routing Key와 Binding Key가 정확히 일치하는 바인딩의 Queue 혹은 Exchange로 메시지를 전달합니다.
Routing Key:
order.new, Binding Key:order.new
- Binding Key가 정확히 일치하기 때문에 메시지가 전달됩니다.
Routing Key:
order.new, Binding Key:order.cancel
- Binding Key가 일치하지 않기 때문에 메시지가 전달되지 않습니다.

Binding Key, arguments를 사용하지 않고, 모든 Binding의 목적지로 메시지를 전달합니다(Broadcast).

Binding Key 패턴이 Routing Key와 매칭되는 Binding의 목적지에만 메시지를 전달합니다.
Routing Key:
order.new, Binding Key:order.*
- Binding Key 패턴이 Routing Key와 매칭되기 때문에 메시지가 전달됩니다.
Routing Key:
payment.new, Binding Key:order.*
- Binding Key 패턴이 Routing Key와 매칭되지 않기 때문에 메시지가 전달되지 않습니다.

메시지 Content Headers 내의 headers 테이블 내 속성과 arguments 내의 속성을 비교해 메시지를 전달할 지 결정합니다.
headers 테이블:
{ ”membership”: “VIP”, “foodType”: “PIZZA” }
arguments:{ ”membership”: “VIP”, “foodType”: “PIZZA”, "x-match": "all” }
- headers 테이블 내 속성과 arguments의 모든(
all이기 때문) 속성이 일치하기 때문에 메시지가 전달됩니다.headers 테이블:
{ “foodType”: “PIZZA” }
arguments:{ ”membership”: “VIP”, “foodType”: “PIZZA”, "x-match": "any” }
- headers 테이블 내 속성과 arguments의 하나 이상의(
any이기 때문) 속성이 일치하기 때문에 메시지가 전달됩니다.headers 테이블:
{ ”membership”: “VIP” }
arguments:{ ”membership”: “VIP”, “foodType”: “PIZZA”, "x-match": "all” }
- headers 테이블 내 속성과 arguments의 모든(
all이기 때문) 속성이 일치하지 않기 때문에 메시지가 전달되지 않습니다.

Binding은 Exchange가 수신한 메시지를 다음 목적지로 전달하기 위한 논리적인 연결 규칙입니다.
단순히 Exchange-Queue 형태로 연결하는 것을 넘어, Exchange-Exchange 형태로 메시지 라우팅 경로를 체이닝할 수 있습니다.
order.new, order.*x-match 속성은 사전 정의 속성으로, all과 any 값을 가질 수 있습니다.all: 메시지 Content Headers 내의 headers 테이블 내 속성이 전부 일치해야 메시지를 전달합니다.any: 메시지 Content Headers 내의 headers 테이블 내 속성이 하나라도 일치하면 메시지를 전달합니다.{ ”membership”: “VIP”, “foodType”: “PIZZA”, "x-match": "all” }Queue는 메시지가 최송 소비되기 전까지 보관되는 FIFO 버퍼입니다.
기본적으로 FIFO 구조이지만, 다중 소비자 환경이나 메시지 우선순위(Priority) 사용 시 엄격한 순서가 보장되지 않을 수 있습니다.
하나의 Queue에는 동일한 역할을 하는 여러 Consumer가 연결될 수 있습니다.
AMQP 모델에서 Queue는 단순한 저장소가 아니라 영리한 객체(Reasonably clever object)로 설계되었습니다.
메시지가 도착하면, Queue는 연결된 Consumer에게 즉시 메시지를 전달하려고 시도합니다.
다중 Consumer 존재 시, 일반적으로 라운드 로빈(Round-Robin) 방식으로 메시지를 분배합니다.
Broker로부터 메시지를 전달받아 소비하는 주체입니다.
Push받을 수 있는 메시지의 최대 허용량을 설정할 수 있습니다.
이를 통해 자신의 처리 역량에 맞춰 메시지 Push 속도를 조절함으로써, 부하를 방지합니다.
여러분들은 그동안 Spring AMQP와 같은 라이브러리를 사용하며 메서드 하나로 메시지를 아주 편리하게 주고받았습니다.
잘 만들어진 도구들이 복잡한 내부 사정을 우아하게 감추어주었기 때문입니다.
이 방대한 명세서를 한 페이지씩 공부하지 않아도 비즈니스 로직을 구현하고 서비스를 배포하는 데에는 별다른 문제가 없습니다.
사실, 저는 RabbitMQ를 사용/운영해 본 경험이 없습니다.
이 사실에 실망하셨나요? 혹은 "써보지도 않았으면서 원리를 논하나?"라는 의구심이 드시나요?
‘사용’이란 무엇일까요?
사용하다: (사람이 사물을) 어떤 목적이나 기능에 맞게 필요로 하거나 소용이 되는 곳에 쓰다.
출처: 네이버 백과사전
현대 개발 시대에서, 많은 개발자들은 자신이 작성한 메서드 아래에 숨어 있는 블랙박스에는 관심을 가지지 않고, ‘동작’하는 것에 관심을 가집니다.
이것이 나쁘다는 이야기는 아닙니다. 내부의 복잡한 구조를 모르더라도, 문제를 해결하기 위한 다양한 라이브러리 활용법을 아는 것도 훌륭한 능력입니다.
그러나, 저는 코드 한 줄을 작성하는 것보다는 동작 원리를 학습하는 것이 더 재미있어 보였습니다.
RabbitMQ를 사용해 본 경험이 없더라도, 저는 이제 메시지 브로커를 잘 활용할 자신이 있습니다.
긴 글 읽어주셔서 감사합니다.
참고 문서
https://www.amqp.org/specification/0-9-1/amqp-org-download
Advanced Message Queuing Protocol (AMQP) Protocol Specification, Version 0-9-1https://queue.acm.org/detail.cfm?id=1255424
Toward a Commodity Enterprise Middleware - John O'Hara, JPMorgan