1. 시작하며 - 왜 Redis를 도입했나?
프로젝트 초기에 WebSocket을 기반으로 한 1:1 채팅 기능을 구현했을 때는 단일 서버 환경을 전제로 하였다. 하지만 서비스의 확장성을 고려하면, 단일 서버만으로는 한계가 명확했다.
특히, 여러 대의 서버가 메시지를 동시에 수신해야 하는 상황에서는 기존 구조만으로는 통신이 불가능했다. 이 문제를 해결하기 위해 Redis Pub/Sub 방식을 도입하였다.
Redis는 메모리 기반의 데이터 저장소로 널리 알려져 있지만, Pub/Sub(발행/구독) 기능을 통해 메시지 브로커로도 활용할 수 있다. 이를 통해 서버 간 메시지 전달을 실시간으로 처리할 수 있다는 장점을 살려 시스템을 개선하고자 했다.
2. Redis Pub/Sub 개념 정리
Redis Pub/Sub는 발행(Publish)과 구독(Subscribe) 모델을 지원한다. 발행자는 특정 채널(channel)에 메시지를 보내고, 구독자는 해당 채널을 통해 메시지를 수신한다.
주요 특징은 다음과 같다.
- 발행자와 구독자는 서로 직접 알 필요가 없다. 채널을 매개로 비동기적으로 메시지를 주고받는다.
- 메시지는 저장되지 않는다. 수신 당시 구독 중이던 클라이언트만 받을 수 있으며, 이후에는 사라진다.
- 서버 간 메시지 브로드캐스팅에 적합하다. 여러 서버가 동일한 채널을 구독하면 하나의 발행 메시지를 동시에 받아 처리할 수 있다.
🗃️ REDIS의 PUB/SUB 기능 (채팅 / 구독 알림)
Redis Publish / Subscribe Publish / Subscribe 란 특정한 주제(topic)에 대하여 해당 topic을 구독한 모두에게 메시지를 발행하는 통신 방법으로 채널을 구독한 수신자(클라이언트) 모두에게 메세지를 전송 하
inpa.tistory.com
3. 개발 과정
3-1. 백엔드 아키텍처 수정
기존 WebSocket 서버는 세션을 메모리에 저장하고 직접 연결을 관리하였다. Redis Pub/Sub를 도입하면서, 구조를 다음과 같이 변경하였다.
- 클라이언트가 /pub/chat/pub 경로로 메시지를 전송하면
- 서버는 해당 메시지를 Redis의 chatroom:{chatRoomId} 채널로 발행(Publish)
- 서버는 Redis의 해당 채널을 구독(Subscribe)하여 메시지를 수신한 뒤
- 해당 메시지를 WebSocket을 통해 구독 중인 클라이언트에게 전송
이 과정을 통해 서버 간 메시지 동기화가 가능해졌다. 서버가 여러 대여도, 같은 채팅방에 접속한 클라이언트들은 문제없이 채팅을 주고받을 수 있게 되었다.
3-2. WebSocket 인증 문제 해결
WebSocket 통신은 HTTP와 달리 최초 연결 이후에는 별도의 인증 토큰을 주고받기 어렵다. 이를 해결하기 위해 JwtHandshakeInterceptor를 추가로 구현하였다.
참고 블로그 글 : https://seungyong.tistory.com/78
웹소켓 연결 JWT 인증 트러블 슈팅
😐정리 - Websocket Handshake요청은 일반적인 HTTP요청을 잡는 HandlerInterceptor에 걸리지 않아 별도의 처리가 필요하다. - STOMP CLIENT에서 담는 헤더들은 웹소켓 연결 이후 STOMP 연결이 진행될 때부터 사용
seungyong.tistory.com
- 클라이언트가 WebSocket 연결 요청 시, 헤더나 파라미터에 JWT 토큰을 함께 전송
- 서버 측 HandshakeInterceptor에서 이 토큰을 검증 후 인증된 세션으로 등록
- 이후 메시지 처리 과정에서도 인증 정보를 활용할 수 있도록 설계
이 과정은 보안 강화를 위해 필수적이었으며, 특히 Redis를 통한 서버 간 메시지 동기화 환경에서도 인증된 사용자 정보 유지가 가능해야 했다.
3-3. 직렬화 문제 해결
메시지를 Redis에 발행할 때, 기본적인 Java 직렬화(Serializable)를 사용할 경우 문제가 발생할 수 있다.
특히, LocalDateTime과 같은 Java 8의 시간 객체를 제대로 직렬화/역직렬화하지 못하는 경우가 있었다.
이를 해결하기 위해 Jackson의 jackson-datatype-jsr310 모듈을 적용하고, RedisTemplate 설정에 GenericJackson2JsonRedisSerializer를 사용하여 JSON 기반 직렬화 포맷을 표준화하였다.
덕분에 LocalDateTime을 포함한 복잡한 객체들도 안정적으로 송수신할 수 있었다.
[Cache] Redis 직렬화 방법에 대해서
안녕하세요 오늘은 Redis 직렬화 방식에 대해 살펴보겠습니다 💪우리는 앞서 Spring Data Redis 의존성과 함께 다양한 방법으로 Spring 서버에서 Redis 를 사용해보았습니다. Redis에 대해 알아봅시다(응
velog.io
3-4. 프론트 연결 이슈 해결
백엔드 구조를 변경한 이후, 프론트엔드에서도 약간의 수정이 필요했다.
- WebSocket 연결 시 토큰을 함께 보내는 방식으로 수정
- Redis Pub/Sub을 통한 서버 구조 변경으로 인해 수신 채널(/sub/chat/room/{roomId})의 구독 로직 변경
- 메시지 전송 시 약간의 포맷 변동(JSON 구조)이 발생했으므로, 클라이언트 측 파싱 로직 수정
초기에는 메세지가 수신되지 않는 이슈가 있었으나, 서버 로그 및 Redis CLI를 통한 publish/subscribe 흐름을 직접 테스트하면서 문제를 추적하여 해결할 수 있었다.
4. 어려웠던 점과 극복 방법
4-1. WebSocket 인증 문제
WebSocket 연결 과정에서는 HTTP 통신과 다르게 표준적인 Authorization 헤더를 지속적으로 사용할 수 없다. 처음에는 프론트엔드에서 토큰을 세션 스토리지에 저장하고 수동으로 추출하는 방식으로 시도했으나, 실시간 연결 특성상 토큰 만료나 인증 불일치 문제가 발생했다.
이를 해결하기 위해 Handshake 단계에서 JWT를 파라미터로 전달받아 서버 측에서 검증하고, 이후 세션에 인증 정보를 저장하는 구조로 변경하였다. 이 과정에서 인증 실패 시 연결 자체를 거부하도록 설정하여 보안성을 강화하였다.
참고 블로그 글: https://hwanheejung.tistory.com/43
[DizzyCode] WebSocket+JWT 시나리오
채팅방에 들어와서 채팅을 한다고 가정해 보자. jwt 토큰을 검사해야 하는 시점은 ①웹소켓으로 업그레이드할 때, ②STOMP 연결할 때, ③메시지를 전송할 때 등등이다. 2, 3은 별로 문제가 되지 않
hwanheejung.tistory.com
4-2. Redis 직렬화 이슈
LocalDateTime 객체를 포함하는 메시지를 Redis를 통해 전송할 때, 기본 Java 직렬화를 사용할 경우 클래스 버전 불일치나 시간 포맷 문제로 예외가 발생했다.
문제를 해결하기 위해 RedisTemplate의 Serializer를 GenericJackson2JsonRedisSerializer로 변경하고, Jackson 모듈에 JavaTimeModule을 추가하여 시간 객체 직렬화 포맷을 명시적으로 설정하였다. 이를 통해 Redis에 저장되는 메시지가 일관된 JSON 포맷을 따르도록 개선하였다.
4-3. 다중 서버 간 메시지 동기화 검증
Pub/Sub 시스템에서는 발행자와 수신자가 직접 연결되지 않기 때문에, 메시지가 정상적으로 브로드캐스팅되는지 검증하기 어렵다. 이를 검증하기 위해 Redis CLI를 통해 직접 subscribe 명령어로 채널을 구독한 뒤, publish 명령어로 테스트 메시지를 발행하여 수신 여부를 수동으로 확인하였다.
이 과정을 통해 서버 간 메시지 전달이 정상적으로 이뤄지고 있다는 것을 검증할 수 있었다.

5. 직접 해보며 배운 것
이번 Redis Pub/Sub 기반 채팅 기능을 도입하면서, 단순히 코드를 짜는 것만으로는 부족하다는 걸 뼈저리게 느꼈다.
처음에는 Redis를 연동하고 WebSocket을 연결하는 것쯤은 쉽게 끝낼 줄 알았다. 하지만 막상 해보니 작은 부분 하나하나가 생각보다 복잡하게 얽혀 있었다.
특히 다음과 같은 부분에서 많은 고민과 시행착오를 겪었다.
- 다중 서버 환경을 고려한 메시지 전달:
서버가 여러 대일 때 메시지를 어떻게 동시에 보내야 하는지, 그 구조를 설계하는 게 생각보다 쉽지 않았다. - WebSocket 인증 처리:
HTTP와 다르게 WebSocket은 인증 흐름이 별개라서, JWT를 어떻게 주고받을지 설계하는 데 시간이 꽤 걸렸다. - Redis 직렬화 문제 해결:
LocalDateTime 같은 시간 데이터가 Redis에 저장되지 않거나 에러를 뿜을 때, 그 원인을 찾고 고치는 데에도 꽤 애를 먹었다. - 실시간 데이터 처리 테스트:
실제로 메시지가 제대로 발행되고 구독되는지 확인하는 과정도 단순하지 않았다.
직접 Redis CLI로 구독하고, publish 명령어를 쳐보며 하나하나 검증해야 했다.
6. 다음 목표
이번에 Redis Pub/Sub 구조를 도입하면서 일단 실시간 채팅이 되는 걸 보는 데까지는 성공했다.
하지만 솔직히 말하면, 해냈다는 기쁨만큼이나 아쉬움도 컸다.
막상 기능을 다 붙여놓고 나니, "여기서 끝낼 수는 없겠다"는 생각이 계속 들었다.
- 메시지 저장 기능 추가
지금은 채팅이 오가는 순간만 존재할 뿐, 기록이 남지 않는다.
대화 내용을 나중에 다시 볼 수 없다면, 채팅의 의미가 반쪽짜리가 된다고 생각했다.
Redis로 실시간 송수신은 계속 유지하되, 별도로 데이터베이스에 안전하게 저장해서
언제든 복구할 수 있게 만드는 걸 다음 목표로 삼았다. - 알림 기능 고도화
단순히 채팅이 오는 걸 넘어서,
읽지 않은 메시지가 몇 개인지 보여주거나, 필요한 경우 푸시 알림을 통해 바로 알 수 있다면
훨씬 더 사람다운 대화 경험이 될 거라고 느꼈다.
이런 부분을 꼭 구현해보고 싶다. - 클러스터 환경 최적화
솔직히 지금은 Redis 하나에 모든 걸 걸고 있는 상황이다.
서버가 죽으면 그대로 끝나는 구조다.
이 부분도 마음 한구석이 불안했기 때문에,
다른 기술을 적용해서라도, 나중에는 장애에도 끄떡없는 시스템을 만들어보고 싶다.(혼자서는 너무 어렵다ㅠㅠ)
7. 마무리하며
처음에는 Redis Pub/Sub을 적용하는 게 그냥 "기능 하나 추가하는 일"쯤으로 생각됐다.
그런데 막상 손을 대고, 하나하나 문제를 맞닥뜨리면서
이게 단순한 코드 짜기가 아니라, 시스템을 설계하는 일이라는 걸 뼈저리게 느꼈다.
메시지를 어떻게 수십 명, 수백 명에게 동시에 보낼지,
서버가 여러 대일 때 메시지가 유실되지 않게 하려면 뭘 해야 하는지,
조금만 방심하면 작은 오류 하나가 전체 시스템을 무너뜨릴 수 있다는 걸 직접 경험했다.
때로는 답답했고, 고치고 또 고치면서 지치기도 했다.
Redis 설정 파일을 들여다보고, WebSocket 인증 문제를 추적하고,
CLI로 직접 subscribe, publish를 해보면서
진짜 개발자가 되어가는 과정을 체험할 수 있었다.
아직 완벽하진 않다.
더 고쳐야 할 것도 많고, 넘어야 할 산도 많다.
하지만 이번 경험은 단순히 '어떤 기능을 구현했다'는 수준을 넘어서
운영 환경을 고민하는 개발자로 한 걸음 나아가게 해줬다.
앞으로도,
"코드만 짜는 개발자"가 아니라,
시스템 전체를 보고, 문제를 미리 생각하고, 사용자 경험까지 고민하는 개발자가 되기 위해
계속해서 부딪히고, 배우고, 성장하고 싶다.