여러 서버에서 발생한 이벤트를 실시간으로 다른 서버에 알려야 합니다. Redis Pub/Sub를 쓰면 발행자가 채널에 메시지를 보내는 순간 모든 구독자가 즉시 받습니다.
발행자(Publisher): 메시지를 채널에 보내는 주체. 구독자 목록을 알 필요가 없습니다.
구독자(Subscriber): 특정 채널을 구독해 메시지를 받는 주체. 메시지를 보내지 않고 받기만 합니다.
Publisher → [채널: chat:room:1] → Subscriber 1
→ Subscriber 2
→ Subscriber 3
발행자는 누가 구독하는지 모르고, 구독자는 누가 발행하는지 모릅니다. 채널이라는 공통 이름을 통해 연결됩니다.
# 특정 채널 구독
SUBSCRIBE chat:room:1
# 여러 채널 구독
SUBSCRIBE chat:room:1 chat:room:2 notifications:user:1000
구독 명령은 블로킹 상태로 메시지를 기다립니다. 메시지가 오면 출력합니다.
1) "message"
2) "chat:room:1"
3) "안녕하세요!"
# 채널에 메시지 전송
PUBLISH chat:room:1 "안녕하세요!"
# (integer) 3 ← 수신한 구독자 수
와일드카드로 여러 채널을 한 번에 구독합니다.
# chat: 으로 시작하는 모든 채널 구독
PSUBSCRIBE chat:*
# notifications: 로 시작하는 채널
PSUBSCRIBE notifications:*
# 구독 해제
PUNSUBSCRIBE chat:*
1) "pmessage"
2) "chat:*" ← 패턴
3) "chat:room:5" ← 실제 채널
4) "새 메시지입니다"
import { createClient } from "redis";
// 구독 전용 클라이언트
const subscriber = createClient();
await subscriber.connect();
// 발행 전용 클라이언트
const publisher = createClient();
await publisher.connect();
// 채널 구독
await subscriber.subscribe("notifications", (message) => {
console.log("받은 메시지:", message);
});
// 메시지 발행
await publisher.publish("notifications", JSON.stringify({
type: "new_comment",
postId: 1000,
userId: 500,
}));
구독과 발행에 클라이언트를 분리하는 이유는, 구독 상태에 들어간 클라이언트는 다른 명령을 실행할 수 없기 때문입니다.
// 메시지 발송 (서버 A)
await publisher.publish(
`chat:room:${roomId}`,
JSON.stringify({ userId, message, timestamp })
);
// 메시지 수신 (서버 B, C...)
await subscriber.subscribe(`chat:room:${roomId}`, (msg) => {
const data = JSON.parse(msg);
io.to(roomId).emit("message", data); // WebSocket 전파
});
// 상품 정보 변경 시 (서버 A)
await db.products.update(id, data);
await publisher.publish("cache:invalidate", `product:${id}`);
// 모든 서버에서 캐시 삭제
await subscriber.subscribe("cache:invalidate", async (key) => {
await redis.del(key);
});
// 특정 사용자에게 알림 발송
await publisher.publish(
`notifications:user:${userId}`,
JSON.stringify({ type: "like", postId: 1000 })
);
// 해당 사용자 세션이 있는 서버에서 수신
await subscriber.subscribe(`notifications:user:${userId}`, (msg) => {
socket.emit("notification", JSON.parse(msg));
});
Redis Pub/Sub는 Fire and Forget 방식입니다.
Fire and Forget: 메시지를 보내고 수신 확인을 기다리지 않는 방식. 성능이 높지만 전달 보장이 없습니다.
이런 한계가 문제라면 Redis Stream을 사용합니다. Stream은 메시지를 저장하고, 구독자가 재접속해도 놓친 메시지를 받을 수 있습니다.
| 항목 | Pub/Sub | Stream |
|---|---|---|
| 메시지 저장 | 없음 | 영구 저장 |
| 놓친 메시지 수신 | 불가 | 가능 |
| 소비자 그룹 | 없음 | 있음 |
| 처리 확인 | 없음 | ACK |
| 적합한 용도 | 실시간 브로드캐스트 | 신뢰성 있는 메시지 처리 |
구독자 모두에게 즉시 전달만 하면 된다면 Pub/Sub, 메시지 유실 없이 처리해야 한다면 Stream을 선택합니다.