콘텐츠 대표 이미지 - NSQ: 분산형 실시간 메시징 플랫폼 완전 정복 가이드
🚀 DB / 서버 · 프로그램개발

NSQ: 분산형 실시간 메시징 플랫폼 완전 정복 가이드

⚡ Go 언어로 만들어진 초고속 메시지 큐, NSQ의 구조부터 실전 구현까지 친구처럼 쉽게 파헤쳐보자!

야, 솔직히 말해봐. 🤔 서버 개발하다가 이런 상황 한 번쯤 겪어봤지?

"주문이 들어오면 재고 시스템도 업데이트하고, 알림 서비스도 보내고, 로그도 남겨야 하는데... 이걸 다 동기로 처리하면 응답이 너무 느려지잖아!"

바로 이럴 때 필요한 게 메시지 큐(Message Queue)야. 그리고 오늘 우리가 파헤칠 주인공은 바로 NSQ! 🎯
NSQ는 Go 언어로 만들어진 분산형 실시간 메시징 플랫폼인데, 설정이 간단하고 성능이 미쳐있어서 스타트업부터 대기업까지 널리 쓰이고 있어.
Bitly가 처음 만들었고, 지금은 오픈소스로 공개되어 있지. 같이 처음부터 끝까지 제대로 파보자! 💪

NSQ 핵심 특징 한눈에 보기 NSQ 메시징 플랫폼 ⚡ 초고속 처리 초당 수백만 메시지 🔗 분산 구조 단일 장애점 없음 🛡️ 메시지 보장 최소 1회 전달 보장 🔧 간단한 설정 설정 파일 최소화 🐹 Go 언어로 개발 · 오픈소스 Bitly 제작 · GitHub 공개

1. NSQ가 뭔데? 기본 개념부터 잡자 🎓

NSQ(엔에스큐)는 Go 언어로 작성된 분산형 실시간 메시지 큐 시스템이야.
2012년 Bitly(URL 단축 서비스로 유명한 회사)에서 처음 개발했고, 현재는 오픈소스로 GitHub에 공개되어 있어.
공식 GitHub 주소는 github.com/nsqio/nsq야.

💡 메시지 큐가 뭔지 모르는 친구들을 위해!
메시지 큐는 쉽게 말해 "중간 우체통"이야. A 서비스가 B 서비스한테 직접 말 걸지 않고, 우체통에 편지를 넣어두면 B가 나중에 꺼내 읽는 방식이지.
이렇게 하면 A와 B가 서로 독립적으로 동작할 수 있어서 시스템이 훨씬 유연해져!

NSQ가 탄생한 배경

Bitly는 하루에 수억 건의 URL 단축 요청을 처리해야 했어. 기존의 메시지 큐 시스템들(RabbitMQ, ActiveMQ 등)은 설정이 복잡하거나 단일 장애점(SPOF, Single Point of Failure) 문제가 있었지.
그래서 Bitly 팀이 직접 만든 게 NSQ야. "설정 간단하고, 빠르고, 분산되어 있고, 장애에 강한" 메시지 큐를 목표로 했어.

NSQ의 핵심 철학

📌 분산(Distributed): 중앙 브로커 없이 각 노드가 독립적으로 동작
📌 수평 확장(Horizontally Scalable): 노드를 추가하는 것만으로 성능 확장 가능
📌 낮은 지연(Low Latency): 메모리 기반 처리로 초고속 메시지 전달
📌 내결함성(Fault Tolerant): 일부 노드가 죽어도 시스템 전체는 살아있음
📌 최소 1회 전달 보장(At-least-once delivery): 메시지가 반드시 처리됨을 보장

2. NSQ의 핵심 컴포넌트 3총사 🔧

NSQ는 크게 3가지 핵심 컴포넌트로 구성돼. 이 3개만 제대로 이해하면 NSQ의 80%는 이해한 거야!

NSQ 핵심 컴포넌트 구조 📤 Producer 메시지 생산자 앱 서버, API 등 메시지를 발행 🗄️ nsqd 메시지 데몬 Topic 관리 Channel 관리 메시지 저장/전달 TCP:4150 HTTP:4151 핵심 브로커 역할 🔍 nsqlookupd 서비스 디스커버리 nsqd 위치 등록/조회 TCP:4160 / HTTP:4161 🖥️ nsqadmin 웹 관리 UI 모니터링 대시보드 HTTP:4171 📥 Consumer 메시지 소비자 워커, 처리 서버 메시지를 구독 PUB SUB 등록 Consumer는 nsqlookupd를 통해 nsqd 위치를 파악하고 직접 연결

