# Redis Pub/Sub 으로 분산 실시간 상태 머신 구현하기

이번 글에서는 사이드 프로젝트 **‘얼음땡(Icebreaker)’**을 개발하면서 겪었던, Redis Pub/Sub 기반의 분산 상태 관리(State Machine) 설계 경험을 정리해보려 한다. 이 프로젝트는 단순한 실시간 게임이었지만, 동시에 **“여러 인스턴스 간 상태 동기화”**라는 흥미로운 문제를 다뤘다.

# 1️⃣ 배경 — 실시간 게임에서의 상태 전파 문제

프로젝트의 요구사항은 명확했다.

  • 사용자들이 하나의 방(Room)에 입장한다.
  • 방장은 게임을 시작·정지할 수 있다.
  • 모든 참가자에게 방 상태 변화가 실시간으로 전파되어야 한다.

이때 우리는 Spring 기반의 STOMP WebSocket을 사용하고 있었다. 처음에는 SimpleBroker 기반의 인메모리 브로커로 구현했지만, 곧 한 가지 한계에 부딪혔다.

“서로 다른 인스턴스에 연결된 사용자들에게는 메시지가 전달되지 않는다.”

즉, 멀티 인스턴스 환경에서 방 상태를 브로드캐스트할 수 없는 문제였다. 우리가 사용한 구성은 다음과 같았다.

  • WAS: EC2 t2.micro 인스턴스 2대
  • Redis: EC2 t2.micro 1대

MVP 단계였기 때문에 Kafka나 RabbitMQ 같은 MQ를 굳이 도입하기보다는, 가볍고 빠른 Redis Pub/Sub으로 분산 브로드캐스트를 구현해보기로 했다.

# 2️⃣ 설계 의도 — 브리지 아키텍처 기반 Pub/Sub

STOMP는 크게 두 가지 형태로 동작한다.

유형 설명 예시
SimpleBroker 애플리케이션 내부 인메모리 브로커 단일 인스턴스용
RelayBroker 외부 MQ(RabbitMQ, ActiveMQ 등)와 연동 다중 인스턴스용

다중 인스턴스 환경에서 SimpleBroker만 사용하면, 다른 인스턴스의 세션 정보에 접근할 수 없기 때문에 메시지 전파가 불가능하다. 따라서 이를 해결하기 위해 Redis Pub/Sub 브리지 아키텍처를 설계했다.

  • WAS 간 메시지 중계 역할을 Redis가 수행
  • 각 인스턴스는 동일한 Redis 채널에 구독
  • 특정 방 상태 변경 이벤트가 발생하면 Redis에 Publish
  • 구독 중인 다른 인스턴스에서도 동일한 메시지를 수신 후 STOMP로 전달

이 구조를 통해 멀티 인스턴스 간 브로드캐스트 문제를 간단히 해결할 수 있었다.

# 3️⃣ 문제 — Redis Pub/Sub의 데이터 유실성

Redis Pub/Sub은 메시지 큐와 달리 내구성(Persistence) 이 없다. 즉, Subscriber가 잠시 끊겨있으면 해당 기간의 메시지는 유실된다.

실시간 게임에서는 모든 참가자가 “현재 방의 상태”를 동일하게 인식해야 하기 때문에, 메시지 유실은 곧 게임 동기화 실패로 이어진다.

이를 해결하기 위해 다음과 같은 보조 구조를 설계했다.

  • Redis String 구조로 각 방의 현재 상태를 별도로 저장

    • 예: room:{roomId} → {"state": "PLAYING", "players": ["user1", "user2"]}
  • 클라이언트가 방에 재입장하거나 재연결할 경우, Redis에서 최신 상태를 조회하여 복원하도록 구성

즉, Pub/Sub은 “변경 이벤트 전파” 용도로만 사용하고, 실제 상태의 Source of Truth는 Redis String으로 관리했다.

# 4️⃣ 상태 전이 로직 — 간단한 커스텀 상태 머신

방의 상태는 단순한 토글 수준이 아니라, 여러 전이 규칙을 가지고 있었다.

예를 들어,

  • WAITINGREADYPLAYINGFINISHED
  • 각 단계마다 유효한 입력만 전이 가능해야 한다.

초기에는 단순한 if-else 분기로 전이를 관리했지만, 상태가 늘어나자 코드 복잡도가 기하급수적으로 증가했다.

이때 Spring StateMachine 프레임워크를 검토했다. 하지만 러닝 커브와 오버헤드가 높았고, MVP 단계에서는 필요한 기능만 직접 구현하는 것이 더 효율적이었다.

그래서 학부 시절 배운 오토마타(Automata) 개념을 떠올려, 아래와 같은 구조를 직접 정의했다.

구성 요소 역할
State 방의 현재 상태 (WAITING, PLAYING 등)
Transition 특정 입력(Event)에 따른 상태 전이
Guard 전이 가능 여부 검사
Action 전이 시 수행되는 로직

결국, Redis에 저장된 room:{id} 상태를 읽고, 전이 가능 여부를 확인한 뒤 새로운 상태로 업데이트하는 경량 커스텀 상태 머신을 완성했다.

# 5️⃣ 상태 전이 → 브로드캐스트

상태가 성공적으로 전이되면, 해당 이벤트를 모든 참가자에게 브로드캐스트해야 했다.

이를 위해 내부적으로 SimpleStompNotifier 콜백을 두었다. 상태 전이가 발생할 때마다 해당 방에 속한 모든 세션에게 STOMP 메시지를 전송하는 방식이다.

즉, 하나의 상태 전이가 다음과 같은 흐름으로 이어졌다.

  1. Redis에 저장된 방 상태 조회
  2. 상태 머신으로 전이 가능 여부 판단
  3. 새로운 상태 저장
  4. Redis Pub/Sub으로 상태 변경 이벤트 발행
  5. 다른 인스턴스에서 수신 후 STOMP를 통해 클라이언트에게 전달

이 과정을 통해, 서버 인스턴스 간의 실시간 상태 동기화클라이언트에게의 실시간 전파를 동시에 달성했다.

# 6️⃣ 트레이드오프와 회고

이 설계는 간결하고 운영 비용이 낮다는 점에서 매우 효율적이었다. 하지만 동시에, Redis Pub/Sub 특유의 제약도 명확했다.

항목 장점 한계
구현 복잡도 낮음, 빠르게 MVP 구축 가능 기능 확장 시 if-else 증가
운영 비용 MQ 불필요, Redis 단일 운영 Redis 장애 시 전체 메시지 유실
확장성 WAS 간 실시간 전파 가능 Subscriber 수 증가 시 부하 증가
복원성 Redis String으로 상태 복원 가능 Pub/Sub 자체는 비내구적

특히 Pub/Sub의 “유실성”은 완전히 해결되지 않는다. 이 문제는 추후 Redis Stream 또는 Kafka를 도입하여 **“메시지 내구성 + 순서 보장”**을 확보하는 방식으로 개선할 예정이다.