Go로 구현하는 RabbitMQ 토픽 설계 완전 정리

Published by

on


1. Ordered Topic — 순서가 중요한 업무 흐름

RabbitMQ는 큐 단위로만 순서를 보장한다.
즉, 메시지를 하나의 큐에서만 소비하면 순서가 유지되고, 여러 큐나 병렬 소비가 일어나면 순서가 깨진다.

이 구조는 결제, 정산, 배송처럼 순차적 업무 흐름이 있는 서비스에서 사용된다.

conn := rb.MustConn("amqp://guest:guest@localhost:5672/")
defer conn.Close()
ch := rb.MustChan(conn)
defer ch.Close()

_ = ch.ExchangeDeclare("order.events", "topic", true, false, false, false, nil)
q, _ := ch.QueueDeclare("order-seq-1001", true, false, false, false, nil)
_ = ch.QueueBind(q.Name, "order.#.1001", "order.events", false, nil)
_ = ch.Qos(1, 0, false)

msgs, _ := ch.Consume(q.Name, "", false, false, false, false, nil)
for m := range msgs {
  log.Printf("Processing: %s", m.Body)
  m.Ack(false)
}

적용 사례

  • 결제 → 송장 → 정산 순차 처리
  • 주문 단위 트랜잭션 관리
  • 재고 감소 → 재고 확정 순서 보존

비즈니스 로직의 순서성이 데이터 정합성과 직접 연결되는 경우에 사용된다.


2. Unordered Topic — 처리량이 우선인 구조

여러 소비자가 동일 큐를 병렬로 구독하면 메시지의 순서는 보장되지 않는다.
이 대신 처리량이 크게 증가한다.

이 구조는 로그, 이벤트, 실시간 트래픽 분석처럼 순서보다 처리 속도와 확장성이 중요한 서비스에서 사용된다.

_ = ch.ExchangeDeclare("log.events", "topic", true, false, false, false, nil)
q, _ := ch.QueueDeclare("all-logs", true, false, false, false, nil)
_ = ch.QueueBind(q.Name, "#", "log.events", false, nil)

for i := 0; i < 8; i++ {
  go func(id int) {
    msgs, _ := ch.Consume(q.Name, "", true, false, false, false, nil)
    for m := range msgs {
      log.Printf("[C%d] %s", id, m.Body)
    }
  }(i)
}

select {}

적용 사례

  • 로그 수집 시스템
  • 행동 로그 기반 추천 시스템
  • IoT 데이터 적재

비즈니스 로직이 대규모 데이터를 병렬로 처리하는 구조일 때 사용된다.


3. Generic Routing Topic — 유연한 라우팅 구조

Topic Exchange의 가장 큰 장점은 유연한 라우팅이다.
.으로 구분된 계층 구조와 *, # 패턴 매칭을 통해 서비스 단위로 메시지를 분배할 수 있다.

_ = ch.ExchangeDeclare("app.events", "topic", true, false, false, false, nil)

userQ, _ := ch.QueueDeclare("user-service", true, false, false, false, nil)
orderQ, _ := ch.QueueDeclare("order-service", true, false, false, false, nil)
anaQ,  _ := ch.QueueDeclare("analytics", true, false, false, false, nil)

_ = ch.QueueBind(userQ.Name, "user.#", "app.events", false, nil)
_ = ch.QueueBind(orderQ.Name, "order.#", "app.events", false, nil)
_ = ch.QueueBind(anaQ.Name, "#", "app.events", false, nil)

rb.PublishJSON(ch, "app.events", "user.created", []byte(`{"id":1}`))
rb.PublishJSON(ch, "app.events", "order.shipped.us", []byte(`{"orderId":77}`))

적용 사례

  • 마이크로서비스 간 이벤트 버스
  • IoT 센서 데이터 라우팅 (sensor.region.device)
  • 멀티리전 동기화 (db.updated.us, db.updated.eu)

비즈니스 로직이 서비스 단위로 분리되어 있고, 구독 구조가 유연해야 하는 경우에 사용된다.


4. Static Routing — 정확 일치 기반의 안정적 전달

Direct Exchange는 라우팅 키가 정확히 일치할 때만 메시지를 전달한다.
정해진 채널로만 메시지를 보내야 하는 경우에 사용된다.

_ = ch.ExchangeDeclare("system.direct", "direct", true, false, false, false, nil)
errQ, _ := ch.QueueDeclare("error-log", true, false, false, false, nil)
_ = ch.QueueBind(errQ.Name, "error", "system.direct", false, nil)

rb.PublishJSON(ch, "system.direct", "error", []byte(`{"msg":"fatal"}`))

적용 사례

  • 운영팀 전용 알림
  • 보안 이벤트 로그
  • 시스템 단일 목적 메시징

비즈니스 로직이 정확히 지정된 경로로만 전달되어야 하는 경우에 사용된다.


5. Hybrid (Partitioned Topic) — 순서와 확장성의 절충

Kafka의 파티션 개념과 비슷한 방식이다.
ID 기반으로 라우팅 키를 구성하면, 각 ID 단위로는 순서가 보장되면서 시스템 전체는 병렬로 확장된다.

for _, id := range []int{1001, 1002, 1003} {
  rk := fmt.Sprintf("order.created.%d", id)
  rb.PublishJSON(ch, "order.events", rk, []byte(fmt.Sprintf(`{"orderId":%d}`, id)))
}

적용 사례

  • 주문 단위 이벤트 처리
  • 사용자 단위 데이터 파이프라인
  • 배송 추적 및 상태 변경

비즈니스 로직이 엔티티 단위(주문, 사용자, 배송 등)로 분리 가능할 때 사용된다.


6. 토픽 설계 기준 정리

비즈니스 로직권장 패턴이유
결제, 정산, 배송Ordered순서성이 정합성에 직접 영향
로그, 이벤트 수집Unordered순서보다 처리량 우선
마이크로서비스 이벤트 버스Generic라우팅 유연성 필요
운영, 보안 알림Static정확한 라우팅 필요
주문별 순서 + 전체 통계HybridID 단위 순서 + 병렬 확장

RabbitMQ에서 토픽 설계는 단순히 키를 어떻게 자르느냐의 문제가 아니다.
토픽의 종류는 시스템이 추구하는 목표,
순서의 정합성인지, 처리량의 극대화인지, 라우팅의 유연성인지에 따라 달라진다.

결국 토픽 설계는
비즈니스 로직의 성격에 따라 달라진다.

댓글 남기기

오픈바이브 | oftenvibe.com에서 더 알아보기

지금 구독하여 계속 읽고 전체 아카이브에 액세스하세요.

계속 읽기