① nsqd - 메시지의 심장 💓

nsqd는 NSQ의 핵심 데몬이야. 실제로 메시지를 받고, 저장하고, 전달하는 역할을 해.
각 서버에 독립적으로 실행되며, 다른 nsqd와 직접 통신하지 않아. 이게 NSQ의 분산 철학이야!

🔹 TCP 포트 4150: Producer/Consumer가 연결하는 포트
🔹 HTTP 포트 4151: REST API, 메시지 발행, 통계 조회
🔹 Topic: 메시지의 카테고리 (예: "주문완료", "결제요청")
🔹 Channel: Topic의 구독 그룹 (같은 채널의 Consumer들은 메시지를 나눠서 처리)

② nsqlookupd - 서비스 디스커버리 🔍

nsqlookupd는 "어디에 어떤 nsqd가 있는지" 알려주는 서비스야.
Consumer가 특정 Topic을 구독하고 싶을 때, nsqlookupd에게 물어보면 해당 Topic을 가진 nsqd 목록을 알려줘.
이 덕분에 Consumer는 nsqd의 IP를 하드코딩할 필요가 없어! 🎉

💡 nsqlookupd는 메시지를 직접 처리하지 않아. 오직 "어디에 뭐가 있는지" 알려주는 디렉토리 역할만 해.
그래서 nsqlookupd가 잠깐 죽어도 이미 연결된 Consumer들은 계속 메시지를 받을 수 있어!

③ nsqadmin - 눈으로 보는 관리 UI 🖥️

nsqadmin은 웹 기반 관리 인터페이스야. 브라우저에서 접속하면 현재 Topic, Channel, 메시지 수, Consumer 수 등을 한눈에 볼 수 있어.
기본 포트는 4171이고, 개발/운영 환경에서 모니터링할 때 정말 유용해!

3. Topic과 Channel: NSQ의 핵심 개념 🎯

NSQ를 이해하는 데 있어서 Topic과 Channel의 차이를 아는 게 정말 중요해.
이걸 헷갈리면 나중에 설계할 때 꼬여버리거든. 확실하게 정리해보자!

Topic → Channel → Consumer 흐름 📦 Topic "주문완료" 메시지 1 메시지 2 메시지 3 메시지 4 메시지 5 복사되어 각 채널로 📢 Channel "알림서비스" 메시지 1 복사본 메시지 2 복사본 📦 Channel "재고관리" 메시지 1 복사본 메시지 2 복사본 👤 Consumer A 👤 Consumer B ↑ 메시지 분산처리 👤 Consumer C 👤 Consumer D 📌 핵심 규칙 Topic → 각 Channel로 메시지가 복사됨 같은 Channel 내에서는 Consumer들이 메시지를 나눠서(분산) 처리 = Pub/Sub + 로드밸런싱 두 가지를 동시에! Channel이 없으면 메시지는 버려짐! ⚠️ Consumer가 먼저 구독해야 함

Topic: 메시지의 카테고리

Topic은 메시지를 분류하는 카테고리야. 예를 들어 이커머스 시스템이라면:
order_completed payment_failed user_signup inventory_update
이런 식으로 Topic을 만들 수 있어. Topic은 nsqd에 메시지가 처음 발행될 때 자동으로 생성돼.

Channel: 구독 그룹

Channel은 Topic을 구독하는 그룹이야. 중요한 건 같은 Topic의 메시지가 각 Channel에 복사된다는 거야.
예를 들어 order_completed Topic에 notification, inventory, analytics 채널이 있다면,
주문 완료 메시지 하나가 세 채널 모두에 복사되어 각각 처리돼!

🌟 실전 팁!
같은 Channel 안에 Consumer가 여러 개 있으면, 메시지가 Consumer들에게 라운드로빈 방식으로 분산돼.
즉, Channel은 "Pub/Sub 패턴"과 "로드밸런싱"을 동시에 제공하는 마법 같은 개념이야! ✨

4. NSQ 설치하고 바로 써보자! 🚀

이론은 충분히 했으니 이제 직접 손을 움직여보자. NSQ 설치는 생각보다 훨씬 간단해!

방법 1: 바이너리 직접 다운로드

NSQ 공식 사이트(nsq.io)에서 OS에 맞는 바이너리를 다운받으면 돼.

# macOS (Homebrew)
brew install nsq

# Linux (직접 다운로드)
wget https://s3.amazonaws.com/bitly-downloads/nsq/nsq-1.3.0.linux-amd64.go1.21.5.tar.gz
tar -zxvf nsq-*.tar.gz
cd nsq-*/bin

