RabbitMQ를 이해해보자, Exchange와 Queue가 헷갈렸던 이유 (2)

 

RabbitMQ를 처음 공부할 때 제일 헷갈렸던 부분은 Exchange였다.

처음에는 Producer가 Queue에 바로 메시지를 넣는다고 생각했다.

Producer → Queue → Consumer
 

개념을 단순하게 볼 때는 이 그림이 편하다.
그런데 RabbitMQ를 실제로 보면 중간에 Exchange가 있다.

Producer → Exchange → Queue → Consumer
 

처음에는 이게 왜 필요한지 잘 와닿지 않았다.

“그냥 Queue에 바로 넣으면 되는 거 아닌가?”
이 생각이 먼저 들었다.

그런데 메시지를 어디로 보낼지 나누기 시작하면 Exchange가 왜 있는지 조금씩 이해된다.

Producer는 메시지를 발행한다.
하지만 Producer가 모든 Queue를 직접 알고 있으면 구조가 복잡해진다.

예를 들어 주문이 생성됐을 때 여러 작업이 필요하다고 해보자.

주문 생성
  ↓
결제 처리
재고 차감
알림 발송
정산 처리
 

Producer가 Queue를 직접 알아야 한다면 이런 식이 된다.

Order Producer
  ↓
payment_queue
stock_queue
notification_queue
settlement_queue
 

이렇게 되면 Producer가 너무 많은 Queue를 알아야 한다.
나중에 새로운 Queue가 추가되면 Producer 코드도 바꿔야 할 수 있다.

Exchange를 두면 Producer는 Exchange에만 메시지를 보낸다.

Order Producer
  ↓
order_exchange
  ↓        ↓        ↓
payment_queue
stock_queue
notification_queue
 

Exchange는 메시지를 받아서 어떤 Queue로 보낼지 결정한다.
이때 필요한 개념이 Binding과 Routing Key다.

간단히 말하면 이렇다.

Exchange: 메시지를 받는 입구
Queue: 메시지가 쌓이는 곳
Binding: Exchange와 Queue를 연결하는 규칙
Routing Key: 메시지를 어디로 보낼지 판단하는 키
 

처음에는 용어가 많아서 어렵게 느껴진다.
그런데 실제 흐름으로 보면 조금 낫다.

Producer가 메시지를 보낸다.

message: 주문 생성됨
routing key: order.created
 

Exchange는 이 routing key를 보고 연결된 Queue 중 어디로 보낼지 판단한다.

order.created → order_created_queue
 

이게 기본 감각이다.

RabbitMQ에서 자주 쓰는 Exchange 타입은 direct, fanout, topic이다.
headers exchange도 있지만, 처음에는 앞의 세 개만 알아도 대부분의 구조를 이해하는 데 충분하다.

먼저 direct exchange부터 보면 된다.

Direct Exchange

direct exchange는 routing key가 정확히 맞는 Queue로 메시지를 보낸다.

예를 들어 exchange에 Queue가 이렇게 연결되어 있다고 해보자.

email_queue      binding key: email.send
sms_queue        binding key: sms.send
push_queue       binding key: push.send
 

Producer가 routing key를 email.send로 메시지를 보내면?

Producer
  ↓ routing key: email.send
Exchange
  ↓
email_queue
 

email_queue로만 메시지가 간다.

이 구조는 명확하다.
특정 작업을 특정 Queue로 보내고 싶을 때 쓰기 좋다.

예를 들면 이런 경우다.

email.send
sms.send
push.send
file.resize
thumbnail.create
 

키가 딱 맞는 Queue로 보내면 되니까 이해하기 쉽다.

다만 direct exchange는 패턴 기반으로 넓게 묶기에는 조금 제한적이다.

Fanout Exchange

fanout exchange는 조금 다르다.
routing key를 거의 신경 쓰지 않고, 연결된 Queue에 메시지를 전부 뿌린다.

Producer
  ↓
Fanout Exchange
  ↓       ↓       ↓
Queue A Queue B Queue C
 

이건 이벤트 브로드캐스트에 가깝다.

예를 들어 “회원가입 완료” 이벤트가 있다고 해보자.

user.registered
 

이 이벤트가 발생했을 때 여러 Consumer가 각자 해야 할 일이 있을 수 있다.

- 가입 환영 메일 발송
- 관리자 알림
- 쿠폰 지급
- 로그 저장
 

fanout exchange를 쓰면 하나의 메시지를 여러 Queue로 보낼 수 있다.

user_event_exchange
  ↓        ↓        ↓
mail_queue
coupon_queue
log_queue
 

처음에는 이 방식이 꽤 편해 보였다.
Producer는 이벤트만 발행하면 되고, 각 Consumer는 자기 Queue에서 메시지를 받아 처리하면 된다.

다만 fanout은 너무 넓게 퍼진다.
연결된 Queue에 전부 보내기 때문에, 세밀한 분기가 필요하면 direct나 topic을 보는 게 낫다.

Topic Exchange

topic exchange는 처음에 제일 헷갈렸는데, 막상 익숙해지면 가장 유연하다.

routing key를 패턴으로 매칭해서 Queue에 보낸다.

예를 들어 routing key를 이런 식으로 잡을 수 있다.

