Pub/Sub Bus
정의
Pub/Sub Bus (Publish-Subscribe bus)는 메시지 송신자(publisher)와 수신자(subscriber)가 서로를 모르고도 통신할 수 있게 해주는 중간 메시지 라우팅 계층이다.
- Publisher: 특정 채널/토픽에 메시지를 발행
- Subscriber: 관심 있는 채널을 구독, 발행된 메시지 수신
- Bus: 둘 사이의 라우팅 담당 (Redis, Kafka, NATS, RabbitMQ 등)
왜 필요한가, 분산 Stateful 서버의 확장 문제
WebSocket 같은 지속 연결 서버를 여러 인스턴스로 확장하면 새로운 문제가 생긴다.
Client A → Server 1
Client B → Server 2 (다른 서버)
질문: A 가 B 에게 메시지를 보내려면?
서버 1 은 B 와 연결되어 있지 않으니 직접 send 할 수 없다.
해결책: Pub/Sub Bus
1. Server 1 이 A 의 메시지 수신
2. Server 1 → Redis publish "channel-B" with payload
3. Server 2 가 "channel-B" 를 구독 중
4. Redis → Server 2 에 메시지 전달
5. Server 2 → B 에게 send
서버는 자기 클라이언트만 관리하면 됨. 서버 간 라우팅은 bus 가 담당.
동작 패턴
Channel 기반
특정 이름의 채널에 발행/구독.
// Publisher
redis.publish('chat:room:123', JSON.stringify({ from: 'A', text: 'hello' }));
// Subscriber
redis.subscribe('chat:room:123', (message) => {
const data = JSON.parse(message);
forwardToClient(data);
});
Pattern 기반 (와일드카드)
redis.psubscribe('chat:room:*', (channel, message) => {
// chat:room:123, chat:room:456 모두 매칭
});
Topic 기반 (계층화)
Kafka 등에서 토픽을 partition 단위로 분배.
topic: user-events
partition 0: user_id % 3 == 0 인 이벤트
partition 1: user_id % 3 == 1
partition 2: ...
Socket.IO + Redis Adapter 패턴
가장 흔한 실무 패턴.
import { Server } from 'socket.io';
import { createAdapter } from '@socket.io/redis-adapter';
import { createClient } from 'redis';
const io = new Server(httpServer);
const pubClient = createClient({ url: 'redis://localhost:6379' });
const subClient = pubClient.duplicate();
await Promise.all([pubClient.connect(), subClient.connect()]);
io.adapter(createAdapter(pubClient, subClient));
// 이후엔 같은 코드로 서버 간 브로드캐스트가 동작
io.to('room-123').emit('msg', payload);
// → Redis publish → 모든 서버 인스턴스 → 각자 자기 클라이언트에게 전달
서버 코드는 단일 인스턴스 때와 동일. Bus 가 multi-server 라우팅을 투명하게 처리한다.
Pub/Sub vs Message Queue (차이점)
| Pub/Sub | Message Queue | |
|---|---|---|
| 메시지 보관 | 즉시 전달 (fire-and-forget) | 큐에 보관, 컨슈머가 pull |
| 다중 수신자 | 모든 subscriber 가 받음 | 보통 1 consumer 가 받음 |
| 메시지 유실 | subscriber 가 offline 이면 잃을 수 있음 | persistent 가능 |
| 예시 | Redis Pub/Sub, NATS | RabbitMQ, SQS, Kafka (consumer group 모드) |
Redis Pub/Sub 은 단순 Pub/Sub. Kafka 는 두 모드 모두 가능 (broadcast + consumer group).
부하 특성
| 구성 요소 | 부하 |
|---|---|
| Publisher 서버 | 메시지마다 publish 호출 (~0.1 ms) |
| Bus | 모든 publish 가 통과, 가장 부담 큰 단일 지점 |
| Subscriber 서버 | 수신 + 자기 클라이언트에게 forward |
IMPORTANT
Bus 가 SPOF (단일 장애점) 가 될 수 있다. 고가용성 클러스터 (Redis Sentinel/Cluster, Kafka multi-broker) 가 필수. 또 다른 함정: 모든 메시지가 bus 를 통과하므로 bus 처리량이 시스템 전체의 상한이 된다.
사용 시나리오
1. WebSocket 클러스터
위에서 설명한 Socket.IO + Redis Adapter 패턴.
2. 마이크로서비스 간 이벤트 전파
주문 서비스 → publish "order.created"
→ 결제 서비스 (subscribe)
→ 재고 서비스 (subscribe)
→ 알림 서비스 (subscribe)
각 서비스가 독립적으로 진화 가능.
3. 실시간 알림 시스템
이벤트 발생 → publish "notify:user-{id}"
→ 사용자가 접속한 서버가 subscribe → 전달
4. Cache invalidation
DB 업데이트 → publish "cache-invalidate:user-{id}"
→ 모든 서버가 자기 로컬 캐시 무효화
대안과 trade-off
gRPC streaming + 서비스 디스커버리
서비스 간 직접 stream. Bus 없음. 더 빠르지만 N×M 연결 관리.
Service mesh (Istio 등)
L7 라우팅으로 메시지 전달. 더 강력하지만 복잡.
Database notification
PostgreSQL LISTEN/NOTIFY 같은 DB 내장 Pub/Sub. 외부 인프라 불필요.
관련 위키
- Stateful, Pub/Sub bus 가 필요한 이유
- Sticky Session, 대안적 접근
💬 댓글