방법 2: Docker로 실행 (추천! 🐳)

Docker를 쓰면 환경 걱정 없이 바로 실행할 수 있어.

# nsqlookupd 실행
docker run -d --name nsqlookupd \
  -p 4160:4160 -p 4161:4161 \
  nsqio/nsq /nsqlookupd

# nsqd 실행 (nsqlookupd에 등록)
docker run -d --name nsqd \
  -p 4150:4150 -p 4151:4151 \
  nsqio/nsq /nsqd \
  --lookupd-tcp-address=nsqlookupd:4160

# nsqadmin 실행
docker run -d --name nsqadmin \
  -p 4171:4171 \
  nsqio/nsq /nsqadmin \
  --lookupd-http-address=nsqlookupd:4161

Docker Compose로 한 번에 실행하기

실제 개발 환경에서는 Docker Compose를 쓰는 게 훨씬 편해!

# docker-compose.yml
version: '3'
services:
  nsqlookupd:
    image: nsqio/nsq
    command: /nsqlookupd
    ports:
      - "4160:4160"
      - "4161:4161"

  nsqd:
    image: nsqio/nsq
    command: /nsqd --lookupd-tcp-address=nsqlookupd:4160
    ports:
      - "4150:4150"
      - "4151:4151"
    depends_on:
      - nsqlookupd

  nsqadmin:
    image: nsqio/nsq
    command: /nsqadmin --lookupd-http-address=nsqlookupd:4161
    ports:
      - "4171:4171"
    depends_on:
      - nsqlookupd
# 실행
docker-compose up -d

# 확인
docker-compose ps
🎉 실행 후 브라우저에서 http://localhost:4171에 접속하면 nsqadmin 대시보드를 볼 수 있어!

기본 동작 테스트 (curl로 빠르게)

# Topic에 메시지 발행 (HTTP API)
curl -d 'hello NSQ!' 'http://127.0.0.1:4151/pub?topic=test'

# 여러 메시지 한 번에 발행 (MPUB)
curl -d $'msg1\nmsg2\nmsg3' 'http://127.0.0.1:4151/mpub?topic=test'

# Topic 목록 확인
curl http://127.0.0.1:4151/stats

# nsqlookupd에서 Topic 조회
curl http://127.0.0.1:4161/topics

5. Go로 Producer/Consumer 구현하기 🐹

NSQ는 Go로 만들어졌으니 Go 클라이언트가 가장 잘 지원돼. 공식 Go 클라이언트 라이브러리는 go-nsq야!

go-nsq 설치

go get github.com/nsqio/go-nsq

Producer 구현

package main

import (
    "log"
    "github.com/nsqio/go-nsq"
)

func main() {
    // NSQ 설정
    config := nsq.NewConfig()
    
    // Producer 생성
    producer, err := nsq.NewProducer("127.0.0.1:4150", config)
    if err != nil {
        log.Fatal("Producer 생성 실패:", err)
    }
    defer producer.Stop()

    // 메시지 발행
    topic := "order_completed"
    message := []byte(`{"order_id": "12345", "user_id": "user_001", "amount": 50000}`)
    
    err = producer.Publish(topic, message)
    if err != nil {
        log.Fatal("메시지 발행 실패:", err)
    }
    
    log.Println("✅ 메시지 발행 성공!")
    
    // 여러 메시지 한 번에 발행
    messages := [][]byte{
        []byte(`{"order_id": "12346"}`),
        []byte(`{"order_id": "12347"}`),
        []byte(`{"order_id": "12348"}`),
    }
    
    err = producer.MultiPublish(topic, messages)
    if err != nil {
        log.Fatal("멀티 발행 실패:", err)
    }
    
    log.Println("✅ 멀티 메시지 발행 성공!")
}

Consumer 구현

package main

import (
    "encoding/json"
    "log"
    "os"
    "os/signal"
    "syscall"
    "github.com/nsqio/go-nsq"
)

// 주문 데이터 구조체
type Order struct {
    OrderID string  `json:"order_id"`
    UserID  string  `json:"user_id"`
    Amount  float64 `json:"amount"`
}

// 메시지 핸들러 구현
type OrderHandler struct{}

