Kim Seogyu
Backend/WebSocket

확장 가능한 실시간 WebSocket 서버 아키텍처 설계

수평 확장 WebSocket 서버에서 세션 라우팅과 메시지 복구를 설계하는 실무 기준을 정리합니다.

Published 2025년 12월 27일2 min read262 words

확장 가능한 실시간 WebSocket 서버 아키텍처 설계

문제 정의

WebSocket 서버를 여러 대로 늘리면 "어떤 유저가 어느 서버에 붙어 있는지"가 분산됩니다. 이 상태를 중앙에서 일관되게 관리하지 않으면 메시지 유실과 오배송이 발생합니다.

권장 구성

  • Load Balancer: 연결 분산.
  • WS 서버 N대: 실제 연결 유지.
  • Redis Hash: userId -> serverId 세션 위치 저장.
  • Redis Pub/Sub: 서버 간 실시간 이벤트 전달.
  • Redis Streams: 재연결 시 누락 메시지 복구.

데이터 모델

HSET ws:sessions {userId} {serverId}
PUBLISH ws:server:{serverId} {event-json}
XADD ws:events * userId {userId} payload {json}

핵심 흐름

  1. 연결 성공 시 ws:sessions에 세션 등록.
  2. A 유저가 B 유저에게 메시지 전송.
  3. B의 serverId를 Hash에서 조회.
  4. 해당 서버 채널로 Pub/Sub 발행.
  5. 수신 서버가 로컬 소켓으로 전달.
  6. 이벤트를 Streams에 기록해 재전송 가능 상태 유지.

서버 종료/장애 대응

  • Heartbeat + TTL: 좀비 세션 자동 정리.
  • Graceful Shutdown: 종료 전 해당 서버 세션 일괄 제거.
  • Reconnect Resume: 클라이언트가 마지막 이벤트 ID를 보내면 Streams에서 누락분 재전송.

예시 코드(개념)

async function onConnect(userId: string, serverId: string) {
  await redis.hset('ws:sessions', userId, serverId);
  await redis.expire(`ws:alive:${userId}`, 30);
}

async function routeMessage(toUserId: string, payload: object) {
  const targetServer = await redis.hget('ws:sessions', toUserId);
  if (!targetServer) return;

  await redis.publish(`ws:server:${targetServer}`, JSON.stringify({ toUserId, payload }));
  await redis.xadd('ws:events', '*', 'userId', toUserId, 'payload', JSON.stringify(payload));
}

관측 지표

  • 연결 수/활성 세션 수
  • Pub/Sub 전달 지연(p95, p99)
  • reconnect 후 누락 복구 성공률
  • 서버별 fan-out 처리량

요약

확장형 WebSocket의 본질은 소켓 핸들링이 아니라 상태 일관성입니다. 세션 위치, 라우팅, 복구 경로를 분리해 설계해야 장애 시 복구가 가능합니다.

Share

Related Articles

Comments

이 블로그는 제가 알고 있는 것들을 잊지 않기 위해 기록하는 공간입니다.
직접 작성한 글도 있고, AI의 도움을 받아 정리한 글도 있습니다.
정확하지 않은 내용이 있을 수 있으니 참고용으로 봐주세요.

© 2026 Seogyu Kim