두 서비스가 직접 통신하면 한쪽이 느려질 때 다른 쪽도 멈춘다. Message Queue는 그 사이에 버퍼를 두어 각 서비스가 자기 속도로 동작할 수 있게 한다. 요청을 즉시 처리하지 않아도 되는 작업 — 이메일 발송, 이미지 변환, 알림 — 에 특히 적합하다.
동기 통신에서는 호출자가 응답을 받을 때까지 블로킹된다. Message Queue를 쓰면 Producer는 메시지를 큐에 넣고 즉시 반환하며, Consumer는 준비됐을 때 꺼내 처리한다.
| 항목 | 동기 (HTTP/gRPC) | 비동기 (Message Queue) |
|---|---|---|
| 응답 대기 | 필수 | 불필요 |
| 결합도 | 강함 | 느슨함 |
| 장애 전파 | 즉시 전파 | 큐가 완충 |
| 처리량 제어 | 어려움 | Consumer 수로 조절 |
[Producer]
|
| publish(message)
v
+----------+
| Queue | <-- 메시지 영속 저장
+----------+
|
| consume(message)
v
[Consumer]
|
| ack() / nack()
v
처리 완료 / 재시도
Producer는 메시지를 큐에 넣기만 하고 Consumer의 상태를 알 필요가 없다. Consumer는 자신이 처리할 수 있는 속도로 메시지를 가져간다.
Producer: 메시지를 생성해 큐에 발행하는 주체. Consumer: 큐에서 메시지를 꺼내 처리하는 주체.
Consumer가 메시지를 꺼낸 시점에 처리가 보장되지는 않는다. 큐는 Consumer가 Ack(Acknowledgement)를 보낼 때까지 메시지를 보유한다. 처리 도중 Consumer가 죽으면 Ack가 오지 않으므로 큐는 메시지를 다시 전달한다.
Consumer 처리 성공: consume → 처리 → ack() → 큐에서 삭제
Consumer 처리 실패: consume → 처리 → nack() → 큐로 반환 → 재전달(Retry)
Ack: 메시지를 정상 처리했음을 큐에 알리는 확인 신호. nack: 처리 실패를 알려 재전달을 요청하는 신호.
Retry 횟수는 큐 설정으로 제한한다. 무한 재시도는 Consumer를 고착시키고 다른 메시지 처리를 막는다.
Retry 한도를 초과한 메시지는 Dead Letter Queue(DLQ)로 이동한다. 본 큐에서 격리해 정상 흐름을 보호하는 안전망이다.
+-------+ retry 초과 +---------+
| Queue | --------------> | DLQ |
+-------+ +---------+
|
수동 검토 / 알림
DLQ(Dead Letter Queue): 반복 실패한 메시지를 별도 보관하는 큐. 운영자가 원인을 분석하고 재처리 여부를 결정한다.
DLQ 없이 실패 메시지를 폐기하면 데이터 손실이 생긴다. DLQ가 쌓이는 속도를 모니터링하면 Consumer 버그나 외부 의존성 장애를 빠르게 감지할 수 있다.
// Producer
await queue.publish({
type: "email.send",
payload: { to: "user@example.com", subject: "Welcome" },
});
// Consumer
queue.subscribe("email.send", async (message, ack, nack) => {
try {
await sendEmail(message.payload);
ack();
} catch (err) {
nack(); // 재시도 요청
}
});
Consumer는 처리 결과에 따라 ack 또는 nack을 호출한다. 큐 라이브러리(RabbitMQ, AWS SQS, Kafka 등)마다 API 형태는 다르지만 이 패턴은 동일하다.
직접 호출이 명확하고 즉각적인 응답이 필요한 경우라면 동기 통신이 더 단순하다. Message Queue는 "지금 당장 처리 안 해도 되는" 작업에만 도입하는 것이 옳다.