func (h *OrderHandler) HandleMessage(msg *nsq.Message) error {
    var order Order
    
    // JSON 파싱
    if err := json.Unmarshal(msg.Body, &order); err != nil {
        log.Printf("❌ JSON 파싱 실패: %v", err)
        // 에러 반환 시 메시지 재시도됨
        return err
    }
    
    log.Printf("📦 주문 처리: ID=%s, 금액=%.0f원", order.OrderID, order.Amount)
    
    // 비즈니스 로직 처리
    // ... 알림 발송, DB 업데이트 등
    
    // nil 반환 = 처리 성공 (FIN 전송)
    return nil
}

func main() {
    config := nsq.NewConfig()
    
    // 동시 처리 수 설정 (기본값: 1)
    config.MaxInFlight = 10
    
    // Consumer 생성 (topic, channel)
    consumer, err := nsq.NewConsumer("order_completed", "notification", config)
    if err != nil {
        log.Fatal("Consumer 생성 실패:", err)
    }
    
    // 핸들러 등록
    consumer.AddHandler(&OrderHandler{})
    
    // 동시 처리 핸들러 여러 개 등록
    // consumer.AddConcurrentHandlers(&OrderHandler{}, 5)
    
    // nsqlookupd를 통해 연결 (권장!)
    err = consumer.ConnectToNSQLookupd("127.0.0.1:4161")
    if err != nil {
        log.Fatal("nsqlookupd 연결 실패:", err)
    }
    
    // 또는 nsqd에 직접 연결
    // err = consumer.ConnectToNSQD("127.0.0.1:4150")
    
    log.Println("🚀 Consumer 시작! 메시지 대기 중...")
    
    // 종료 시그널 대기
    sigChan := make(chan os.Signal, 1)
    signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
    <-sigChan
    
    consumer.Stop()
    log.Println("👋 Consumer 종료")
}
🌟 중요한 포인트!
Consumer의 HandleMessage에서 nil을 반환하면 NSQ에 FIN(처리 완료) 신호를 보내.
에러를 반환하면 REQ(재시도 요청) 신호를 보내서 메시지가 다시 큐에 들어가.
이게 바로 "최소 1회 전달 보장"의 핵심 메커니즘이야!

6. Python으로도 NSQ 쓸 수 있어! 🐍

Go만 쓰는 건 아니야. Python, Node.js, Java 등 다양한 언어에서 NSQ를 사용할 수 있어.
Python 예제도 간단히 살펴보자!

pynsq 설치

pip install pynsq

Python Producer (HTTP API 사용)

import requests
import json

def publish_message(topic: str, data: dict) -> bool:
    """NSQ에 메시지 발행"""
    url = f"http://127.0.0.1:4151/pub?topic={topic}"
    
    try:
        response = requests.post(
            url,
            data=json.dumps(data),
            headers={'Content-Type': 'application/json'}
        )
        response.raise_for_status()
        print(f"✅ 메시지 발행 성공: {topic}")
        return True
    except requests.RequestException as e:
        print(f"❌ 발행 실패: {e}")
        return False

# 사용 예시
order_data = {
    "order_id": "12345",
    "user_id": "user_001",
    "amount": 50000,
    "items": ["상품A", "상품B"]
}

publish_message("order_completed", order_data)

Python Consumer (pynsq 사용)

import nsq
import json
import tornado.ioloop

def handle_message(message):
    """메시지 처리 핸들러"""
    try:
        data = json.loads(message.body)
        print(f"📦 주문 처리: {data.get('order_id')}")
        
        # 비즈니스 로직
        # ...
        
        message.finish()  # 처리 완료
    except Exception as e:
        print(f"❌ 처리 실패: {e}")
        message.requeue()  # 재시도 요청

# Consumer 설정
reader = nsq.Reader(
    message_handler=handle_message,
    lookupd_http_addresses=['http://127.0.0.1:4161'],
    topic='order_completed',
    channel='notification',
    max_in_flight=10
)

print("🚀 Python Consumer 시작!")
nsq.run()

7. 실전 운영을 위한 고급 설정 ⚙️

개발 환경에서 테스트하는 건 쉬운데, 실제 프로덕션 환경에서 안정적으로 운영하려면 몇 가지 중요한 설정들을 알아야 해!

nsqd 주요 설정 옵션

옵션 기본값 설명
--mem-queue-size 10000 메모리에 유지할 최대 메시지 수
--max-msg-size 1048576 (1MB) 최대 메시지 크기
--msg-timeout 60s 메시지 처리 타임아웃
--max-req-timeout 1h 최대 재시도 대기 시간
--data-path /tmp/nsqd 디스크 저장 경로
--max-rdy-count 2500 Consumer당 최대 RDY 카운트
--statsd-address - StatsD 서버 주소 (모니터링)

