진행하고 있는 프로젝트의 운영환경의 서버가 다중 인스턴스로 되어 있다. 한 서버로의 변경 요청이 왔을 때, 이를 SSE로 해당 서버와 연결된 모든 클라이언트에 전달하고 있었는데, 다른 서버에 연결된 클라이언트들은 해당 변경 사항을 전달받지 못하는 문제가 생겼다. 해당 문제를 해결하기 위해 메시지 브로커 방식을 고려해 보았고, Redis Pub/Sub을 활용한 메시징 큐 방식을 적용할 수 있었다. 이 과정에서 학습했던 Redis의 Pub/Sub 내용에 대해 정리해보고자 글을 쓴다.
참고한 자료는 Redis 공식문서, 개발자를 위한 레디스다.
1. Redis란?
https://persi0815.tistory.com/33
2. Redis Pub/Sub이란?
Redis의 Pub/Sub은 매우 가볍게 최소한의 메시지 전달 기능을 제공한다.
발행자는 메시지를 수신자에게 직접 보내지 않고, 어떤 구독자가 있는지 모르는 상태에서 메시지를 특정 채널로 보낸다. 그러면 하나 이상의 채널에 관심을 표하는 구독자는 자신이 관심있는 채널의 메시지만 수신한다. 이처럼 발행자와 구독자가 서로를 모르는 형태를 '비동기적 분리(Decouplig)'이라고 하는데, 이를 통해 네트워크 구조를 훨씬 더 유연하게 가져갈 수 있고, 확장성 또한 좋다.
다만, 소비자에게 모두 정상적으로 전달이 됐는지, 누가 해당 메시지를 발행했는지를 확인할 수 없고, 발행자가 특정한 '채널'에 데이터를 전송하면 채널을 듣고 있는 모든 소비자에게 데이터가 발행된 순서대로 전파(push)된 뒤 삭제되는 일회성을 가진다. 즉, at-most-once 전달 방식을 취하고, 메시지가 잘 전달됐는지에 대한 정보를 보장하지 않는 fire-and-forget 이라는 비동기 프로그래밍의 디자인 패턴을 따른다. 그래서 작업의 완료나 결과에 대한 처리가 필요하지 않을 때 유용하게 사용된다.
*반대로 Redis stream은 데이터가 게속해서 추가되는 방식으로 저장된다.(append-only)

