Redis Pub/Sub(Publish/Subscribe)은 메시지를 발행하는 Publisher와 메시지를 구독하는 Subscriber를 분리하여 통신할 수 있도록 해주는 메시징 기능이다.
Publisher는 특정 Subscriber를 직접 알 필요 없이 Channel에 메시지를 발행(Publish)하고, 해당 Channel을 구독하고 있는 Subscriber들이 메시지를 전달받는다.
즉, 다음과 같은 구조이다.
Publisher
│
│ PUBLISH
▼
Redis Channel
│
├──────────────┐
▼ ▼
Subscriber A Subscriber B
│ │
└── 메시지 수신 ──┘
Publisher와 Subscriber가 서로 직접 연결되지 않는다는 점이 핵심이다.
일반적인 애플리케이션에서는 한 컴포넌트가 다른 컴포넌트에게 직접 메시지를 전달하는 방식이 흔하다.
예를 들어 채팅 애플리케이션에서 사용자가 메시지를 전송하면 다음과 같은 구조가 될 수 있다.
Client A
│
▼
Web Server 1
│
▼
Client B
그런데 서버가 여러 대로 확장되면 문제가 발생할 수 있다.
┌─ Web Server 1 ─ Client A
Client ───────┤
├─ Web Server 2 ─ Client B
└─ Web Server 3 ─ Client C
Client A와 Client B가 서로 다른 서버에 연결되어 있다면,
Server 1에서 발생한 메시지를 Server 2에 어떻게 전달할 것인가?
이때 Redis Pub/Sub을 사용할 수 있다.
Redis
┌─────────┐
│ Channel │
└─────────┘
▲ ▲ ▲
│ │ │
│ │ │
Publish Subscribe
│ │ │
│ │ │
┌─────┘ │ └─────┐
▼ ▼ ▼
Server 1 Server 2 Server 3
각 서버가 동일한 Channel을 구독하고 있다면 어느 서버에서 메시지를 발행하더라도 해당 Channel을 구독하는 서버들이 메시지를 받을 수 있다.
Redis Pub/Sub에서는 크게 세 가지 개념을 이해하면 된다.
메시지를 발행하는 주체이다.
PUBLISH chat "Hello"
Publisher는 누가 해당 메시지를 받을지 알 필요가 없다.
메시지가 전달되는 논리적인 통로이다.
예를 들어:
chat
notification
order
payment
와 같은 Channel을 만들 수 있다.
Publisher
│
│ "Hello"
▼
"chat"
│
├── Subscriber A
├── Subscriber B
└── Subscriber C
특정 Channel을 구독하고 메시지를 받는 주체이다.
SUBSCRIBE chat
이후 chat Channel에 메시지가 발행되면 해당 Subscriber가 메시지를 전달받는다.
Redis CLI를 사용하면 Pub/Sub을 간단하게 확인할 수 있다.
먼저 Subscriber에서 다음 명령을 실행한다.
SUBSCRIBE chat
그러면 Redis는 해당 클라이언트가 chat Channel을 구독하고 있다는 상태를 유지한다.
다른 Redis 클라이언트에서 다음 명령을 실행한다.
PUBLISH chat "Hello Redis!"
그러면 Redis가 chat을 구독하고 있는 Subscriber에게 메시지를 전달한다.
결과적으로:
Publisher
PUBLISH chat "Hello Redis!"
│
▼
┌─────────────────┐
│ Redis │
│ │
│ Channel: chat │
└─────────────────┘
│
├─────────────► Subscriber A
│
├─────────────► Subscriber B
│
└─────────────► Subscriber C
Redis Pub/Sub에서 가장 중요한 특징 중 하나이다.
Publisher가 메시지를 발행했을 때 그 순간 Channel을 구독하고 있는 Subscriber에게 메시지를 전달할 뿐, 메시지를 저장하지 않는다.
예를 들어:
10:00:00
Subscriber A 연결
10:00:01
Publisher → "Hello"
10:00:02
Subscriber A 연결 종료
10:00:03
Publisher → "World"
10:00:04
Subscriber A 다시 연결
Subscriber A가 10:00:03에 발행된 "World" 메시지를 받을 수 있을까?
받을 수 없다.
왜냐하면 Pub/Sub은 메시지를 저장하지 않기 때문이다.
Publisher
│
│ "World"
▼
Redis
│
└── 현재 구독 중인 Subscriber에게만 전달
Subscriber가 연결되어 있지 않았다면 메시지는 사라진다.
Pub/Sub은 메시지의 유실을 어느 정도 허용할 수 있고, 실시간 전달이 중요한 상황에 적합하다.
대표적으로:
등에 사용할 수 있다.
예를 들어 채팅 서버에서는:
Client A
│
│ 메시지 전송
▼
Server 1
│
│ PUBLISH
▼
Redis
│
│
├────────► Server 1
├────────► Server 2
└────────► Server 3
│
▼
Client B
여러 서버가 같은 Channel을 구독하고 있으면 서버 간 메시지 전달에 사용할 수 있다.
Redis Pub/Sub은 기본적으로 Broadcast 방식으로 동작한다.
예를 들어:
Channel: chat
Subscriber A
Subscriber B
Subscriber C
가 모두 chat을 구독하고 있을 때,
PUBLISH chat "Hello"
를 실행하면 A, B, C 모두 메시지를 받는다.
"Hello"
│
▼
Redis
┌──────┼──────┐
▼ ▼ ▼
A B C
즉,
"이 메시지를 한 명에게 전달한다"
가 아니라
"이 Channel을 구독하고 있는 모든 Subscriber에게 전달한다"
라는 개념이다.
Pub/Sub을 이해할 때 Message Queue와 비교하면 훨씬 쉽다.
Message
│
▼
Redis
┌─────┼─────┐
▼ ▼ ▼
A B C
모든 Subscriber가 메시지를 받는다.
Queue에서는 일반적으로 하나의 메시지를 하나의 Consumer가 처리하는 형태를 생각할 수 있다.
Message
│
▼
Queue
│
┌─────┼─────┐
▼ ▼ ▼
Worker Worker Worker
메시지를 분산 처리하는 목적에 적합하다.
따라서 둘의 목적이 다르다.
| 특징 | Pub/Sub | Queue |
|---|---|---|
| 전달 방식 | Broadcast | Consumer에게 분배 |
| 메시지 저장 | X | 시스템에 따라 O |
| 늦게 접속한 Consumer | 과거 메시지 수신 불가 | 저장되어 있다면 가능 |
| 실시간성 | 높음 | 높거나 중간 |
| 메시지 유실 | 가능 | 구현에 따라 방지 가능 |
| 대표 사용 사례 | 채팅, 실시간 알림 | 작업 처리, 비동기 작업 |
Redis에서 메시징을 구현할 때는 Pub/Sub뿐만 아니라 Redis Streams도 사용할 수 있다.
가장 큰 차이는 메시지를 저장하느냐이다.
Publisher
│
▼
Redis
│
▼
Subscriber
메시지 전달 후 저장하지 않음
Publisher
│
▼
Redis Stream
│
├── Consumer A
├── Consumer B
└── Consumer C
메시지를 Stream에 저장
따라서 다음과 같이 생각하면 쉽다.
Pub/Sub = 실시간 방송
Stream = 기록이 남는 메시지 스트림
메시지 유실이 허용되지 않거나 나중에 다시 처리해야 한다면 Redis Streams 같은 영속적인 메시징 방식을 고려하는 것이 좋다.
Redis의 기본적인 Pub/Sub 명령어만으로 사용할 수 있다.
SUBSCRIBE chat
PUBLISH chat "Hello"
구조가 매우 단순하다.
Redis는 메모리 기반 데이터 저장소이기 때문에 메시지를 매우 빠르게 전달할 수 있다.
따라서 실시간성이 중요한 시스템에 적합하다.
Publisher가 Subscriber의 위치나 개수를 알 필요가 없다.
Publisher
│
▼
Redis Channel
│
├── Subscriber A
├── Subscriber B
└── Subscriber C
Publisher는 단순히 Channel에 메시지를 발행하면 된다.
WebSocket 서버가 여러 대 존재할 때 Redis를 중간 메시지 브로커처럼 활용할 수 있다.
Redis
chat channel
▲ ▲ ▲
│ │ │
Server1 Server2 Server3
│ │ │
Client Client Client
이런 구조를 통해 서버 간 이벤트 전달을 구현할 수 있다.
가장 중요한 단점이다.
Subscriber가 연결되어 있지 않은 순간에 메시지가 발행되면 메시지를 받을 수 없다.
Publisher
│
▼
Redis
│
X
Subscriber 없음
→ 메시지 유실
따라서 절대 유실되면 안 되는 메시지에는 적합하지 않을 수 있다.
Pub/Sub은 메시지를 저장하지 않기 때문에,
"어제 발생했던 메시지를 다시 처리해줘"
같은 요구사항을 구현하기 어렵다.
Redis Pub/Sub 자체는 다음과 같은 것을 관리하지 않는다.
즉, 단순한 메시지 전달 기능에 가깝다.
Spring Boot에서는 Redis를 이용해서 Pub/Sub 구조를 구현할 수 있다.
일반적인 구조는 다음과 같다.
┌──────────────────────┐
│ Publisher │
│ │
│ RedisTemplate │
└──────────┬───────────┘
│
│ publish
▼
┌──────────────────────┐
│ Redis │
│ │
│ Channel │
└──────────┬───────────┘
│
│ subscribe
▼
┌──────────────────────┐
│ Subscriber │
│ │
│ MessageListener │
└──────────────────────┘
Spring에서는 RedisTemplate을 이용해서 메시지를 발행할 수 있다.
예를 들어:
@Service
@RequiredArgsConstructor
public class RedisPublisher {
private final RedisTemplate<String, Object> redisTemplate;
public void publish(String channel, Object message) {
redisTemplate.convertAndSend(channel, message);
}
}
사용할 때는:
redisPublisher.publish(
"chat",
new ChatMessage("hello")
);
와 같이 사용할 수 있다.
내부적으로는 Redis의 PUBLISH와 비슷한 역할을 한다.
Subscriber는 Redis의 Channel을 구독하고 메시지가 들어왔을 때 처리한다.
Spring에서는 MessageListener 등을 사용할 수 있다.
@Component
public class RedisSubscriber implements MessageListener {
@Override
public void onMessage(Message message, byte[] pattern) {
String body = new String(message.getBody());
System.out.println("받은 메시지: " + body);
}
}
그리고 해당 Listener가 특정 Channel을 구독하도록 설정한다.
Spring에서는 RedisMessageListenerContainer를 이용해서 Redis Pub/Sub Listener를 관리할 수 있다.
개념적으로 다음과 같은 구조이다.
Redis
│
│ Message
▼
RedisMessageListenerContainer
│
▼
MessageListener
│
▼
Business Logic
예를 들어:
@Bean
public RedisMessageListenerContainer redisMessageListener(
RedisConnectionFactory connectionFactory,
MessageListenerAdapter listenerAdapter,
ChannelTopic topic
) {
RedisMessageListenerContainer container =
new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
container.addMessageListener(listenerAdapter, topic);
return container;
}
이런 식으로 Redis Channel과 Listener를 연결할 수 있다.
Redis Pub/Sub의 대표적인 사용 사례가 WebSocket 기반 채팅 시스템이다.
예를 들어 서버가 3대 있다고 가정한다.
Redis
chat:room:1
▲ ▲ ▲
│ │ │
Server1 Server2 Server3
│ │ │
User A User B User C
User A가 Server 1에 연결되어 있고,
User B가 Server 2에 연결되어 있다고 하자.
User A가 메시지를 전송하면:
User A
│
▼
Server 1
│
│ PUBLISH
▼
Redis
│
│ chat:room:1
▼
Server 2
│
▼
User B
이렇게 다른 서버에 연결된 User B에게도 메시지를 전달할 수 있다.
Channel을 어떻게 설계할지도 중요하다.
예를 들어 채팅방마다 Channel을 만든다면:
chat:room:1
chat:room:2
chat:room:3
와 같이 사용할 수 있다.
특정 채팅방의 메시지는 해당 방의 Channel에 발행한다.
User
│
▼
chat:room:1
│
├── Server 1
└── Server 2
이렇게 하면 room:1을 구독한 서버들만 메시지를 받게 된다.
Redis Pub/Sub에서는 특정 Channel뿐만 아니라 패턴을 이용해서 구독할 수도 있다.
예:
PSUBSCRIBE chat:room:*
그러면 다음과 같은 Channel을 모두 받을 수 있다.
chat:room:1
chat:room:2
chat:room:3
즉,
chat:room:*
라는 패턴으로 여러 Channel을 한 번에 구독할 수 있다.
다만 Channel이 매우 많아지고 메시지량이 많아지는 환경에서는 패턴 구독에 따른 부하도 고려해야 한다.
Redis Pub/Sub은 단순하고 빠르지만 다음 질문을 먼저 생각해야 한다.
YES → Pub/Sub 고려
NO → Stream / Message Queue 등 고려
YES → Pub/Sub과 맞지 않음
NO → Pub/Sub 사용 가능
YES → Pub/Sub만으로는 부족할 수 있음
YES → Pub/Sub에 적합
정리하면 다음과 같다.
메시지 전달 필요
│
▼
메시지 저장이 필요한가?
/ \
YES NO
│ │
▼ ▼
Redis Stream Redis Pub/Sub
│
▼
재처리 / 소비 상태 관리
물론 실제 시스템에서는 Kafka, RabbitMQ, SQS 등의 메시징 시스템도 요구사항에 따라 선택할 수 있다.
Redis Pub/Sub은 Publisher와 Subscriber가 Channel을 통해 메시지를 주고받는 실시간 메시징 기능이다.
핵심 특징은 다음과 같다.
Redis Pub/Sub을 처음 공부하면서 가장 중요하게 이해해야 하는 부분은 Redis를 단순한 캐시 저장소가 아니라 서버 간 메시지를 전달해주는 중간 매개체로 사용할 수 있다는 것이다.
특히 서버가 여러 대인 환경에서는 각 서버가 독립적으로 동작하기 때문에 서버 간 실시간 이벤트 전달이 필요하다.
이때 Redis Pub/Sub을 사용하면 다음과 같은 구조를 만들 수 있다.
┌─────────────┐
│ Redis │
│ Pub/Sub │
└──────┬──────┘
│
┌───────────────┼───────────────┐
│ │ │
▼ ▼ ▼
Server 1 Server 2 Server 3
│ │ │
▼ ▼ ▼
Client A Client B Client C
각 서버는 Redis Channel을 구독하고 있다가 메시지가 발생하면 해당 메시지를 자신의 연결된 클라이언트들에게 전달한다.
다만 Redis Pub/Sub은 메시지 브로커의 기능을 모두 제공하는 것이 아니라 "실시간 메시지 전달"에 초점을 맞춘 기능이라는 점을 기억해야 한다.
특히 메시지를 저장하지 않기 때문에,
"메시지를 반드시 전달해야 하는가?"
라는 질문이 Redis Pub/Sub을 사용할지 결정하는 중요한 기준이 된다.
Redis Pub/Sub은 Redis Channel을 중심으로 Publisher가 발행한 메시지를 해당 Channel의 Subscriber들에게 실시간으로 Broadcast하는 메시징 방식이며, 메시지를 저장하지 않기 때문에 빠르고 단순하지만 메시지 유실과 재처리가 필요한 시스템에는 적합하지 않다.