메시지 재시도 전략 (Backoff)

Consumer에서 처리 실패 시 NSQ는 자동으로 재시도를 해. Go 클라이언트에서 재시도 설정을 커스터마이징할 수 있어:

config := nsq.NewConfig()

// 재시도 지연 설정
config.DefaultRequeueDelay = 90 * time.Second
config.MaxRequeueDelay = 15 * time.Minute

// 백오프 전략 설정
config.BackoffStrategy = &nsq.ExponentialBackoffStrategy{
    MinDelay: 1 * time.Second,
    MaxDelay: 10 * time.Minute,
}

// 최대 재시도 횟수 (기본: 5)
// 초과 시 메시지는 자동으로 FIN 처리됨
config.MaxAttempts = 10

메시지 지연 발행 (Deferred Publishing)

특정 시간 후에 메시지를 처리하고 싶을 때 사용해. 예를 들어 "30분 후에 쿠폰 만료 알림 보내기" 같은 경우!

// 30분 후에 처리될 메시지 발행
delay := 30 * time.Minute
err = producer.DeferredPublish(topic, delay, message)
if err != nil {
    log.Fatal("지연 발행 실패:", err)
}
log.Println("⏰ 30분 후 처리될 메시지 발행 완료!")

메모리 vs 디스크 저장

NSQ는 기본적으로 메모리에 메시지를 저장해. 하지만 --mem-queue-size를 초과하면 자동으로 디스크에 저장해.

🔹 메모리 저장: 초고속, 하지만 nsqd 재시작 시 손실 가능
🔹 디스크 저장: 느리지만 영속성 보장

--mem-queue-size=0으로 설정하면 모든 메시지를 디스크에 저장해서 영속성을 완전히 보장할 수 있어!

⚠️ 주의!
NSQ는 기본적으로 메시지 순서를 보장하지 않아. 순서가 중요한 비즈니스 로직이라면 Consumer 쪽에서 별도로 처리해야 해.
또한 기본 설정에서 nsqd가 재시작되면 메모리의 메시지는 손실될 수 있으니 중요한 메시지는 --mem-queue-size=0 설정을 고려해봐!

8. 실전 아키텍처 패턴 🏗️

NSQ를 실제 서비스에 적용할 때 자주 쓰이는 패턴들을 알아보자!

이커머스 주문 처리 NSQ 아키텍처 🌐 API 서버 주문 접수 Producer 역할 PUB 🗄️ nsqd Topic: order_completed Channel: notification / inventory analytics / email 📢 알림 서비스 푸시/SMS 발송 ch: notification 📦 재고 서비스 재고 차감 처리 ch: inventory 📊 분석 서비스 데이터 집계 ch: analytics 📧 이메일 서비스 주문 확인 메일 ch: email 🔍 nsqlookupd 서비스 디스커버리 nsqd 위치 관리 🖥️ nsqadmin 웹 모니터링 UI http://localhost:4171 등록 주문 1건 → nsqd → 4개 채널 동시 처리 (알림 + 재고 + 분석 + 이메일) 🚀

패턴 1: 이벤트 드리븐 마이크로서비스

위 그림처럼 하나의 이벤트(주문 완료)를 여러 서비스가 독립적으로 처리하는 패턴이야.
각 서비스는 서로를 전혀 모르고, NSQ를 통해서만 통신해. 이게 바로 느슨한 결합(Loose Coupling)의 핵심이야!

패턴 2: 작업 큐 (Work Queue)

무거운 작업(이미지 처리, 동영상 인코딩 등)을 백그라운드에서 처리할 때 사용해.
같은 Channel에 여러 Worker를 붙이면 자동으로 로드밸런싱이 돼!

// 여러 Worker가 같은 채널 구독 → 자동 로드밸런싱
// Worker 1
consumer1, _ := nsq.NewConsumer("video_encode", "workers", config)
consumer1.AddHandler(&VideoEncoder{WorkerID: 1})
consumer1.ConnectToNSQLookupd("127.0.0.1:4161")

// Worker 2 (같은 채널!)
consumer2, _ := nsq.NewConsumer("video_encode", "workers", config)
consumer2.AddHandler(&VideoEncoder{WorkerID: 2})
consumer2.ConnectToNSQLookupd("127.0.0.1:4161")

// 메시지가 Worker 1, 2에 자동으로 분산됨 🎉

패턴 3: 고가용성 구성 (HA)

프로덕션 환경에서는 nsqd와 nsqlookupd를 여러 대 운영해야 해!

