Redis Pub/Sub 제대로 알기: 기본 개념부터 운영 노하우까지
1. Redis Pub/Sub이란
1-1. 기본 개념: 라디오 방송처럼 동작하는 메시징
Pub/Sub은 Publish(발행)와 Subscribe(구독)를 합친 말입니다. 라디오 방송을 떠올리면 쉽습니다.
- 방송국(발행자)이 특정 주파수(채널)로 방송을 내보냅니다.
- 그 주파수를 맞춰놓은 청취자(구독자)는 방송을 실시간으로 듣습니다.
- 방송국은 누가 듣고 있는지 몰라도 되고, 청취자도 방송국이 어디 있는지 몰라도 됩니다.
Redis Pub/Sub도 동일합니다. 발행자가 PUBLISH channel message 명령으로 채널에 메시지를 보내면, 그 채널을 SUBSCRIBE channel로 구독 중인 모든 클라이언트가 즉시 메시지를 받습니다. 발행자와 구독자가 서로의 존재를 몰라도 되기 때문에 시스템 간 결합도(Coupling)가 크게 낮아집니다.
여기서 흔히 헷갈리는 게 큐(Queue)와의 차이입니다.
| 구분 | Pub/Sub | 큐(Queue) |
|---|---|---|
| 메시지 전달 대상 | 구독 중인 모든 클라이언트 (1:N 브로드캐스트) | 여러 워커 중 하나만 소비 (1:1 분배) |
| 예시 | “새 주문 발생” 이벤트를 알림/로그/통계 서버가 각자 받아 처리 | “이미지 리사이징 작업”을 워커 풀 중 하나가 가져가 처리 |
즉, 여러 소비자가 같은 메시지를 각자 다른 목적으로 받아야 하면 Pub/Sub, 여러 워커가 작업을 나눠서 처리해야 하면 큐(예: Redis Streams, RabbitMQ Queue)가 맞는 선택입니다.
1-2. 핵심 동작 원리
- Fire and Forget (발행 후 방치): 메시지는 Redis 메모리에 저장되지 않습니다. 발행 시점에 구독 중인 클라이언트에게 전달되고 즉시 소멸합니다. 구독자가 그 순간 연결되어 있지 않았다면 메시지는 그냥 사라집니다.
- PUSH 방식 전달: 구독자가 메시지를 주기적으로 폴링(Polling)하는 게 아니라, Redis 서버가 연결된 클라이언트에게 메시지를 즉시 밀어넣어(Push) 줍니다. 그래서 지연이 거의 없습니다.
- 단방향 채널 연결: 클라이언트가
SUBSCRIBE를 실행하면 해당 커넥션은 ‘구독 전용 상태’로 전환되어, 같은 커넥션에서 일반 Key-Value 명령(GET,SET등)을 실행할 수 없습니다. 그래서 애플리케이션에서는 Pub/Sub용 커넥션을 별도로 열어야 합니다.
1-3. 실무에서 언제 쓰나
- 실시간 알림: 특정 이벤트(주문 완료, 결제 승인 등)가 발생하면 알림 서버가 즉시 반응해 푸시 발송
- 채팅 온라인 상태 동기화: 서버가 여러 대로 스케일아웃되어 있을 때, “A 유저가 온라인이 됨”을 모든 서버 인스턴스에 브로드캐스트해서 상태를 맞춤
- 캐시 무효화(Cache Invalidation) broadcast: 서버 인스턴스마다 로컬 캐시(in-memory cache)를 갖고 있을 때, 데이터가 변경되면 “이 키는 무효화하라”는 메시지를 모든 인스턴스에 뿌려 동시에 캐시를 비움
- WebSocket 서버 간 메시지 릴레이: WebSocket 연결이 여러 서버에 분산되어 있을 때, 특정 유저에게 메시지를 보내려면 그 유저가 연결된 서버가 어디인지 몰라도 Pub/Sub으로 모든 서버에 뿌리고 해당 서버만 실제 전달
공통점은 “메시지가 한 번 유실돼도 크게 문제없는, 실시간성이 중요한 브로드캐스트” 라는 것입니다.
2. 운영 시 반드시 주의해야 할 5가지 장애 포인트
2-1. 메시지 유실 위험 (At-most-once 보장)
기초 개념: 앞서 말했듯 Pub/Sub은 메시지를 저장하지 않는 “휘발성” 구조입니다. 구독자의 네트워크가 잠깐 끊기거나, 배포 도중 서버가 재시작되는 그 순간에 발행된 메시지는 영구적으로 유실됩니다. 다시 연결해도 지나간 메시지를 받을 방법이 없습니다.
실전 사례: 배포 중 애플리케이션 서버가 재시작되면서 그 사이 발생한 캐시 무효화 메시지를 놓쳐, 배포 후 일부 인스턴스가 낡은 캐시를 계속 서빙하는 문제가 흔히 발생합니다. 이런 경우 배포 파이프라인에 “재시작 후 캐시 강제 새로고침” 단계를 추가하거나, 아예 유실이 허용되지 않는 데이터는 처음부터 Pub/Sub을 쓰지 않는 게 맞습니다.
대처: 유실되어도 무방한 실시간 알림/브로드캐스트에만 사용해야 합니다. 결제/주문처럼 메시지 손실이 절대 없어야 하는 시스템에는 Redis Streams, Kafka, RabbitMQ처럼 메시지를 디스크나 메모리에 저장하고 재전송을 지원하는 도구를 도입해야 합니다.
2-2. Out of Memory (OOM) 및 Outgoing Buffer 폭발
기초 개념: 발행자가 메시지를 보내는 속도보다 구독자가 받아서 처리하는 속도가 느리면, Redis는 아직 전달하지 못한 메시지를 클라이언트별 출력 버퍼(Output Buffer)에 쌓아둡니다. 구독자가 계속 느리면 이 버퍼가 무한정 커지다가 Redis 서버 전체 메모리를 고갈시킬 수 있습니다.
실전 사례: 구독자 애플리케이션이 GC pause나 배포 중 순간적으로 응답을 멈추는 사이, 발행 트래픽이 몰리는 시간대(예: 이벤트 시작 직후 대량 알림)와 겹치면 버퍼가 수십 MB까지 순식간에 쌓이는 경우가 있습니다. 이때 버퍼 제한이 없으면 Redis가 OOM으로 다운되어 Pub/Sub과 무관한 다른 서비스(캐시, 세션 등)까지 함께 장애가 납니다.
대처: Redis 설정에서 pubsub 전용 출력 버퍼 제한을 반드시 걸어야 합니다.
client-output-buffer-limit pubsub 32mb 8mb 60
# 버퍼가 32MB를 넘거나, 8MB 이상 상태가 60초 지속되면 해당 클라이언트 강제 끊기
느린 구독자를 조기에 발견하려면 INFO clients의 output_buffer 관련 지표나 CLIENT LIST를 모니터링에 연동해, 특정 클라이언트의 버퍼가 임계치에 근접하면 알람이 뜨도록 구성하는 것이 좋습니다.
2-3. Redis Cluster 환경에서의 전체 노드 브로드캐스팅
기초 개념: Redis Cluster는 데이터를 여러 노드에 슬롯(Slot) 단위로 분산 저장합니다. 그런데 일반 Pub/Sub은 이 슬롯 개념과 무관하게 동작해서, 특정 노드의 채널에만 PUBLISH를 해도 클러스터 내 모든 노드로 메시지가 전파됩니다. 채널 수와 트래픽이 늘어나면 노드 간 통신(Gossip)으로 네트워크 대역폭이 급증합니다.
실전 사례: 클러스터 노드가 6대인 서비스에서 초당 수백 건의 채팅 메시지를 Pub/Sub으로 처리했는데, 실제로는 메시지가 6배로 복제되어 노드 간 트래픽이 증폭되는 걸 나중에 발견한 경우가 있습니다. 노드 수가 늘어날수록 이 증폭 비율도 커지기 때문에, 클러스터 규모를 확장할 계획이 있다면 미리 대비해야 합니다.
대처: Redis 7.0부터 지원하는 Sharded Pub/Sub(SPUBLISH, SSUBSCRIBE)을 사용하면, 채널이 특정 슬롯에 매핑되어 그 슬롯을 담당하는 샤드(마스터+레플리카) 안에서만 메시지가 유통됩니다. 채널명이 키처럼 해시 슬롯에 매핑되는 방식이라, 클러스터 규모가 커질수록 효과가 큽니다. 다만 Sharded Pub/Sub은 패턴 구독(PSUBSCRIBE 방식)을 지원하지 않으므로, 기존에 패턴 구독을 쓰고 있었다면 채널 구조를 먼저 정리해야 마이그레이션할 수 있습니다.
2-4. 커넥션 리소스 관리
기초 개념: 앞서 1-2에서 봤듯 SUBSCRIBE를 실행한 커넥션은 일반 명령을 쓸 수 없는 전용 상태가 됩니다. 그래서 구독자 서버는 Pub/Sub만을 위한 별도 TCP 커넥션을 계속 유지해야 하고, 이 커넥션은 Redis의 전체 연결 수(maxclients)를 소비합니다.
실전 사례: 구독자 역할을 하는 서버 인스턴스를 오토스케일링으로 늘렸는데, 인스턴스마다 Pub/Sub 커넥션을 하나씩 새로 열면서 maxclients 한도에 근접해 신규 연결이 거부되는 장애가 발생할 수 있습니다.
대처: 애플리케이션에서는 일반 쿼리용 커넥션 풀(Connection Pool)과 Pub/Sub 전용 커넥션을 명확히 분리해서 관리해야 합니다. 예를 들어 NestJS + ioredis 조합이라면 일반 명령용 Redis 클라이언트와, subscribe만 담당하는 별도 Redis 클라이언트 인스턴스를 나눠서 생성하는 식입니다. 인스턴스 수가 늘어날 때 커넥션 수가 선형으로 늘어나는지 미리 계산해 maxclients 여유분을 확보해야 합니다.
2-5. 블로킹 및 패턴 구독(PSUBSCRIBE) 성능 저하
기초 개념: PSUBSCRIBE news.*처럼 와일드카드로 채널을 구독하면, Redis는 메시지가 발행될 때마다 등록된 모든 패턴을 하나씩 대조하는 선형 탐색(O(N))을 수행합니다. 패턴 구독 수가 많아질수록 발행 하나당 CPU 비용이 늘어납니다.
실전 사례: 유저별로 user.{id}.* 같은 패턴을 동적으로 구독하게 설계했다가, 유저 수가 늘면서 패턴 목록이 수만 개로 불어나 단순 채팅 메시지 하나를 발행하는 데도 CPU 사용량이 눈에 띄게 증가한 경우가 있습니다.
대처: 패턴 구독 대신 명확한 채널명을 직접 지정하는 방식을 권장합니다. 위 예시라면 user.{id} 형태로 유저마다 고정 채널을 만들어 정확히 그 채널만 구독하게 하면, 발행 시 O(1)에 가깝게 처리됩니다. 채널 네이밍 컨벤션을 설계 초기에 정해두면 이후 패턴 구독으로 우회할 필요가 없어집니다.
3. 기술 비교: Pub/Sub vs Streams vs Kafka/RabbitMQ
| 구분 | Redis Pub/Sub | Redis Streams | Kafka / RabbitMQ |
|---|---|---|---|
| 메시지 저장 | X (즉시 휘발) | O (메모리/디스크) | O (디스크) |
| 재전송 지원 | 불가 | 가능 (Consumer Group) | 가능 |
| 주요 목적 | 단순 실시간 브로드캐스팅 | 메시지 큐, 대용량 로그 | 복잡한 이벤트 스트리밍, 보장된 큐 |
선택 기준은 결국 “메시지가 유실돼도 되는가”와 “운영 복잡도를 얼마나 감당할 수 있는가”입니다.
- 실시간 알림, 온라인 상태 동기화처럼 유실 허용 + 단순한 브로드캐스트만 필요하면 → Pub/Sub
- 유실은 안 되지만 Redis 인프라를 이미 쓰고 있어 별도 시스템 도입이 부담되면 → Streams
- 트래픽 규모가 크고, 여러 팀/서비스가 함께 쓰는 범용 이벤트 백본이 필요하면 → Kafka / RabbitMQ
핵심요약
- Redis Pub/Sub은 발행자와 구독자를 결합도 없이 연결하는 실시간 브로드캐스트 메시징 패턴이며, 메시지를 저장하지 않고 즉시 Push로 전달한다.
- 메시지 유실(At-most-once), OOM/버퍼 폭발, 클러스터 전체 브로드캐스팅, 커넥션 리소스 소비, 패턴 구독 성능 저하가 5대 운영 장애 포인트다.
- 각 문제는 유실 허용 범위 내 사용,
client-output-buffer-limit설정, Sharded Pub/Sub(Redis 7+) 도입, 커넥션 풀 분리, 채널명 직접 지정으로 대처할 수 있다. - 메시지 손실이 절대 허용되지 않는 경우는 Pub/Sub이 아니라 Redis Streams나 Kafka/RabbitMQ를 선택해야 한다.