개인의 기록
  • 소개
  • 프로젝트
  • 글
  • 링크

© 2026 newgirok

← 글 목록

Message Queue — 서비스 간 비동기 메시지를 처리하는 큐

2025년 1월 1일
MQMessage QueueProducerConsumerDead Letter Queue

두 서비스가 직접 통신하면 한쪽이 느려질 때 다른 쪽도 멈춘다. 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: 큐에서 메시지를 꺼내 처리하는 주체.

Ack와 Retry

Consumer가 메시지를 꺼낸 시점에 처리가 보장되지는 않는다. 큐는 Consumer가 Ack(Acknowledgement)를 보낼 때까지 메시지를 보유한다. 처리 도중 Consumer가 죽으면 Ack가 오지 않으므로 큐는 메시지를 다시 전달한다.

Consumer 처리 성공:  consume → 처리 → ack()  → 큐에서 삭제
Consumer 처리 실패:  consume → 처리 → nack() → 큐로 반환 → 재전달(Retry)

Ack: 메시지를 정상 처리했음을 큐에 알리는 확인 신호. nack: 처리 실패를 알려 재전달을 요청하는 신호.

Retry 횟수는 큐 설정으로 제한한다. 무한 재시도는 Consumer를 고착시키고 다른 메시지 처리를 막는다.

Dead Letter Queue (DLQ)

Retry 한도를 초과한 메시지는 Dead Letter Queue(DLQ)로 이동한다. 본 큐에서 격리해 정상 흐름을 보호하는 안전망이다.

+-------+    retry 초과    +---------+
| Queue | --------------> |   DLQ   |
+-------+                 +---------+
                               |
                         수동 검토 / 알림

DLQ(Dead Letter Queue): 반복 실패한 메시지를 별도 보관하는 큐. 운영자가 원인을 분석하고 재처리 여부를 결정한다.

DLQ 없이 실패 메시지를 폐기하면 데이터 손실이 생긴다. DLQ가 쌓이는 속도를 모니터링하면 Consumer 버그나 외부 의존성 장애를 빠르게 감지할 수 있다.

구현 예시 (TypeScript)

// 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 형태는 다르지만 이 패턴은 동일하다.

언제 쓸까

  • 처리 시간이 길어 응답 지연이 허용되지 않는 작업 (이미지 리사이징, PDF 생성)
  • 트래픽 급증 시 Consumer 수를 늘려 처리량을 수평 확장해야 할 때
  • 하나의 이벤트를 여러 Consumer가 독립적으로 처리해야 할 때 (이메일 + 슬랙 + 로그 동시 처리)

직접 호출이 명확하고 즉각적인 응답이 필요한 경우라면 동기 통신이 더 단순하다. Message Queue는 "지금 당장 처리 안 해도 되는" 작업에만 도입하는 것이 옳다.

← 이전 글Caching Strategy — 자주 쓰는 데이터를 임시 저장하는 전략
다음 글 →Event Driven Architecture — 이벤트 발생을 중심으로 서비스가 협력하는 방식