이러한 특징들로 인해, 공식문서에는 다음과 같이 나와있다.
If your application requires stronger delivery guarantees, you may want to learn about Redis Streams. Messages in streams are persisted, and support both at-most-once as well as at-least-once delivery semantics.
사용자의 애플리케이션이 절대 메시지를 놓치면 안되는 상황이라면 Redis Streams를 권장하고 있다는 것을 확인할 수 있다. 이와 관련한 이유가 발생해서 Redis Streams도 사용했는데, 궁금하다면 관련 글을 참고 바란다.
Redis Pub/Sub은 모든 응답을 3개의 요소로 이루어진 배열 형태로 보내는데, 첫 번째 항목이 메시지 유형을 나타낸다.
- 성공적으로 채널을 구독했을 때 : 1. subscribe 2. 구독한 채널 이름 3. 구독중인 총 채널 개수
- 성공적으로 채널 구독을 해지했을 때 : 1. unsubscribe 2. 해지한 채널 이름 3. 현재 남아있는 구독 채널 수
- 다른 클라이언트가 PUBLISH로 보낸 메시지를 수신할 때 : 1. message 2. 메시지가 발생한 채널 이름 3. 실제 메시지 내용
구독시와 해지 시에 응답의 세 번째 요소로 전달되는 '구독 수'는 현재 구독 중인 채널 수와 현재 구독중인 패턴 수를 합친 숫자이다.
하나 이상이 채널을 구독 중인 클라이언트는 '구독 전용 상태'가 되는데, RESP2 프로토콜에서는 '구독 전용 상태'에서 구독 및 해지, 연결 확인, 종료 명령어만 사용할 수 있다. 하지만, RESP3 프로토콜(Redis 6.0 ~ 가능)에서는 해당 제약이 사라져 '구독 전용 상태' 에서 어떤 명령이든 자유롭게 발행할 수 있게 되었다.(RESP versions 공식 문서) RESP2의 경우, 응답으로 전달되는 구독 수가 정확히 '0'이 되어야 Pub/Sub 상태를 완전히 종료하고 다시 일반 모드로 돌아오게 된다. 이때 만일 구독중에 데이터 조회를 하고자 한다면, 다른 연결(다른 소켓)을 맺어서 조회를 해와야했다. RESP3에서는 연결 하나로 푸시 메시지와 일반 메시지 모두를 받을 수 있기에 이들을 구분하기 위해 Redis가 별도의 태그(Push Type)를 붙여서 보낸다.
Redis의 Pub/Sub은 일반적인 데이터(key-value)가 저장되는 공간과 분리되어 설계되어있다. 그리고 Redis는 보통 db 0부터 db 15까지 번호로 구분된 데이터베이스 공간을 가지는데, Pub/Sub은 해당 경계를 무시한다.
3. Redis Pub/Sub 커맨드
발행
PUBLISH channel_name message // channel_name이라는 채널에 message 발행
-> (integer) 1 // 해당 채널의 구독자의 수 반환
구독
SUBSCRIBE chan1 chan2 // chan1와 chan2 모두를 구독
PSUBSCRIBE mail-* // mail-* 패턴(glob-style)에 일치하는 채널을 한번에 구독.
PSUBSCRIBE로 구독하면, 메시지가 pmessage 타입으로 전달된다.
message는 '1. message 2. 메시지가 발생한 채널 이름 3. 실제 메시지 내용' 이렇게 3 요소로 전달이 된다면,
pmessage는 '1. pmessage 2. 설정했던 원래의 패턴(ex.mail-*) 3. 메시지가 발생한 채널 이름 4. 실제 메시지 내용'으로 전달이 된다.
이때 SUBSCRIBE로는 message 타입으로 메시지가 전달되기에 두 방식으로 모두 구독을 하면 같은 내용의 메시지가 다른 타입으로 중복되어 전달된다. 이를 통해 알림이 두 번 가거나 데이터가 중복으로 db에 쌓이는 등 문제가 발생할 수 있다.
해지
UNSUBSCRIBE chan1
PUNSUBSCRIBE mail-*
연결 확인
PING
Heartbeat 용도로 쓰이며, 이에 대한 응답으로 PONG이라는 응답이 온다.
종료
QUIT
이에 대한 응답으로 OK가 오고, 즉시 연결(소켓)을 닫는다. 구독을 일일이 하나씩 해지(UNSUBSCRIBE)하기 귀찮거나 접속을 끊고 싶을 때 사용한다.
각 커멘드의 내부 동작 구조에 대해 더 알고 싶다면, 아래 글을 참고하자.
https://redisgate.kr/redis/command/pubsub_internal.php
4. Shared Redis Pub/Sub이란?
대규모 서비스에서는 데이터를 분산해서 저장하고 처리하기 위해 Cluster 구조를 도입한다. 이때, 데이터가 발행이 되면 모든 노드로 메시지가 전파가 되어 불필요한 복제가 생겨 노드가 많아질수록 클러스터 내부 통신망에 부하가 크게 커지는 문제가 있었다. 이를 해결하기 위해 Shared Pub/Sub 기능이 Redis 7.0부터 도입되었고, 이는 같은 slot을 가지고 있는 노드 간에만 Pub/Sub으로 메시지가 전파가 된다.

