한양과학기술고등학교

에듀테크 프로젝트 대시보드

메시지 큐 & 발행–구독

메시지 큐 & 발행–구독

보내는 쪽과 받는 쪽 사이에 "우체통"을 하나 두면, 두 서비스는 서로가 켜져 있는지 몰라도 되고 속도가 달라도 터지지 않는다

한 줄 요약 — 서비스 A가 B를 직접 호출하는 대신, A는 브로커에 메시지를 넣고(produce) B는 준비됐을 때 꺼내 간다(consume). 이 한 겹 덕분에 결합도↓ · 버퍼링 · 부하 평탄화 · 재시도가 공짜로 생긴다.

왜 중간에 큐를 둘까?

쇼핑몰에서 "주문하기"를 누르면 결제, 재고 차감, 영수증 메일, 배송사 연동, 추천 학습용 로그 적재까지 여러 일이 벌어진다. 이걸 전부 한 요청 안에서 순서대로 호출하면 — 메일 서버가 느리면 주문 버튼이 5초간 멈추고, 배송사 API가 죽으면 주문 자체가 실패한다. 대신 주문 서비스는 OrderPlaced 메시지 하나만 브로커에 던지고 즉시 응답한다. 나머지 서비스들은 그 메시지를 각자 속도로 처리한다.

두 가지 전달 방식

① 작업 큐 (Point-to-Point)

메시지 하나는 소비자 하나만 가져간다. 여러 소비자를 붙이면 일이 나눠진다(경쟁 소비자). 처리량을 늘리려면 소비자만 추가.

P ─▶ [ ■ ■ ■ ■ ] ─▶ C1
            └──▶ C2
            └──▶ C3

② 발행–구독 (Pub/Sub · Fan-out)

메시지 하나가 구독한 모든 소비자에게 복사되어 간다. 각 구독자는 자기 큐(오프셋)를 따로 갖는다. "이벤트를 알림"에 적합.

P ─▶ (topic) ─┬─▶ [■■] ─▶ 결제
            ├─▶ [■■] ─▶ 메일
            └─▶ [■■] ─▶ 분석

직접 만져 보기 — 생산자·브로커·소비자 시뮬레이터

생산 속도 4/s
소비자 수 2
처리 시간 900ms
큐 용량 8
생산됨 (produced)
0
소비됨 (consumed)
0
큐에 대기 중
0
버려짐 (큐 가득참)
0

생산 속도를 소비 능력보다 높이면 큐가 차오르다가 가득 차면 메시지가 버려진다(backpressure). 소비자를 늘리거나 처리 시간을 줄이면 다시 안정된다. 발행–구독 모드에서는 메시지 하나가 모든 소비자에게 복사되므로 "소비됨"이 생산량의 배수로 늘어난다.

아주 작은 브로커 구현

핵심만 남기면 브로커는 "토픽별 리스트 + 구독자 목록"이다. 실무 브로커는 여기에 영속화, ack/재시도, 오프셋, 파티션이 붙는다.

class Broker:
    def __init__(self):
        self.topics = {}          # topic -> [메시지 대기열]
        self.subscribers = {}     # topic -> [소비자 큐]

    def subscribe(self, topic):
        q = Queue()
        self.subscribers.setdefault(topic, []).append(q)
        return q                 # 구독자는 자기 큐를 받는다

    def publish(self, topic, msg):
        for q in self.subscribers.get(topic, []):
            q.put(msg)          # fan-out: 모든 구독자에게 복사

# 생산자 — B가 켜져 있든 말든 신경 안 씀
broker.publish("order.placed", {"id": 42, "total": 19900})

# 소비자 — 자기 속도로 하나씩 꺼내 처리하고 ack
while True:
    msg = mailbox.get()          # 없으면 블로킹(대기)
    send_receipt_email(msg)
    mailbox.task_done()          # 처리 완료 통지(ack)

전달 보장 (delivery guarantee)

네트워크는 언제든 끊길 수 있으므로 "정확히 한 번"은 공짜가 아니다. 보통 이 셋 중 하나를 고른다.

수준동작위험 / 대가쓰는 곳
at-most-once보내고 잊음. ack 안 기다림유실 가능메트릭·로그처럼 몇 개 빠져도 되는 것
at-least-onceack 못 받으면 재전송중복 도착 가능 → 소비자가 멱등해야 함대부분의 실무 (가장 흔함)
exactly-once중복 제거 + 트랜잭션느리고 복잡, 범위 제한적정산·결제 등 중복이 치명적인 것
멱등성(idempotency) — 같은 메시지를 두 번 처리해도 결과가 같게 만들면 at-least-once로 충분하다. 예: "잔액 -1000" 대신 "주문 42의 결제 상태 = 완료"처럼 결과 상태를 지정하거나, 처리한 메시지 ID를 기록해 두고 중복을 건너뛴다.

어떤 브로커가 있나

도구모델특징
RabbitMQ큐 + 익스체인지 라우팅유연한 라우팅 규칙, 메시지 단위 ack, 중소 규모에 편함
Apache Kafka로그(파티션) + 오프셋메시지를 지우지 않고 보관, 소비자가 오프셋을 되감아 재처리, 초고처리량
Redis Streams / Pub-Sub인메모리 스트림매우 빠름, 가벼움, 이미 Redis를 쓰면 추가 인프라 불필요
AWS SQS / SNS관리형 큐 / 토픽서버 관리 없음, SNS(팬아웃) + SQS(작업 큐) 조합

정리