🔹 nsqlookupd: 최소 2대 이상 운영 (홀수 권장)
🔹 nsqd: 각 서버에 1개씩 배포 (Producer와 같은 서버에 배치하면 네트워크 오버헤드 최소화)
🔹 Consumer: nsqlookupd 주소를 여러 개 설정하면 자동으로 모든 nsqd에서 메시지 수신

consumer.ConnectToNSQLookupd("nsqlookupd-1:4161")
consumer.ConnectToNSQLookupd("nsqlookupd-2:4161")

9. NSQ vs 다른 메시지 큐 비교 🥊

NSQ가 좋다고 했는데, 다른 메시지 큐들과 비교하면 어떨까? 솔직하게 비교해볼게!

항목 NSQ RabbitMQ Kafka Redis Pub/Sub
설정 복잡도 ⭐ 매우 간단 ⭐⭐⭐ 복잡 ⭐⭐⭐⭐ 매우 복잡 ⭐ 매우 간단
처리량 높음 중간 매우 높음 높음
메시지 순서 보장 ❌ 미보장 ✅ 보장 ✅ 파티션 내 보장 ❌ 미보장
메시지 영속성 선택적 ✅ 기본 지원 ✅ 기본 지원 ❌ 미지원
메시지 재처리 제한적 ✅ 지원 ✅ 완전 지원 ❌ 미지원
분산 구조 ✅ 기본 지원 클러스터 설정 필요 ✅ 기본 지원 클러스터 설정 필요
학습 곡선 낮음 👍 중간 높음 낮음
적합한 규모 중소~대규모 중소규모 대규모 소규모
💡 언제 NSQ를 선택해야 할까?
✅ 빠르게 메시지 큐를 도입해야 할 때
✅ 설정 복잡도를 최소화하고 싶을 때
✅ 분산 환경에서 단일 장애점 없이 운영하고 싶을 때
✅ Go 기반 스택을 사용하는 팀
❌ 메시지 순서가 반드시 보장되어야 할 때 → Kafka 고려
❌ 메시지 재처리(리플레이)가 필요할 때 → Kafka 고려

10. 모니터링과 운영 꿀팁 🍯

HTTP API로 상태 확인

# nsqd 전체 통계
curl http://127.0.0.1:4151/stats

# 특정 Topic 정보
curl http://127.0.0.1:4151/stats?topic=order_completed

# 헬스체크
curl http://127.0.0.1:4151/ping

# Topic 목록
curl http://127.0.0.1:4151/topics

# Channel 목록
curl http://127.0.0.1:4151/channels?topic=order_completed

nsq_tail로 실시간 메시지 확인

# 특정 Topic의 메시지를 실시간으로 확인 (디버깅에 유용!)
nsq_tail --lookupd-http-address=127.0.0.1:4161 \
         --topic=order_completed \
         --channel=debug_channel

nsq_to_file로 메시지 백업

# 메시지를 파일로 저장 (감사 로그, 백업 등)
nsq_to_file --lookupd-http-address=127.0.0.1:4161 \
            --topic=order_completed \
            --channel=backup \
            --output-dir=/var/log/nsq \
            --datetime-format="%Y%m%d"

Prometheus + Grafana 연동

NSQ는 StatsD 형식으로 메트릭을 내보낼 수 있어. Prometheus exporter를 사용하면 Grafana 대시보드로 예쁘게 시각화할 수 있어!

# nsqd StatsD 설정
/nsqd --statsd-address=127.0.0.1:8125 \
      --statsd-interval=60s \
      --statsd-prefix=nsq.
🌟 운영 꿀팁 모음!
1️⃣ 메시지 크기 제한: 큰 데이터는 S3/GCS에 저장하고 NSQ에는 URL만 전달해
2️⃣ Consumer 수 조절: 처리량이 부족하면 같은 Channel에 Consumer를 추가하면 돼
3️⃣ 타임아웃 설정: 처리 시간이 긴 작업은 msg-timeout을 충분히 늘려줘
4️⃣ 채널 정리: 사용하지 않는 채널은 HTTP API로 삭제해서 메모리 낭비를 막아
5️⃣ 로그 레벨: 프로덕션에서는 --log-level=warn으로 설정해서 로그 양을 줄여

11. 완전한 실전 예제: 주문 처리 시스템 🛒

지금까지 배운 내용을 종합해서 실제로 쓸 수 있는 주문 처리 시스템을 만들어보자!
이런 시스템은 재능넷 같은 플랫폼에서도 실제로 활용할 수 있는 패턴이야. 🎯

프로젝트 구조

