에듀테크 프로젝트 대시보드
메시지 큐 & 발행–구독
보내는 쪽과 받는 쪽 사이에 "우체통"을 하나 두면, 두 서비스는 서로가 켜져 있는지 몰라도 되고 속도가 달라도 터지지 않는다
쇼핑몰에서 "주문하기"를 누르면 결제, 재고 차감, 영수증 메일, 배송사 연동, 추천 학습용 로그 적재까지 여러 일이 벌어진다.
이걸 전부 한 요청 안에서 순서대로 호출하면 — 메일 서버가 느리면 주문 버튼이 5초간 멈추고, 배송사 API가 죽으면 주문 자체가 실패한다.
대신 주문 서비스는 OrderPlaced 메시지 하나만 브로커에 던지고 즉시 응답한다. 나머지 서비스들은 그 메시지를
각자 속도로 처리한다.
메시지 하나는 소비자 하나만 가져간다. 여러 소비자를 붙이면 일이 나눠진다(경쟁 소비자). 처리량을 늘리려면 소비자만 추가.
메시지 하나가 구독한 모든 소비자에게 복사되어 간다. 각 구독자는 자기 큐(오프셋)를 따로 갖는다. "이벤트를 알림"에 적합.
생산 속도를 소비 능력보다 높이면 큐가 차오르다가 가득 차면 메시지가 버려진다(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)
네트워크는 언제든 끊길 수 있으므로 "정확히 한 번"은 공짜가 아니다. 보통 이 셋 중 하나를 고른다.
| 수준 | 동작 | 위험 / 대가 | 쓰는 곳 |
|---|---|---|---|
| at-most-once | 보내고 잊음. ack 안 기다림 | 유실 가능 | 메트릭·로그처럼 몇 개 빠져도 되는 것 |
| at-least-once | ack 못 받으면 재전송 | 중복 도착 가능 → 소비자가 멱등해야 함 | 대부분의 실무 (가장 흔함) |
| exactly-once | 중복 제거 + 트랜잭션 | 느리고 복잡, 범위 제한적 | 정산·결제 등 중복이 치명적인 것 |
| 도구 | 모델 | 특징 |
|---|---|---|
| RabbitMQ | 큐 + 익스체인지 라우팅 | 유연한 라우팅 규칙, 메시지 단위 ack, 중소 규모에 편함 |
| Apache Kafka | 로그(파티션) + 오프셋 | 메시지를 지우지 않고 보관, 소비자가 오프셋을 되감아 재처리, 초고처리량 |
| Redis Streams / Pub-Sub | 인메모리 스트림 | 매우 빠름, 가벼움, 이미 Redis를 쓰면 추가 인프라 불필요 |
| AWS SQS / SNS | 관리형 큐 / 토픽 | 서버 관리 없음, SNS(팬아웃) + SQS(작업 큐) 조합 |