채팅 서버를 스케일 아웃했을 때 발생하는 메세지 전달 문제를 해결하는 방법 중 필요한 개념이 Redis Pub/Sub라는 것을 알게 되었다.
Publisher가 Channel에 메시지를 발행하면 해당 Channel을 구독한 모든 Subscriber가 메시지를 받는다.
하지만 STOMP에서도 이미 SUBSCRIBE와 메시지 전송을 사용하고 있기 때문에 다음과 같은 의문이 생긴다.
STOMP도 구독이고 Redis도 구독이라면 둘은 무엇이 다른가?
결론부터 말하면 두 기술은 서로 다른 구간을 담당한다.
- STOMP는 사용자와 Spring 서버 사이에서 메시지를 주고받기 위한 규칙이다.
- Redis Pub/Sub은 여러 Spring 서버가 메시지를 공유하기 위한 통신 방식이다.
Redis Pub/Sub은 STOMP를 사용하기 위한 필수 기술은 아니다.
Spring 서버가 여러 대로 늘어났을 때 서로 다른 서버에 연결된 사용자에게도 메시지를 전달하기 위해 등장하는 기술이다.
Pub/Sub이란 무엇인가?
Pub/Sub은 Publish와 Subscribe를 합친 말이다.
- Publisher: 메시지를 발행하는 주체 (메세지를 보내는 쪽)
- Channel: 메시지가 발행되는 주제 또는 통로
- Subscriber: 특정 Channel을 구독하고 메시지를 받는 주체 (메세지를 받는 쪽)
Publisher는 메시지를 받을 사람을 직접 지정하지 않고, 특정 Channel에 메시지를 발행한다.
PUBLISH chat:room:1 "안녕하세요"
| 부분 | 의미 |
| PUBLISH | Redis Pub/Sub 메시지 발행 명령어 |
| chat:room:1 | 메시지를 발행할 Channel 이름 |
| "안녕하세요" | 전달할 메시지 내용, 즉 Payload |
Subscriber는 관심 있는 Channel을 미리 구독한다.
SUBSCRIBE chat:room:1
이후 해당 Channel에 메시지가 발행되면 Redis가 현재 구독 중인 모든 Subscriber에게 메시지를 전달한다.
Publisher는 Subscriber가 누구인지 알 필요가 없다.
Subscriber도 메시지를 발행한 Publisher가 누구인지 알 필요가 없다.
둘은 오직 Channel을 기준으로 연결된다.
Pub/Sub의 본질은 정말 1:N일까?
Pub/Sub을 설명할 때 흔히 아는 개념 중에 하나가
Pub/Sub의 본질은 한 명이 메시지를 던지면 N명이 받는 것이다.
이 표현은 Pub/Sub의 동작을 쉽게 이해하기에는 좋지만, 정확한 본질이라고 보기는 어렵다.
Pub/Sub의 진짜 핵심은 다음과 같다.
Publisher와 Subscriber가 서로를 직접 알지 않고 Channel을 기준으로 메시지를 주고받는 구조다.
하나의 Channel을 여러 Subscriber가 구독할 수 있기 때문에 결과적으로 하나의 메시지가 여러 곳에 전달된다.
그러나 항상 한 명이 보내고 여러 명이 받는 것은 아니다.
- 구독자가 없으면 아무도 받지 않는다.
- 구독자가 한 명이면 한 명만 받는다.
- 구독자가 여러 명이면 여러 명이 받는다.
- Publisher도 여러 명일 수 있다.
따라서 Pub/Sub은 실제로 다음과 같은 N:M 구조도 가능하다.
Publisher A ─┐
├─ Channel ─┬─ Subscriber 1
Publisher B ─┘ ├─ Subscriber 2
└─ Subscriber 3
즉, 1:N 전달은 Pub/Sub 구조에서 나타나는 대표적인 결과이고, 더 근본적인 특징은 발행자와 구독자가 서로 분리된다는 것이다.
Redis Key-Value와 Pub/Sub은 무엇이 다를까?
Redis를 캐시로 사용할 때는 일반적으로 Key-Value 형태로 데이터를 저장한다.
SET user:1 "철수"
GET user:1
SET으로 저장한 값은 삭제되거나 TTL이 만료되기 전까지 Redis에 남아 있다. 따라서 나중에 GET으로 조회할 수 있다.
반면 Pub/Sub의 메시지는 저장되지 않는다.
PUBLISH chat:room:1 "안녕하세요"
메시지가 발행되는 순간 해당 Channel을 구독하고 있는 Subscriber에게 전달될 뿐이다.
| 구분 | Redis Key-Value | Redis Pub/Sub |
| 주요 목적 | 데이터 저장과 조회 | 실시간 메시지 전달 |
| 대표 명령 | SET, GET | PUBLISH, SUBSCRIBE |
| 데이터 저장 | 저장됨 | 저장되지 않음 |
| 나중에 조회 | 가능 | 불가능 |
| 통신 구조 | 요청과 응답 | 발행과 구독 |
| 주요 사용처 | 캐시, 세션, 카운터 | 실시간 알림, 서버 간 통신 |
Redis Channel은 데이터를 저장하는 Key가 아니다.
쉽게 비유하면 Key-Value는 물건을 넣어 두는 보관함이고, Pub/Sub Channel은 메시지가 지나가는 방송 채널이다.
Key-Value
데이터 저장 → 나중에 조회
Pub/Sub
메시지 발행 → 현재 구독자에게 즉시 전달 → 메시지 소멸
Redis Pub/Sub 메시지는 왜 사라질까?
Redis Pub/Sub은 메시지를 저장하지 않기 때문에 메시지가 발행되는 순간 구독하고 있어야 받을 수 있다.
1. Subscriber가 chat:room:1 구독
2. Publisher가 메시지 발행
3. Redis가 구독자에게 즉시 전달
4. Subscriber가 메시지 수신
만약 2번에서 메시지가 발행될 때 Subscriber가 연결되어 있지 않다면 해당 메시지는 받을 수 없다.
Subscriber 연결 끊김
↓
Publisher가 메시지 발행
↓
메시지를 받을 구독자가 없음
↓
메시지 소멸
나중에 다시 구독하더라도 지나간 메시지를 받을 수 없다.
Redis Pub/Sub은 메시지를 최대 한 번 전달하는 at-most-once 방식을 사용한다. Redis가 메시지를 보낸 뒤 Subscriber의 네트워크가 끊기거나 처리 과정에서 오류가 발생해도 메시지를 다시 보내주지 않는다.
Spring 서버가 한 대라면 Redis Pub/Sub은 필요 없다
Spring 서버가 한 대라면 모든 사용자의 WebSocket 연결이 같은 서버에 들어 있다.
사용자 A ─┐
사용자 B ─┼─ Spring 서버 1대
사용자 C ─┘
Spring 서버는 현재 자신에게 연결된 WebSocket 세션과 구독 정보를 모두 알고 있다.
채팅방 1 구독자
사용자 A의 WebSocket 연결
사용자 B의 WebSocket 연결
사용자 C의 WebSocket 연결
사용자 A가 메시지를 보내면 같은 서버가 사용자 B와 C의 연결을 찾아 메시지를 전달할 수 있다.
사용자 A
→ Spring 서버
→ 사용자 B, 사용자 C
이 구조에서는 Spring의 Simple Broker와 SimpMessagingTemplate만으로 채팅을 구현할 수 있다.
즉, 서버가 한 대인 상황에서는 Redis Pub/Sub이 없어도 된다.
서버가 여러 대가 되면 문제가 발생한다
사용자가 늘어나 Spring 서버를 두 대로 확장했다고 가정해 보자.
로드 밸런서는 사용자의 WebSocket 연결을 두 서버로 나누어 전달한다.
사용자 A, B → Spring 서버 1
사용자 C, D → Spring 서버 2
WebSocket은 연결을 계속 유지하는 방식이기 때문에 각 사용자는 특정 Spring 서버에 연결되어 있다.
- 서버 1은 사용자 A와 B의 WebSocket 연결을 관리한다.
- 서버 2는 사용자 C와 D의 WebSocket 연결을 관리한다.
이 상태에서 사용자 A가 메시지를 보내면 메시지는 서버 1에 도착한다.
서버 1은 자신에게 연결된 사용자 B에게는 메시지를 전달할 수 있다. 하지만 사용자 C와 D는 서버 2에 연결되어 있으므로 서버 1이 직접 메시지를 보낼 수 없다.
서버 1이 알고 있는 연결
├─ 사용자 A
└─ 사용자 B
서버 2가 알고 있는 연결
├─ 사용자 C
└─ 사용자 D
결과적으로 다음과 같은 문제가 발생한다.
사용자 A가 메시지 전송
↓
서버 1이 메시지 처리
↓
사용자 B는 메시지 수신
사용자 C와 D는 서버 2에 연결되어 있어 메시지를 받지 못함
각 서버가 자신의 WebSocket 연결 정보만 관리하기 때문이다.
Redis Pub/Sub이 여러 Spring 서버를 연결한다
이 문제를 해결하기 위해 모든 Spring 서버가 동일한 Redis Channel을 구독하도록 만든다.
Spring 서버 1 → SUBSCRIBE chat:room:1
Spring 서버 2 → SUBSCRIBE chat:room:1
사용자 A의 메시지가 서버 1에 들어오면 서버 1은 메시지를 처리한 뒤 Redis Channel에 발행한다.
Spring 서버 1
→ PUBLISH chat:room:1 "안녕하세요"
Redis는 해당 Channel을 구독하고 있는 모든 Spring 서버에 메시지를 전달한다.