과정을 좀 더 자세히 보자면,
1. Shared Channel을 특정 slot에 할당한다.
2. 메시지를 발행할 때는 해당 채널이 속한 slot을 관리하는 노드로 보낸다.
3. 클러스터는 발행된 메시자가 해당 샤드(Shard)내의 모든 노드에게만 전달되도록 한다.
4. 구독자는 해당 slot을 담당하는 마스터 노드나 그 노드의 복제본(Replica) 중 아무 곳에나 연결하여 메시지를 받을 수 있다.
이를 통해 클러스터 전체에 흐르는 데이터 양이 줄어들 수 있었고, 노드를 추가할수록 처리 능력을 수평적으로 확장할 수 있게 되었다.
5. Redis keyspace notifications (문서)
이건 처음 알게된 기능이었는데, 꽤나 유용해 보여서 기록한다.
Redis는 데이터를 저장하고 조회하는 캐시로써 널리 이용되고 있다. 그래서인지 Redis의 값이 변경이 되거나 삭제되는 순간 로그를 찍거나 DB에 변화를 주고 싶은 순간들이 많았다.
예를 들어 조회 성능 향상용으로 Redis를 사용하고, Write Through 캐시 전략을 사용한다면, 캐시 값을 DB보다 먼저 변경하게 되는데, 이때 매번 로직적으로 Redis의 값을 삭제하거나 변경하고 나서 DB의 값도 갱신해주어야 한다. 그런데, 만일 관심사의 분리처럼 Redis가 데이터의 변경사항을 전달하고, 해당 정보를 가지고 DB나 로컬 캐시 값을 수정하는 파이프라인을 따로 만든다면? 중복되는 로직이 많이 사라질 것 같았다.
그런데, 이러한 기능을 Redis Keyspace Notifications가 제공하고 있었고!!! 이에 대해 자세히 알아보자.
먼저 왜 Pub/Sub 관련 글에서 해당 기능을 설명하는지부터 말하자면, Pub/Sub 기능을 사용해서 변경 사항을 소비자에게 전달하는 방식으로 동작하기 때문이다. 하지만, Pub/Sub 기반이기에 유실 가능성이 있다는 것을 인지해야 하고, DB 동기화 처럼 정합성이 중요하다면, 추가적으로 Batch 작업 등으로 정합성을 맞추는 보완책이 필요할 수 있다는 점을 짚고 넘어가자.
Redis는 하나의 사건을 두 가지 시각으로 방송한다.
- Key-space Notification: 키(Key) 중심이다. "특정 키"에 일어나는 모든 일을 감시할 때 사용한다.
- Key-event Notification: 이벤트(Event) 중심이다. "특정 명령어"가 어떤 키들에 영향을 줬는지 감시할 때 사용한다.
Redis의 데이터가 변할 때마다 모든 정보를 소비자(서버)에게 전달하면 양이 많아 Redis 서버와 네트워크 성능에 영향을 줄 수 있다. 그래서 KEA(Key-space& Key-event All)을 쓰기보다는 필요한 명령어만 설정해서 해당 정보만 골라 받는 것이 좋을 것 같다.
K Keyspace events, published with __keyspace@<db>__ prefix.
E Keyevent events, published with __keyevent@<db>__ prefix.
g Generic commands (non-type specific) like DEL, EXPIRE, RENAME, ...
$ String commands
l List commands
s Set commands
h Hash commands
z Sorted set commands
t Stream commands
d Module key type events
x Expired events (events generated every time a key expires)
e Evicted events (events generated when a key is evicted for maxmemory)
m Key miss events generated when a key that doesn't exist is accessed (Note: not included in the 'A' class)
n New key events generated whenever a new key is created (Note: not included in the 'A' class)
o Overwritten events generated every time a key is overwritten (Note: not included in the 'A' class)
c Type-changed events generated every time a key's type changes (Note: not included in the 'A' class)
A Alias for "g$lshztdxe", so that the "AKE" string means all the events except "m", "n", "o" and "c".
K 또는 E는 필수로 선택해서 알림의 방향성(키 중심 vs 명령어 중심)을 결정하고, 그 다음에 아래 소문자나 특수문자로 된 기능을 함께 선택해주면 된다. 예시는 다음과 같다.
- 리스트(List)의 변화만 키 중심(Keyspace)으로 보고 싶다 ➔ Kl
- 데이터가 만료(Expired)되는 것만 사건 중심(Keyevent)으로 보고 싶다 ➔ Ex
- 모든 종류의 이벤트를 두 가지 방식으로 다 보고 싶다 ➔ KEA
기본적으로 keyspace notifications 기능은 비활성화되어 있기에 사용하기 위해서는 활성화 설정을 해줘야 한다.
활성화 방법으로는 두가지가 있는데, 첫번째는 CONFIG SET 명령어를 사용하는 것이고, 두번째는 redis.conf의 notify-keyspace-events를 사용하는 것이다.
공식문서의 예시를 통해 살펴보자.
1. CONFIG SET notify-keyspace-events KEg (K, E, g 활성화)으로 설정을 해주면,
2. Redis는 두 개의 채널에 동시에 메시지를 던진다.
- PUBLISH __keyspace@0__:mykey del (키 중심)
- PUBLISH __keyevent@0__:del mykey (이벤트 중심)
Spring Boot에서 Redis Pub/Sub을 사용하는 예제는 다음을 참고 바란다.
https://persi0815.tistory.com/164
[NoSQL] Redis Pub/Sub 이론부터 코드까지! A to Z (2)
진행하고 있는 프로젝트의 운영환경의 서버가 다중 인스턴스로 되어 있다. 한 서버로의 변경 요청이 왔을 때, 이를 SSE로 해당 서버와 연결된 모든 클라이언트에 전달하고 있었는데, 다른 서버에
persi0815.tistory.com
'공부 > NoSQL' 카테고리의 다른 글
| [NoSQL] Redis Pub/Sub 이론부터 코드까지! A to Z (2) (0) | 2025.12.18 |
|---|---|
| [Redis] Refresh Token Redis에 넣어보기 (0) | 2024.07.08 |
| [Redis] FCM Token Redis에 넣어보기 (1) | 2024.07.08 |