교환기 유형: Direct, Topic, Fanout 및 Headers
적절한 교환기와 바인딩 전략을 선택해 메시지를 올바른 소비자에게 라우팅합니다.
교환기 유형: Direct, Topic, Fanout 및 Headers은(는) CoddyKit의 무료 Node.js Backend Development Bootcamp 강의입니다. 이것은 4개 중 2번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 Node.js Backend Development Bootcamp 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. Node.js Backend Development Bootcamp 강의에는 총 4개의 강의가 포함되어 있습니다.
이 강의의 일부는 아직 번역되지 않았으며 영어로 표시됩니다.
Why Exchanges Exist
In RabbitMQ, producers never publish directly to a queue. They publish to an exchange, and the exchange decides which queue(s) receive the message based on bindings and a routing key.
This indirection is the core of event-driven routing. To get messages to the right consumers you must choose the correct exchange type:
direct— exact routing-key matchtopic— pattern match with wildcardsfanout— broadcast to all bound queuesheaders— match on message header attributes
Picking the wrong one means consumers miss messages or get flooded with irrelevant ones.
The Mental Model: Exchange + Binding + Routing Key
Three pieces work together:
- Routing key: a string the producer attaches to each message (e.g.
order.created). - Binding: a rule connecting an exchange to a queue, often with a binding key.
- Exchange type: the algorithm that compares the routing key against the bindings.
The exchange evaluates every binding. A message can land in zero, one, or many queues. If it matches no binding, it is dropped (unless an alternate exchange is configured).
We'll use the amqplib library throughout these examples.
// Establishing a channel with amqplib
const amqp = require('amqplib');
async function connect() {
const conn = await amqp.connect('amqp://localhost');
const channel = await conn.createChannel();
return { conn, channel };
}
module.exports = { connect };Direct Exchange: Exact Match
A direct exchange delivers a message to queues whose binding key exactly equals the message's routing key.
It is the go-to choice when each message type maps to a specific worker. Classic use case: routing log messages by severity (error, warning, info) to dedicated queues.
- Routing key
error→ only the queue bound witherror. - Multiple queues can share the same binding key — all of them receive a copy.
const amqp = require('amqplib');
async function setupDirect(channel) {
const ex = 'logs_direct';
await channel.assertExchange(ex, 'direct', { durable: true });
const errors = await channel.assertQueue('errors', { durable: true });
await channel.bindQueue(errors.queue, ex, 'error');
const allLogs = await channel.assertQueue('all_logs', { durable: true });
await channel.bindQueue(allLogs.queue, ex, 'error');
await channel.bindQueue(allLogs.queue, ex, 'warning');
await channel.bindQueue(allLogs.queue, ex, 'info');
}
module.exports = { setupDirect };Publishing to a Direct Exchange
The producer passes the routing key as the second argument to channel.publish(exchange, routingKey, content). The exchange does the rest.
Here a message with routing key error reaches both the errors queue and the all_logs queue from the previous scene, while an info message only reaches all_logs.
Notice the body must be a Buffer.
async function publishLog(channel, severity, message) {
const ex = 'logs_direct';
channel.publish(
ex,
severity, // routing key: 'error' | 'warning' | 'info'
Buffer.from(JSON.stringify({ message, ts: Date.now() })),
{ persistent: true }
);
console.log(`Sent [${severity}] ${message}`);
}
// publishLog(channel, 'error', 'DB connection lost');
module.exports = { publishLog };Topic Exchange: Pattern Matching
A topic exchange matches routing keys against binding patterns using two wildcards:
*(star) matches exactly one word.#(hash) matches zero or more words.
Routing keys are dot-delimited words, e.g. order.eu.created. This is the most flexible exchange and is ideal for hierarchical event names.
order.*.createdmatchesorder.eu.createdbut notorder.created.order.#matchesorder.created,order.eu.shipped, etc.*.eu.*matches any region-EU event with three words.
Setting Up Topic Bindings
Consider an order pipeline. Different services care about different slices of the event stream:
- An audit service wants everything:
order.#. - A EU compliance service wants only EU events:
order.eu.*. - A shipping service wants any region's shipped event:
order.*.shipped.
One published event can satisfy several patterns at once, fanning out only to the interested consumers.
const amqp = require('amqplib');
async function setupTopic(channel) {
const ex = 'orders_topic';
await channel.assertExchange(ex, 'topic', { durable: true });
const audit = await channel.assertQueue('audit', { durable: true });
await channel.bindQueue(audit.queue, ex, 'order.#');
const euCompliance = await channel.assertQueue('eu_compliance', { durable: true });
await channel.bindQueue(euCompliance.queue, ex, 'order.eu.*');
const shipping = await channel.assertQueue('shipping', { durable: true });
await channel.bindQueue(shipping.queue, ex, 'order.*.shipped');
}
module.exports = { setupTopic };Reasoning About Topic Matches
Let's trace which queues receive each event given the bindings order.# (audit), order.eu.* (EU), order.*.shipped (shipping).
order.eu.created→ audit + EUorder.us.shipped→ audit + shippingorder.eu.shipped→ audit + EU + shippingorder.created→ audit only (only one word afterorder, soorder.eu.*andorder.*.shippeddon't match)
This pure-JavaScript helper mimics the topic matching algorithm so you can verify your patterns offline.
function topicMatch(pattern, key) {
const p = pattern.split('.');
const k = key.split('.');
function rec(pi, ki) {
if (pi === p.length) return ki === k.length;
if (p[pi] === '#') {
for (let skip = ki; skip <= k.length; skip++) {
if (rec(pi + 1, skip)) return true;
}
return false;
}
if (ki < k.length && (p[pi] === '*' || p[pi] === k[ki])) {
return rec(pi + 1, ki + 1);
}
return false;
}
return rec(0, 0);
}
const keys = ['order.eu.created', 'order.us.shipped', 'order.eu.shipped', 'order.created'];
for (const key of keys) {
console.log(key, {
audit: topicMatch('order.#', key),
eu: topicMatch('order.eu.*', key),
shipping: topicMatch('order.*.shipped', key),
});
}Fanout Exchange: Broadcast
A fanout exchange ignores the routing key entirely and delivers every message to all bound queues. It is the simplest and fastest exchange.
Use it for broadcast scenarios: cache invalidation across all app instances, real-time notifications, or pushing updates to many WebSocket gateways.
Each consumer typically declares its own exclusive, auto-deleting queue so it gets a private copy of the broadcast.
const amqp = require('amqplib');
async function subscribeBroadcast(channel, onMessage) {
const ex = 'cache_invalidations';
await channel.assertExchange(ex, 'fanout', { durable: true });
// Exclusive, server-named queue: unique per consumer, auto-deleted on disconnect
const q = await channel.assertQueue('', { exclusive: true });
await channel.bindQueue(q.queue, ex, ''); // routing key ignored for fanout
await channel.consume(q.queue, (msg) => {
if (msg) {
onMessage(JSON.parse(msg.content.toString()));
channel.ack(msg);
}
});
}
module.exports = { subscribeBroadcast };Headers Exchange: Match on Attributes
A headers exchange ignores the routing key and instead matches on the message's headers (key/value pairs). Bindings specify the headers to match plus a special x-match argument:
x-match: all— every specified header must match (AND).x-match: any— at least one specified header must match (OR).
This is useful when routing depends on multiple independent dimensions (e.g. format and region) that don't compose cleanly into a single dotted key.
const amqp = require('amqplib');
async function setupHeaders(channel) {
const ex = 'reports_headers';
await channel.assertExchange(ex, 'headers', { durable: true });
const pdfEu = await channel.assertQueue('pdf_eu', { durable: true });
await channel.bindQueue(pdfEu.queue, ex, '', {
'x-match': 'all',
format: 'pdf',
region: 'eu',
});
const anyCsv = await channel.assertQueue('any_csv', { durable: true });
await channel.bindQueue(anyCsv.queue, ex, '', {
'x-match': 'any',
format: 'csv',
priority: 'high',
});
}
module.exports = { setupHeaders };Publishing with Headers
The producer attaches header values via the headers property of the publish options. The empty routing key is conventional for headers exchanges since it is unused.
Given the bindings from the previous scene, a message with { format: 'pdf', region: 'eu' } reaches pdf_eu. A message with { format: 'csv', region: 'us' } reaches any_csv (because x-match: any matched on format).
async function publishReport(channel, payload, headers) {
const ex = 'reports_headers';
channel.publish(
ex,
'', // routing key unused for headers exchange
Buffer.from(JSON.stringify(payload)),
{ headers, persistent: true }
);
}
// publishReport(channel, { id: 42 }, { format: 'pdf', region: 'eu' });
// publishReport(channel, { id: 43 }, { format: 'csv', region: 'us' });
module.exports = { publishReport };Choosing the Right Exchange
A practical decision guide:
- Direct — you have a fixed set of categories and want exact routing (severity levels, task types).
- Topic — event names are hierarchical and consumers subscribe to flexible patterns. The most common choice for microservice event buses.
- Fanout — every consumer must see every message (broadcast, cache busting).
- Headers — routing depends on multiple non-hierarchical attributes, or you need AND/OR logic across dimensions.
Rule of thumb: reach for topic by default in event-driven systems; it subsumes direct (a pattern with no wildcards behaves like direct) and is far more extensible than fanout.
Quick Check
Test your understanding of exchange selection.
Recap
You learned how RabbitMQ routes messages through exchanges and bindings:
- Producers publish to exchanges, never directly to queues; the exchange type is the routing algorithm.
- Direct — exact routing-key match for fixed categories.
- Topic — wildcard pattern match (
*= one word,#= zero or more) for hierarchical events; the default pick for event buses. - Fanout — broadcast to all bound queues, ignoring the routing key.
- Headers — match on header attributes with
x-match: all(AND) orany(OR).
Always declare the exchange and queues with assertExchange/assertQueue and connect them with bindQueue. Choose the exchange that matches how your consumers need to subscribe, and you'll route the right messages to the right places with minimal coupling.
AI 튜터와 함께 JavaScript을(를) 배우세요 — 무료
브라우저에서 실제 코드를 작성하고 실행하며, 24/7 AI 튜터로부터 즉각적인 도움을 받고, 웹이나 앱에서 중단한 부분부터 계속 학습하세요.
- 코스
- 22
- 레슨
- 92
자주 묻는 질문
“교환기 유형: Direct, Topic, Fanout 및 Headers” 강의는 무료인가요?
네 — “교환기 유형: Direct, Topic, Fanout 및 Headers” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 Node.js Backend Development Bootcamp 강의 전체를 잠금 해제할 수 있습니다. Node.js Backend Development Bootcamp 강의에는 총 4개의 강의가 포함되어 있습니다.
“교환기 유형: Direct, Topic, Fanout 및 Headers”에서 뭘 배우나요?
적절한 교환기와 바인딩 전략을 선택해 메시지를 올바른 소비자에게 라우팅합니다. 브라우저에서 직접 실행하는 실습 코드로 Node.js Backend Development Bootcamp을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.
Node.js Backend Development Bootcamp을(를) 시작하는 데 경험이 필요한가요?
사전 경험은 필요하지 않습니다. CoddyKit의 Node.js Backend Development Bootcamp은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 2번째 강의입니다.
“교환기 유형: Direct, Topic, Fanout 및 Headers” 강의는 얼마나 걸리나요?
대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.
이 Node.js Backend Development Bootcamp 강의에서 코드를 작성하고 실행할 수 있나요?
네. 모든 Node.js Backend Development Bootcamp 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.
이 강의의 모든 강의
- 생산자, 소비자 및 AMQP 모델
- 교환기 유형: Direct, Topic, Fanout 및 Headers
- 확인 응답, 데드 레터 큐 및 재시도
- 작업 큐, 프리페치 및 경쟁 소비자