order-system/
├── cmd/
│   ├── producer/
│   │   └── main.go      # 주문 발행 서버
│   ├── notification/
│   │   └── main.go      # 알림 Consumer
│   └── inventory/
│       └── main.go      # 재고 Consumer
├── pkg/
│   ├── nsq/
│   │   └── client.go    # NSQ 공통 클라이언트
│   └── models/
│       └── order.go     # 주문 모델
└── docker-compose.yml

공통 NSQ 클라이언트 (pkg/nsq/client.go)

package nsqclient

import (
    "log"
    "time"
    "github.com/nsqio/go-nsq"
)

// NSQ 설정 생성
func NewConfig() *nsq.Config {
    config := nsq.NewConfig()
    config.MaxInFlight = 20
    config.DefaultRequeueDelay = 30 * time.Second
    config.MaxRequeueDelay = 10 * time.Minute
    config.MaxAttempts = 5
    config.HeartbeatInterval = 30 * time.Second
    return config
}

// Producer 생성
func NewProducer(addr string) (*nsq.Producer, error) {
    producer, err := nsq.NewProducer(addr, NewConfig())
    if err != nil {
        return nil, err
    }
    
    // 연결 테스트
    if err := producer.Ping(); err != nil {
        return nil, err
    }
    
    log.Printf("✅ NSQ Producer 연결 성공: %s", addr)
    return producer, nil
}

// Consumer 생성
func NewConsumer(
    topic, channel string,
    handler nsq.Handler,
    lookupAddrs []string,
) (*nsq.Consumer, error) {
    consumer, err := nsq.NewConsumer(topic, channel, NewConfig())
    if err != nil {
        return nil, err
    }
    
    consumer.AddHandler(handler)
    
    for _, addr := range lookupAddrs {
        if err := consumer.ConnectToNSQLookupd(addr); err != nil {
            return nil, err
        }
    }
    
    log.Printf("✅ NSQ Consumer 시작: topic=%s, channel=%s", topic, channel)
    return consumer, nil
}

주문 모델 (pkg/models/order.go)

package models

import "time"

type Order struct {
    OrderID   string    `json:"order_id"`
    UserID    string    `json:"user_id"`
    Items     []Item    `json:"items"`
    Amount    float64   `json:"amount"`
    Status    string    `json:"status"`
    CreatedAt time.Time `json:"created_at"`
}

type Item struct {
    ProductID string  `json:"product_id"`
    Name      string  `json:"name"`
    Quantity  int     `json:"quantity"`
    Price     float64 `json:"price"`
}

Producer 서버 (cmd/producer/main.go)

package main

import (
    "encoding/json"
    "log"
    "time"
    "github.com/google/uuid"
    nsqclient "order-system/pkg/nsq"
    "order-system/pkg/models"
)

const (
    NSQ_ADDR  = "127.0.0.1:4150"
    TOPIC     = "order_completed"
)

func main() {
    producer, err := nsqclient.NewProducer(NSQ_ADDR)
    if err != nil {
        log.Fatal("Producer 초기화 실패:", err)
    }
    defer producer.Stop()

    // 테스트 주문 생성 및 발행
    for i := 0; i < 10; i++ {
        order := models.Order{
            OrderID: uuid.New().String(),
            UserID:  "user_001",
            Items: []models.Item{
                {
                    ProductID: "prod_001",
                    Name:      "Go 프로그래밍 강의",
                    Quantity:  1,
                    Price:     50000,
                },
            },
            Amount:    50000,
            Status:    "completed",
            CreatedAt: time.Now(),
        }

        data, err := json.Marshal(order)
        if err != nil {
            log.Printf("JSON 변환 실패: %v", err)
            continue
        }

        if err := producer.Publish(TOPIC, data); err != nil {
            log.Printf("❌ 발행 실패: %v", err)
        } else {
            log.Printf("✅ 주문 발행: %s", order.OrderID)
        }

        time.Sleep(500 * time.Millisecond)
    }
}

알림 Consumer (cmd/notification/main.go)

package main

import (
    "encoding/json"
    "log"
    "os"
    "os/signal"
    "syscall"
    "github.com/nsqio/go-nsq"
    nsqclient "order-system/pkg/nsq"
    "order-system/pkg/models"
)

type NotificationHandler struct{}

