🔔 실시간 알림, 어떻게 구현하나요?
사용자가 댓글을 달면 원글 작성자에게 즉시 알림을 보내고 싶습니다. 주문이 들어오면 주방 화면에 즉시 표시되어야 합니다. 이런 "실시간 전달"이 필요한 순간, Redis는 두 가지 선택지를 제공합니다.
첫 번째는 Pub/Sub(발행/구독)입니다. 이름 그대로 누군가 메시지를 발행(Publish)하면 구독(Subscribe) 중인 모든 수신자에게 즉시 전달됩니다. 마치 라디오 방송처럼, 방송 중에 채널을 켠 사람만 들을 수 있습니다.
두 번째는 Streams입니다. Streams는 메시지를 로그처럼 디스크에 기록합니다. 나중에 연결된 소비자도 과거 메시지를 읽을 수 있고, 처리 확인(ACK)까지 가능합니다. 마치 카카오톡처럼 내가 오프라인이었어도 다시 켜면 메시지가 남아있죠.
두 방식의 차이를 이해하면 어떤 상황에 무엇을 써야 할지 명확해집니다. 이번 편에서는 두 방식을 직접 실습하고 선택 기준을 정리합니다.
"메시지가 유실되어도 괜찮은가?" — YES라면 Pub/Sub, NO라면 Streams
📻 핵심 개념 1: Pub/Sub — 즉시 전달, 보관 없음
Pub/Sub은 채널(Channel)이라는 개념을 사용합니다. 구독자는 채널을 구독하고, 발행자는 채널에 메시지를 보냅니다. Redis는 그 메시지를 현재 구독 중인 모든 구독자에게 즉시 전달합니다.
핵심은 보관하지 않는다는 점입니다. 구독자가 오프라인이었다면? 그 사이에 발행된 메시지는 영원히 사라집니다. 연결이 끊기면 재구독 전까지의 모든 메시지를 놓칩니다.
주요 명령어
| 명령어 | 설명 |
|---|---|
SUBSCRIBE channel |
채널 구독 (연결 유지 필요) |
PSUBSCRIBE pattern |
패턴으로 다중 채널 구독 (예: chat:*) |
PUBLISH channel message |
채널에 메시지 발행, 반환값: 수신한 구독자 수 |
UNSUBSCRIBE channel |
구독 해제 |
PUBSUB CHANNELS |
현재 활성 채널 목록 |
# 터미널 A: 채널 구독 (이 명령 실행 후 대기 상태)
SUBSCRIBE chat:room1
# 결과:
# 1) "subscribe"
# 2) "chat:room1"
# 3) (integer) 1 ← 현재 구독 중인 채널 수
# 터미널 B: 메시지 발행
PUBLISH chat:room1 "안녕하세요!"
# 결과: (integer) 1 ← 수신한 구독자 수
PUBLISH chat:room1 "반갑습니다"
# 결과: (integer) 1
# 구독자가 없는 채널에 발행
PUBLISH chat:empty "아무도 없음"
# 결과: (integer) 0 ← 아무도 받지 못함
SUBSCRIBE 명령을 실행한 연결은 구독 전용 모드로 전환됩니다. 이 연결에서는 SUBSCRIBE/UNSUBSCRIBE/PING 외 다른 명령을 실행할 수 없습니다. 발행(PUBLISH)은 별도 연결이 필요합니다.
🌊 핵심 개념 2: Streams — 처리 추적, 재처리 가능
Redis Streams는 2018년 Redis 5.0에서 도입된 자료구조입니다. 메시지를 로그처럼 순서대로 저장하며, 각 메시지에는 자동으로 고유 ID가 부여됩니다. ID 형식은 타임스탬프-시퀀스입니다 (예: 1710000000000-0).
Streams의 핵심 기능은 소비자 그룹(Consumer Group)입니다. 여러 소비자가 작업을 나눠서 처리할 수 있고, 처리 완료 시 XACK로 확인합니다. XACK 전까지는 메시지가 "대기 중" 상태로 남아 재처리가 가능합니다.
Streams 핵심 명령어
| 명령어 | 설명 |
|---|---|
XADD stream * field value |
메시지 추가 (*는 자동 ID) |
XLEN stream |
스트림의 메시지 수 |
XREAD COUNT n STREAMS stream id |
메시지 읽기 (0 = 처음부터) |
XRANGE stream - + |
전체 메시지 조회 |
XGROUP CREATE stream group id |
소비자 그룹 생성 |
XREADGROUP GROUP g c COUNT n STREAMS s > |
그룹 소비 (미처리 메시지) |
XACK stream group id |
처리 완료 확인 |
# Lab 6 — Streams 기본 실습
DEL stream:order
# 메시지 추가 (* = 자동 ID 생성)
XADD stream:order * order_id 1001 status created item "coffee"
XADD stream:order * order_id 1002 status created item "latte"
XADD stream:order * order_id 1003 status created item "tea"
XLEN stream:order # → 3
# 처음부터 전체 조회
XRANGE stream:order - +
# 1) 1) "1710000000001-0"
# 2) 1) "order_id" 2) "1001" 3) "status" 4) "created" ...
# 최신 2개 읽기
XREAD COUNT 2 STREAMS stream:order 0
# 소비자 그룹 생성 ($ = 이후 메시지부터, 0 = 처음부터)
XGROUP CREATE stream:order kitchen $ MKSTREAM
# 새 메시지 추가 후 그룹 소비
XADD stream:order * order_id 1004 status created item "juice"
XREADGROUP GROUP kitchen worker1 COUNT 10 STREAMS stream:order >
# 처리 완료 확인 (ACK)
XACK stream:order kitchen [위에서 받은 메시지 ID]
🆚 Pub/Sub vs Streams 비교
📻 Pub/Sub
- ✅ 구현이 매우 간단
- ✅ 초저지연 (즉시 전달)
- ✅ 다수 구독자에게 동시 전달
- ❌ 메시지 보관 없음
- ❌ 오프라인 구독자는 놓침
- ❌ 처리 확인(ACK) 없음
- ❌ 재처리 불가
🌊 Streams
- ✅ 메시지 영구 보관
- ✅ 오프라인 후 재연결 가능
- ✅ 처리 확인(XACK) 지원
- ✅ 재처리/감사 추적 가능
- ✅ 소비자 그룹으로 병렬 처리
- ❌ 구현이 복잡함
- ❌ 메모리 사용량 증가
선택 기준 요약
| 상황 | 추천 | 이유 |
|---|---|---|
| 실시간 채팅 (메시지 유실 허용) | Pub/Sub | 단순, 즉각 전달 |
| 실시간 알림 (연결 중인 사용자에게만) | Pub/Sub | 구현 간단, 오프라인 무시 |
| 주문/결제 이벤트 (유실 불가) | Streams | ACK, 재처리, 감사 추적 |
| 로그 수집 / 이벤트 소싱 | Streams | 순서 보장, 재생 가능 |
| 여러 서비스가 같은 이벤트 처리 | Streams (소비자 그룹) | 병렬 처리, 부하 분산 |
🐍 Lab 6: Python으로 Pub/Sub 구현
# pip install redis
import redis
import time
r = redis.Redis(host="localhost", port=6379, decode_responses=True)
# 발행자: 알림 메시지 발송
def publish_notification(channel: str, message: str):
receivers = r.publish(channel, message)
print(f"[Publisher] '{message}' → 채널: {channel}, 수신자: {receivers}명")
# 알림 발송 시뮬레이션
publish_notification("notify:user:1000", "새 댓글이 달렸습니다")
publish_notification("notify:user:1000", "팔로워가 생겼습니다")
publish_notification("notify:broadcast", "서버 점검 예정: 02:00~03:00")
import redis
r = redis.Redis(host="localhost", port=6379, decode_responses=True)
# 구독자: 패턴으로 여러 채널 구독
pubsub = r.pubsub()
pubsub.psubscribe("notify:*") # notify: 로 시작하는 모든 채널
print("[Subscriber] 알림 채널 구독 시작...")
for message in pubsub.listen():
if message["type"] == "pmessage":
print(f"[알림] 채널: {message['channel']}, 내용: {message['data']}")
🟢 Lab 6: Node.js로 Streams 구현
// npm install redis
import { createClient } from "redis";
const producer = createClient();
const consumer = createClient();
await producer.connect();
await consumer.connect();
const STREAM_KEY = "stream:order";
const GROUP = "kitchen";
// 소비자 그룹 생성 (이미 있으면 무시)
try {
await consumer.xGroupCreate(STREAM_KEY, GROUP, "$", { MKSTREAM: true });
console.log("[Setup] 소비자 그룹 생성 완료");
} catch (e) {
console.log("[Setup] 소비자 그룹 이미 존재");
}
// 프로듀서: 주문 이벤트 추가
async function addOrder(orderId, item) {
const id = await producer.xAdd(STREAM_KEY, "*", {
order_id: orderId,
item,
status: "created",
timestamp: Date.now().toString(),
});
console.log(`[Producer] 주문 추가: ${orderId} (${item}), ID: ${id}`);
return id;
}
// 컨슈머: 주문 처리
async function processOrders() {
// > : 아직 전달되지 않은 새 메시지
const messages = await consumer.xReadGroup(GROUP, "worker1", [
{ key: STREAM_KEY, id: ">" }
], { COUNT: 10 });
if (!messages || messages.length === 0) {
console.log("[Consumer] 처리할 주문 없음");
return;
}
for (const { messages: msgs } of messages) {
for (const { id, message } of msgs) {
console.log(`[Consumer] 주문 처리: ${message.order_id} - ${message.item}`);
// 처리 완료 확인 (ACK)
await consumer.xAck(STREAM_KEY, GROUP, id);
console.log(`[Consumer] ACK 완료: ${id}`);
}
}
}
// 실행
await addOrder("1001", "아메리카노");
await addOrder("1002", "라떼");
await addOrder("1003", "녹차");
await processOrders();
await producer.quit();
await consumer.quit();
⚠️ 실전 팁: 한국 실무 사례로 보는 선택 기준
LINE의 오픈챗 서비스는 인기 채널에 트래픽이 집중되는 "핫키" 문제를 경험했습니다. 단일 Redis 노드에 부하가 집중되면 Pub/Sub 지연이 급증합니다. 이처럼 특정 채널에 수만 명이 몰리는 구조라면 샤딩 전략이 필요합니다.
Kakao는 Redis의 CPU 지표와 성능 이슈를 모니터링하며 메시징 아키텍처를 개선했습니다. 대용량 실시간 메시징에서 Redis만으로는 한계가 있을 수 있으며, Kafka 등 전용 메시지 브로커와의 혼용도 실무에서 자주 등장하는 패턴입니다.
🚨 Pub/Sub의 치명적 약점
Redis가 재시작되거나 구독자가 잠깐 연결이 끊기면 그 사이의 모든 메시지는 영원히 사라집니다. 결제, 주문, 재고 변경 같은 비즈니스 크리티컬 이벤트에는 절대 Pub/Sub만 사용하지 마세요.
📌 7편 핵심 요약
- Pub/Sub: 즉시 전달, 보관 없음 → 연결된 구독자에게만 전달
- Streams: 메시지 저장 + ACK + 재처리 → 비즈니스 크리티컬 이벤트에 적합
- 선택 기준: "메시지 유실이 허용되는가?" YES → Pub/Sub, NO → Streams
- 소비자 그룹: 여러 워커가 Streams 메시지를 나눠 처리, XACK로 완료 확인
- 핫키 주의: 인기 채널 집중 시 단일 Redis 노드 과부하 가능 → 샤딩 고려
✅ 7편 체크리스트
- SUBSCRIBE/PUBLISH로 Pub/Sub 기본 동작을 확인했다
- XADD/XRANGE로 Streams에 메시지를 추가하고 조회했다
- XGROUP CREATE로 소비자 그룹을 생성했다
- XREADGROUP ... > 로 미처리 메시지를 소비했다
- XACK로 처리 완료를 확인했다
- Pub/Sub과 Streams의 사용 시나리오 차이를 설명할 수 있다
🧠 퀴즈 3문항
Q1. PUBLISH chat:room1 "hello" 결과가 0이면 무슨 의미인가요?
Q2. Streams에서 XREADGROUP의 > (꺾쇠)는 무엇을 의미하나요?
Q3. 결제 완료 이벤트에 Pub/Sub과 Streams 중 어떤 것을 써야 하나요?
📝 과제 3가지
- 패턴 구독: PSUBSCRIBE notify:* 로 여러 알림 채널을 한꺼번에 구독하고, 각각 다른 채널에 발행해 수신되는지 확인하세요.
- Streams 재처리: XREADGROUP으로 메시지를 받고 XACK를 하지 않은 상태에서 XPENDING으로 대기 중인 메시지를 확인하세요.
- 메시지 크기 제한: XADD stream MAXLEN 100 * ... 로 스트림 최대 100개로 제한하고, 100개 이상 추가 시 어떻게 되는지 확인하세요.
📚 Redis 입문 시리즈 전체 목록
참고: Redis Data Types 공식 문서
'개발 > REDIS' 카테고리의 다른 글
| #9 💾 퍼시스턴스: RDB·AOF·복구 체크 (0) | 2026.03.19 |
|---|---|
| #8 ⚔️ 동시성 제어: 트랜잭션(MULTI/EXEC)·WATCH (0) | 2026.03.19 |
| #6🏆 Sorted Sets: 랭킹/리더보드 (1) | 2026.03.19 |
| #5 📬 Lists·Sets: 큐와 중복 방지 (0) | 2026.03.19 |
| #4 List와 Queue 활용하기: 비동기 작업 큐 만들기 (0) | 2026.03.17 |