order.created
order.cancelled
payment.completed
payment.failed
user.registered
 

topic exchange에서는 *, # 같은 패턴을 쓴다.

*  : 단어 하나 매칭
#  : 여러 단어 매칭
 

예를 들어 binding key가 이렇다고 해보자.

order.*      → order 관련 이벤트 중 한 단계 패턴
payment.#    → payment로 시작하는 모든 이벤트
*.failed     → failed로 끝나는 이벤트
 

그러면 메시지가 이렇게 라우팅될 수 있다.

routing key: order.created
→ order.* 에 매칭

routing key: payment.card.failed
→ payment.# 에 매칭

routing key: payment.failed
→ payment.#, *.failed 둘 다 매칭 가능
 

topic exchange는 이벤트 기반 구조에서 꽤 자주 쓰기 좋다.

예를 들어 서비스 이벤트를 이런 식으로 정리할 수 있다.

user.created
user.deleted
order.created
order.cancelled
payment.failed
payment.completed
 

그러면 각 Consumer가 관심 있는 패턴만 구독하면 된다.

user.*        → 사용자 이벤트 처리
order.*       → 주문 이벤트 처리
*.failed      → 실패 이벤트 처리
 

이 구조는 확장성이 좋다.
나중에 이벤트가 추가돼도 기존 패턴에 맞으면 자연스럽게 라우팅된다.

다만 너무 자유롭게 쓰면 routing key 규칙이 지저분해질 수 있다.
처음부터 naming rule을 어느 정도 정해두는 게 좋다.

예를 들면 이런 식이다.

도메인.행위
user.created
order.cancelled
payment.failed
 

또는 조금 더 세분화하면:

서비스.도메인.행위
shop.order.created
shop.payment.failed
admin.user.deleted
 

정답은 없지만, 팀 안에서 규칙 없이 쓰기 시작하면 나중에 메시지 흐름을 추적하기가 힘들어진다.

Binding은 연결 규칙이다

Exchange와 Queue는 그냥 붙어 있는 게 아니다.
Binding이라는 연결 규칙이 있다.

Exchange -- binding key --> Queue
 

direct exchange에서는 binding key와 routing key가 정확히 맞아야 한다.

routing key: email.send
binding key: email.send
→ 전달됨
 

topic exchange에서는 패턴으로 맞는다.

routing key: order.created
binding key: order.*
→ 전달됨
 

fanout exchange에서는 binding key를 크게 신경 쓰지 않고 연결된 Queue로 전부 보낸다.

처음에는 Exchange, Queue, Routing Key, Binding이 따로 노는 개념처럼 보였다.
그런데 메시지 흐름으로 보면 결국 하나다.

Producer가 routing key와 함께 메시지를 보낸다.
Exchange는 binding 규칙을 본다.
규칙에 맞는 Queue로 메시지를 전달한다.
Consumer는 Queue에서 메시지를 가져간다.
 

이 흐름만 잡으면 RabbitMQ 구조가 훨씬 덜 헷갈린다.

어떤 Exchange를 써야 할까?

정리하면 이렇게 볼 수 있다.

direct
- 정확히 특정 Queue로 보내고 싶을 때
- email.send, sms.send 같은 명확한 작업에 적합

fanout
- 연결된 모든 Queue에 뿌리고 싶을 때
- 이벤트 브로드캐스트에 적합

topic
- 패턴 기반으로 유연하게 라우팅하고 싶을 때
- 도메인 이벤트 구조에 적합
 

실제로는 direct와 topic을 많이 보게 된다.
fanout은 이벤트를 여러 Consumer에게 그대로 뿌릴 때 쓰기 좋다.

예를 들어 단순 작업 큐라면 direct가 편하다.

thumbnail.create → thumbnail_queue
email.send → email_queue
 

도메인 이벤트를 여러 관심사로 나누려면 topic이 편하다.

order.created
order.cancelled
payment.failed
 

전체 이벤트를 여러 시스템에 동시에 알리고 싶다면 fanout도 괜찮다.

user_registered_exchange
  ↓       ↓       ↓
mail    coupon   log
 

처음 RabbitMQ를 볼 때는 Queue만 보면 될 줄 알았다.
그런데 실제로는 Exchange를 어떻게 잡느냐가 메시지 구조를 많이 좌우한다.

Queue는 메시지가 쌓이는 곳이다.
Exchange는 메시지가 어디로 갈지 결정하는 입구다.

이 차이를 알고 나면 RabbitMQ 설정을 볼 때 조금 덜 막힌다.

Producer
  ↓
Exchange
  ↓
Queue
  ↓
Consumer
 

그리고 이 중간에서 routing key와 binding이 메시지의 길을 정한다.


다음으로 봐야 할 건 “Consumer가 메시지를 가져간 뒤 실패하면 어떻게 되는가”다.

RabbitMQ는 메시지를 보내는 것보다, 실패했을 때 어떻게 처리할지가 더 중요해진다.
그 부분에서 ack, nack, retry, DLQ 같은 개념이 나온다.

작업 기록과 샘플 코드는 GitHub에도 정리해두고 있어요.

GitHub 팔로우

Continue Reading

이전 글 / 다음 글