func (h *NotificationHandler) HandleMessage(msg *nsq.Message) error {
    var order models.Order
    if err := json.Unmarshal(msg.Body, &order); err != nil {
        log.Printf("❌ 파싱 실패: %v", err)
        return err
    }

    // 알림 발송 로직
    log.Printf("📢 알림 발송 → 사용자: %s, 주문: %s, 금액: %.0f원",
        order.UserID, order.OrderID, order.Amount)
    
    // 실제로는 여기서 FCM, SMS, 카카오 알림톡 등 발송
    // sendPushNotification(order.UserID, order.OrderID)
    
    return nil // 처리 완료
}

func main() {
    lookupAddrs := []string{"127.0.0.1:4161"}
    
    consumer, err := nsqclient.NewConsumer(
        "order_completed",
        "notification",
        &NotificationHandler{},
        lookupAddrs,
    )
    if err != nil {
        log.Fatal("Consumer 시작 실패:", err)
    }

    sigChan := make(chan os.Signal, 1)
    signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
    <-sigChan

    consumer.Stop()
    log.Println("👋 알림 서비스 종료")
}

12. 자주 만나는 문제와 해결법 🔧

문제 1: 메시지가 계속 재시도됨

🔴 증상: Consumer에서 같은 메시지가 계속 들어옴
🟢 원인: HandleMessage에서 에러를 반환하거나 타임아웃이 발생
🟢 해결:
- 처리 로직에서 에러 핸들링 확인
- msg-timeout 값을 처리 시간보다 충분히 크게 설정
- 영구적 실패 메시지는 msg.Finish()로 강제 완료 처리

문제 2: Consumer가 메시지를 받지 못함

🔴 증상: Producer는 발행하는데 Consumer가 수신 못 함
🟢 원인: Channel이 생성되지 않았거나 nsqlookupd 연결 문제
🟢 해결:
- Consumer가 먼저 실행되어 Channel을 생성해야 함
- nsqadmin에서 Topic/Channel 상태 확인
- nsqlookupd 주소 및 포트 확인

문제 3: 메모리 사용량이 계속 증가

🔴 증상: nsqd 프로세스 메모리가 계속 늘어남
🟢 원인: Consumer 처리 속도보다 Producer 발행 속도가 빠름
🟢 해결:
- Consumer 수를 늘리거나 MaxInFlight 값 증가
- --mem-queue-size 조정으로 디스크 오프로드 활성화
- Producer 발행 속도 제한(Rate Limiting) 적용

문제 4: nsqd 재시작 후 메시지 손실

🔴 증상: nsqd 재시작 후 처리 중이던 메시지가 사라짐
🟢 원인: 메모리에만 저장된 메시지는 재시작 시 손실
🟢 해결:
- --mem-queue-size=0으로 모든 메시지를 디스크에 저장
- --data-path를 영속 볼륨으로 설정
- 중요 메시지는 DB에도 상태를 기록하는 이중화 전략 사용

마무리: NSQ, 이제 두렵지 않지? 🎉

자, 여기까지 NSQ의 모든 것을 파헤쳐봤어! 처음엔 "분산형 실시간 메시징 플랫폼"이라는 말이 어렵게 느껴졌을 수도 있는데, 막상 뜯어보면 개념이 정말 깔끔하고 직관적이지 않아? 😄

NSQ의 핵심을 한 줄로 정리하면:
"Producer가 Topic에 메시지를 넣으면, 각 Channel의 Consumer들이 나눠서 처리한다"
이것만 기억해도 NSQ의 80%는 이해한 거야!

📚 오늘 배운 것 정리
✅ NSQ의 3대 컴포넌트: nsqd, nsqlookupd, nsqadmin
✅ Topic(카테고리)과 Channel(구독 그룹)의 차이
✅ Docker로 NSQ 환경 구성하기
✅ Go/Python 클라이언트로 Producer/Consumer 구현
✅ 실전 아키텍처 패턴 (이벤트 드리븐, 작업 큐, HA)
✅ 운영 팁과 트러블슈팅

재능넷에서 다양한 개발 재능을 가진 분들이 활동하고 있는데, NSQ 같은 메시징 시스템 구축 경험은 백엔드 개발자로서 정말 강력한 무기가 될 거야. 실제 프로젝트에 적용해보면서 경험을 쌓아봐! 💪

NSQ 공식 문서(nsq.io)와 GitHub(github.com/nsqio/nsq)에 더 자세한 내용이 있으니 꼭 참고해봐. 그리고 궁금한 점이 생기면 언제든지 파고들어! 개발은 결국 직접 해보는 게 최고야. 🚀

댓글 작성

이 글에 대한 여러분의 생각을 들려주세요

댓글 0
댓글을 불러오는 중입니다.

아직 댓글이 없습니다.