전체 과정은 다음과 같다.
1. 사용자 A가 STOMP로 서버 1에 메시지를 전송한다.
2. 서버 1이 메시지를 처리하고 DB에 저장한다.
3. 서버 1이 Redis Channel에 메시지를 발행한다.
4. Redis가 Channel을 구독 중인 모든 Spring 서버에 전달한다.
5. 각 Spring 서버가 자신에게 연결된 사용자에게 STOMP로 전달한다.
Redis는 사용자에게 직접 WebSocket 메시지를 보내지 않는다.
Redis의 역할은 모든 Spring 서버가 같은 채팅 메시지를 알 수 있도록 전달하는 것이다.
Redis Subscriber와 STOMP Subscriber는 서로 다르다
여기서 가장 헷갈리는 부분이 Redis Pub/Sub을 설명할 때 사용자를 Subscriber라고 표현한다.
Redis Channel
├─ Subscriber A
├─ Subscriber B
└─ Subscriber C
Redis에 직접 접속한 클라이언트가 Subscriber가 될 수 있으므로 틀린 설명은 아니다.
하지만 Spring과 STOMP로 채팅 서비스를 구현할 때는 일반적으로 다음처럼 구분된다.
Redis Subscriber
= Spring 서버
STOMP Subscriber
= 채팅방을 구독한 사용자
즉, 두 종류의 구독이 존재한다.
| 구분 | 구독자 | 구독 대상 |
| Redis 구독 | Spring 서버 | chat:room:1 Redis Channel |
| STOMP 구독 | 브라우저 사용자 | /topic/chat/1 STOMP Destination |
이름이 비슷하더라도 Redis Channel과 STOMP Destination은 서로 다른 개념이다.
Redis Channel: chat:room:1
└─ Spring 서버들이 구독
STOMP Destination: /topic/chat/1
└─ 채팅 사용자들이 구독
결국 하나의 채팅 메시지는 두 단계에 걸쳐 전달된다.
- Redis가 모든 Spring 서버에 메시지 전달
- 각 Spring 서버가 자신에게 연결된 사용자에게 STOMP 메시지 전달
정리
- WebSocket : 사용자와 서버가 연결을 유지한다.
- STOMP : CONNECT, SUBSCRIBE, SEND 같은 메시지 규칙을 제공한다.
- Spring Message Broker : STOMP Destination의 구독자를 관리하고 메시지를 전달한다.
- Redis Pub/Sub : Spring 서버가 여러 대일 때 서버 사이에 메시지를 공유한다.
지금까지의 개념을 순서대로 연결하면 다음과 같다.
사용자
→ STOMP SEND
→ @MessageMapping
→ SimpMessagingTemplate
→ STOMP Destination
→ 구독 사용자
그다음 서버를 여러 대로 확장할 때 Redis Pub/Sub을 추가한다.
사용자
→ STOMP
→ Spring 서버 1
→ Redis Pub/Sub
→ Spring 서버 1, 2, 3
→ 각 서버에 연결된 STOMP 구독 사용자
'개발공부 > WEB' 카테고리의 다른 글
| STOMP에서 JWT 인증은 어디서 해야 할까? CONNECT와 Interceptor 이해하기 (0) | 2026.09.10 |
|---|---|
| WebSocket 메시지 흐름을 구조화하는 STOMP (0) | 2026.09.07 |
| WebSocket이란? Spring Boot로 간단한 실시간 통신 구현하기 (0) | 2026.09.04 |
| SSR(Server Side Rendering)과 CSR(Client Side Rendering)이란? (0) | 2026.06.25 |