멀티 인스턴스 환경에서 채팅 메시지 전파하기: Redis Pub/Sub
- Published on
Junyoung Yang
카카오 테크 캠퍼스 최종 프로젝트에서 UniSchedule - 대학생을 위한 일정 관리 서비스를 만들면서 팀 일정 관리뿐 아니라 팀원들이 소통할 수 있는 채팅 기능도 함께 구현하게 되었다.
처음의 단일 인스턴스 환경(로컬이나 테스트 서버)에서는 채팅 메시지 전송이 자연스럽게 동작했다. 하지만 운영서버인 AWS ECS/Fargate 환경에서는 인스턴스가 여러 개였고, 이때 문제가 발생했다. 사용자마다 서로 다른 서버 인스턴스에 연결될 수 있었고, 이 경우 한 인스턴스에서 발생한 채팅 메시지가 다른 인스턴스에 연결된 사용자에게 전달되지 않았다.
이 문제를 해결하기 위해 Redis Pub/Sub를 적용했다.
기존 구현
팀 채팅 기능을 구현하기 위해 Spring Boot에서 단순 WebSocket을 사용했다.
단일 인스턴스 환경에서의 테스트
단일 인스턴스 환경(로컬 환경이나 테스트 서버)에서는 문제가 발생하지 않았다.
채팅 기능의 기본 흐름은 다음과 같다.
- 사용자가 서버에 연결한다. (WebSocket)
- 사용자가 메시지를 전송한다.
- 서버가 메시지를 수신한다.
- 서버가 같은 채팅방에 있는 사용자들에게 메시지를 전달한다.
단일 인스턴스에서는 모든 WebSocket 연결이 하나의 서버에 붙어 있기 때문에 이 흐름이 문제 없이 동작했다.
하지만 이 구조는 여러 인스턴스(AWS ECS)로 구성된 운영 서버에서는 그대로 사용할 수 없었다.
멀티 인스턴스 환경의 문제
멀티 인스턴스 환경에서는 다음 상황이 발생할 수 있다.
- 사용자 A는 인스턴스 1에 연결된다.
- 사용자 B는 인스턴스 2에 연결된다.
- A가 메시지를 전송한다.
- 인스턴스 1이 메시지를 처리한다.
- 인스턴스 2는 해당 메시지가 발생한 사실을 모른다.
이 경우 인스턴스 1에 연결된 사용자에게만 메시지가 전달되고, 인스턴스 2에 연결된 사용자에게는 메시지가 보이지 않는다. 따라서 같은 채팅방을 사용하고 있어도 일부 사용자끼리만 메시지를 주고받는 것처럼 보이는 문제가 생긴다.
해결 방법
이 문제를 해결하기 위해 Redis Pub/Sub을 도입했다. DB에 채팅 데이터를 저장해 인스턴스 간 공유하는 방식도 생각했지만, 메시지 전파를 위해 매번 DB를 조회하는 구조는 부담이 커 보였다. 그래서 Redis Pub/Sub을 통해 메시지 발생 사실을 인스턴스 간에 전달하는 방식이 더 단순하고 적절해 보였다. 이 내용은 ADR로 정리해 팀에도 공유했다.
Redis Pub/Sub이란?
Redis Pub/Sub은 특정 채널을 구독한 클라이언트들에게 메시지를 발행하는 방식이다. 발행자는 채널에 메시지를 보내고, 그 채널을 구독 중인 수신자들이 메시지를 받는다.

적용 내용
Redis Pub/Sub이 적용된 흐름은 다음과 같다.
- 사용자가 메시지를 전송한다.
- 메시지를 받은 인스턴스가 Redis 채널에 메시지를 발행한다. (모든 인스턴스는 해당 채널을 이미 구독하고 있다)
- 각 인스턴스는 채널에서 메시지를 수신한 뒤, 자신에게 연결된 사용자들에게 전달한다.
sequenceDiagram
participant ClientA as Client A (Server 1)
participant Server1 as Backend (Instance 1)
participant Redis as Redis (Pub/Sub)
participant Server2 as Backend (Instance 2)
participant ClientB as Client B (Server 2)
ClientA ->> Server1: 1. WebSocket 연결 (팀 ID: 123)
Server1 -->> ClientA: 연결 승인
ClientB ->> Server2: 2. WebSocket 연결 (팀 ID: 123)
Server2 -->> ClientB: 연결 승인
Note over Server1,Server2: `team:chat:123` 채널 구독 (ChatMessageListener)
ClientA ->> Server1: 3. 메시지 전송 (Send)
Server1 ->> Server1: 4. (ChatService) DB에 메시지 저장
Server1 ->> Redis: 5. (MessageBrokerService) <br /> PUBLISH `team:chat:123`
Redis -->> Server1: 6. (Listener) 메시지 수신
Redis -->> Server2: 6. (Listener) 메시지 수신
Server1 ->> ClientA: 7. (Handler) 메시지 브로드캐스트
Server2 ->> ClientB: 7. (Handler) 메시지 브로드캐스트
이 구조를 통해 한 인스턴스에서 발생한 메시지도 다른 인스턴스로 전달할 수 있었다.
주의한 부분
Redis Pub/Sub은 이벤트 전파에는 단순하고 빠르지만, 메시지를 영구적으로 보관하는 큐는 아니다. 구독 중이지 않은 인스턴스나 끊어진 연결에 대해 나중에 다시 전달해주는 구조로 기대하면 안 된다.
그래서 UniSchedule에서는 Pub/Sub을 채팅 메시지의 원본 저장소로 보지 않았다. 메시지 자체는 DB에 저장하고, Pub/Sub은 각 인스턴스가 새 메시지 발생 사실을 실시간으로 알기 위한 통로로만 사용했다. 사용자가 재접속하거나 과거 메시지를 다시 조회해야 하는 경우에는 DB를 기준으로 복구할 수 있어야 했다.
정리

UniSchedule에서 채팅 메시지 전파 문제를 겪은 뒤, 서버를 늘리는 것이 단순히 처리 성능만 늘리는 일이 아니라는 점을 알게 됐다. 서버가 여러 대가 되면 이벤트를 어떻게 전달할지, 상태를 어디에 둘지도 함께 생각해야 했다.
단일 인스턴스에서는 자연스럽게 동작하던 흐름이 멀티 인스턴스에서는 어긋날 수 있다는 것을 직접 경험했고, 이후에는 초기 설계 단계에서도 멀티 인스턴스 환경을 조금 더 의식하게 되었다.