실시간 게임 서버 적용 #WebSocket, Room 배치, 상태 공유, 오토스케일링

실시간 멀티플레이 게임 서버는 일반적인 HTTP 기반 API 서버와 다른 운영상의 문제를 가진다. 클라이언트와 서버 사이에 연결을 장시간 유지해야 하며, 플레이어의 세션과 게임 상태를 관리하고, 서버 인스턴스가 추가되거나 종료될 때도 진행 중인 게임을 안정적으로 처리해야 한다.

특히 WebSocket 기반 게임 플랫폼에서는 단순히 서버 인스턴스를 늘리는 것만으로 확장성이 확보되지 않는다. 연결의 소유권, Room 배치, 서버 간 상태 공유, 세션 복구, 종료되는 인스턴스의 연결 이전까지 함께 설계해야 한다.

이 글에서는 실시간 게임 서버를 Kubernetes와 같은 컨테이너 오케스트레이션 환경에서 운영하는 상황을 기준으로 핵심 개념과 설계 방법을 살펴본다.

WebSocket 연결과 세션 유지

WebSocket 연결의 특징

HTTP API는 일반적으로 요청을 보내고 응답을 받은 뒤 연결을 종료하거나 연결을 재사용한다. 반면 WebSocket은 최초 HTTP Upgrade 과정을 거쳐 양방향 통신 채널을 유지한다.

클라이언트와 서버는 연결이 유지되는 동안 별도의 HTTP 요청 없이 메시지를 주고받을 수 있다.

실시간 게임에서는 다음과 같은 기능에 WebSocket을 활용할 수 있다.

  • 플레이어의 이동 및 행동 입력 전달
  • 게임 상태와 전투 결과 전송
  • 매칭 대기열 및 매칭 완료 알림
  • 채팅과 파티 상태 동기화
  • 접속 상태와 서버 이벤트 전달

WebSocket 연결은 특정 서버 인스턴스에 종속된다. 클라이언트가 A 서버에 연결되어 있다면, 해당 TCP 연결과 WebSocket 세션은 기본적으로 A 서버가 관리한다.

로드 밸런서가 다음 메시지를 다른 서버로 전달한다고 해서 기존 연결의 소유권이 자동으로 변경되지는 않는다.

따라서 연결을 유지하는 동안에는 연결을 소유한 서버와 게임 세션의 관계를 명확하게 관리해야 한다.

연결 상태와 게임 세션 분리

WebSocket 연결 상태와 게임 세션 상태는 서로 다른 개념이다.

연결 상태는 클라이언트와 서버 사이의 통신 채널이 살아 있는지를 나타낸다.

세션 상태는 플레이어의 인증 정보, 매칭 상태, 소속 Room, 게임 진행 정보 등을 나타낸다.

예를 들어 플레이어의 네트워크가 일시적으로 끊어져 WebSocket 연결이 종료되더라도 게임 세션까지 즉시 삭제할 필요는 없다. 재접속을 허용한다면 일정 시간 동안 세션을 유지하고, 인증이 완료된 클라이언트가 기존 세션을 복구하도록 설계할 수 있다.

세션 데이터는 성격에 따라 저장 위치를 구분한다.

데이터적절한 관리 위치
실제 WebSocket 객체연결을 소유한 서버 메모리
플레이어의 연결 상태서버 메모리 및 공유 상태 저장소
플레이어와 Room의 관계권위 서버 및 공유 메타데이터
인증 및 재접속 정보서버 측 세션 저장소
확정된 게임 결과와 보상영속 데이터베이스

WebSocket 객체 자체를 Redis에 저장하여 다른 서버에서 그대로 사용하는 방식은 불가능하다. 공유 저장소에는 연결 객체가 아니라 연결 소유 서버의 식별자, 세션 식별자, 재접속에 필요한 메타데이터 등을 저장한다.

연결 유지와 타임아웃

장시간 연결을 유지하는 환경에서는 네트워크 장비나 로드 밸런서가 유휴 연결을 종료할 수 있다.

이를 방지하고 연결 상태를 확인하기 위해 Ping/Pong 또는 애플리케이션 수준의 Heartbeat를 사용할 수 있다.

다만 Heartbeat 응답이 없다고 해서 즉시 플레이어를 영구적으로 탈퇴 처리해서는 안 된다. 네트워크 지연이나 일시적인 패킷 손실일 수 있기 때문이다.

연결 종료 감지, 재접속 허용 시간, 게임 이탈 처리 정책을 별도로 설계해야 한다.

Room과 서버 인스턴스 배치

Room의 역할

Room은 함께 게임을 진행하는 플레이어와 해당 게임의 상태를 묶는 논리적 단위다.

예를 들어 대전 게임에서 Room은 다음 정보를 관리할 수 있다.

  • 참가 플레이어 목록
  • 게임 진행 단계
  • 플레이어별 입력 및 상태
  • 게임 규칙과 제한 시간
  • 게임 결과 및 종료 상태

Room을 관리하는 방식은 게임 서버 아키텍처에 따라 달라진다. 중요한 것은 Room의 논리적 식별자와 실제 게임 상태를 처리하는 서버 인스턴스의 관계를 명확하게 정의하는 것이다.

Room과 서버 인스턴스의 관계

가장 일반적인 구조는 하나의 Room을 특정 게임 서버 인스턴스가 전담하도록 배치하는 방식이다.

Game Server Cluster
├── Game Server A
│   ├── Room A
│   └── Room B
├── Game Server B
│   ├── Room C
│   └── Room D
└── Game Server C
    ├── Room E
    └── Room F

이 구조에서는 각 Room의 게임 상태를 담당하는 권위 서버가 명확하다. 여러 서버가 같은 게임 상태를 동시에 수정하지 않으므로 동기화와 정합성 관리가 상대적으로 단순하다.

다만 Room의 상태를 관리하는 서버가 종료되면 해당 Room의 진행 상태를 복구하거나 게임을 종료하는 정책이 필요하다.

Room 배치 전략

Room을 어느 서버에 배치할지는 게임의 특성과 서버의 자원 사용량에 따라 결정한다.

라운드 로빈 방식

새 Room을 서버 인스턴스에 순서대로 배치한다. 구현이 단순하지만 Room마다 필요한 CPU와 메모리가 다르면 부하가 불균형해질 수 있다.

최소 부하 방식

CPU 사용량, 메모리, 활성 Room 수, 연결 수 등을 기준으로 상대적으로 여유 있는 서버에 배치한다. 단순한 Room 개수보다 실제 게임 부하를 반영할 수 있지만, 부하 지표를 수집하고 배치 결정을 내리는 시스템이 필요하다.

게임 유형 기반 방식

게임 모드나 예상 자원 사용량에 따라 서버 그룹을 구분한다. 가벼운 캐주얼 게임과 연산량이 많은 시뮬레이션 게임을 분리하면 서로 다른 부하 특성에 맞춰 자원을 관리할 수 있다.

Kubernetes 환경에서의 배치

Kubernetes의 Pod는 게임 서버 프로세스를 실행하는 단위다. Deployment는 Pod의 배포와 복제본 관리를 담당하며, Service는 일반적으로 Pod 집합에 대한 네트워크 접근 경로를 제공한다.

그러나 Kubernetes Service는 게임 Room을 이해하지 못한다. 단순한 Service 뒤에 게임 서버를 배치한다고 해서 특정 Room의 플레이어가 항상 같은 서버로 연결되는 것은 아니다.

따라서 게임 서버 플랫폼에서는 다음과 같은 계층을 둘 수 있다.

  • 매칭 서버: 게임을 시작할 플레이어를 결정한다.
  • 배치 관리자: Room을 담당할 게임 서버를 선택한다.
  • 게임 서버: 배정된 Room의 상태와 게임 로직을 처리한다.
  • 공유 상태 저장소: 서버 간 세션 메타데이터와 이벤트를 전달한다.

배치 관리자는 Room ID와 서버 ID의 관계를 기록하고, 클라이언트가 접속해야 할 게임 서버의 연결 정보를 제공한다.

실제 구현에서는 Agones 같은 게임 서버 오케스트레이션 시스템을 활용할 수도 있다. 게임 서버를 일반적인 Stateless API 서버와 구분하여 관리할 수 있기 때문이다.

서버 간 상태 공유

로컬 메모리의 한계

게임 서버가 단일 인스턴스로 운영될 때는 플레이어 세션이나 Room 상태를 메모리에 저장하는 것만으로도 충분할 수 있다.

하지만 서버 인스턴스가 여러 개로 늘어나면 각 서버는 서로 다른 메모리 공간을 갖는다.

Game Server A        Game Server B
┌──────────────┐     ┌──────────────┐
│ Player A     │     │ Player C     │
│ Room A       │     │ Room B       │
└──────────────┘     └──────────────┘
        │                    │
        └─────────┬──────────┘
                  │
          Shared State Store
               (Redis)

서버 A의 메모리에 저장된 정보를 서버 B가 직접 읽을 수는 없다. 서버 간에 필요한 정보는 네트워크를 통해 전달하거나 공유 저장소에 기록해야 한다.

Redis를 이용한 공유 상태 관리

Redis는 빠른 읽기와 쓰기가 필요한 세션 메타데이터, 접속 상태, 매칭 대기열, 임시 상태 등을 관리하는 데 활용할 수 있다.

예를 들어 다음과 같은 정보를 저장할 수 있다.

player:{playerId}:session
    serverId
    roomId
    connectionStatus

room:{roomId}:metadata
    ownerServerId
    gameMode
    roomStatus

위 구조는 개념적인 예시다. 실제 Redis 키와 데이터 구조는 조회 패턴, 만료 정책, 원자적 갱신 요구사항에 맞춰 설계해야 한다.

Redis에 Room의 현재 담당 서버를 기록하면 매칭 서버나 게이트웨이가 해당 Room을 처리할 서버를 찾을 수 있다.

하지만 Redis에 서버 ID를 기록하는 것만으로 게임 상태가 공유되는 것은 아니다. 게임 진행 상태를 실제로 공유해야 하는지, 아니면 해당 서버로 메시지를 전달하면 충분한지를 먼저 구분해야 한다.

상태 공유와 이벤트 전달의 구분

상태 공유는 현재 값을 조회하거나 갱신하는 데 초점을 둔다. 예를 들어 플레이어의 현재 소속 Room이나 세션 만료 시각이 이에 해당한다.

이벤트 전달은 특정 사건을 다른 서버에 알리는 데 초점을 둔다. 예를 들어 파티원이 접속을 종료했거나 매칭이 완료되었다는 사실을 전달하는 경우다.

이벤트 전달에는 Redis Pub/Sub, Redis Streams 또는 별도의 메시지 브로커를 활용할 수 있다.

Redis Pub/Sub는 구독자가 연결되어 있을 때 이벤트를 전달하는 데 적합하지만, 기본적으로 메시지의 영속적인 보관이나 오프라인 구독자에 대한 재전달을 보장하지 않는다.

이벤트 유실이 허용되지 않는 작업이라면 Redis Streams나 영속 메시지 브로커를 검토해야 한다. 다만 메시지가 보존되더라도 중복 소비와 재처리 가능성을 고려해야 한다.

게임 상태의 권위와 정합성

서버 간 상태 공유에서 가장 중요한 원칙은 동일한 게임 상태를 여러 서버가 무분별하게 수정하지 않도록 하는 것이다.

예를 들어 하나의 Room을 서버 A가 담당한다면, 서버 B가 해당 Room의 전투 결과를 임의로 변경하지 못하도록 해야 한다.

일반적으로 게임 서버가 게임 진행 상태의 권위자가 되고, 공유 저장소는 검색과 라우팅, 이벤트 전달 등을 담당하도록 분리한다.

포인트 차감, 아이템 구매, 보상 지급처럼 경제적 정합성이 중요한 작업은 별도의 트랜잭션과 중복 처리 방지 정책이 필요하다. 단순히 Redis에 현재 잔액을 기록하거나 메시지를 발행하는 것만으로는 정합성이 보장되지 않는다.

스케일 아웃과 스케일 인

스케일 아웃

스케일 아웃은 서버 인스턴스를 추가하여 처리 용량을 늘리는 방식이다.

Before
Game Server A
├── Room A
├── Room B
└── Room C

After
Game Server A       Game Server B
├── Room A          ├── Room D
├── Room B          └── Room E
└── Room C

새 서버가 추가되면 신규 Room을 해당 서버에 배치할 수 있다. 그러나 기존 Room이 자동으로 새 서버로 이동하는 것은 아니다.

이미 생성된 Room에는 게임 상태와 플레이어 연결이 존재하기 때문이다.

따라서 스케일 아웃 시에는 신규 Room 배치 정책과 기존 Room의 유지 정책을 분리해야 한다.

게임 서버는 CPU 사용률만으로 부하를 판단하기 어려울 수 있다. 다음 지표를 함께 고려하는 편이 효과적이다.

  • 활성 WebSocket 연결 수
  • 활성 Room 수
  • Room별 플레이어 수
  • 게임 틱 처리 시간
  • 메시지 처리 지연
  • CPU 및 메모리 사용량
  • 네트워크 송수신량

Kubernetes HPA는 CPU, 메모리 또는 설정된 사용자 정의 지표를 기반으로 Pod 복제본 수를 조정할 수 있다. 다만 HPA가 Pod를 추가하더라도 게임 Room 배치 시스템이 신규 서버를 실제 배치 대상으로 인식하도록 준비되어 있어야 한다.

스케일 인

스케일 인은 서버 인스턴스를 줄여 자원 사용량과 운영 비용을 낮추는 방식이다.

Stateless API 서버라면 요청을 다른 인스턴스로 보내는 방식으로 축소를 비교적 쉽게 처리할 수 있다. 하지만 상태를 가진 게임 서버는 활성 Room과 WebSocket 연결 때문에 즉시 종료하기 어렵다.

예를 들어 특정 서버에 플레이어가 접속해 있고 게임이 진행 중이라면, 해당 서버를 종료하는 순간 연결이 끊기고 게임 상태가 손실될 수 있다.

따라서 게임 서버의 스케일 인은 단순한 Pod 삭제가 아니라 신규 작업 배정 중단 → 기존 작업 정리 → 안전한 종료의 과정으로 설계해야 한다.

게임 서버에 적합한 축소 기준

활성 Room이 많은 서버보다 활성 Room이 적고 종료가 임박한 서버를 우선적으로 축소하는 것이 유리할 수 있다.

예를 들어 다음 조건을 고려할 수 있다.

  • 신규 Room 배정이 중단되었는가?
  • 현재 진행 중인 Room이 모두 종료되었는가?
  • 남아 있는 연결과 세션이 정리되었는가?
  • 진행 중인 저장 및 보상 지급 작업이 완료되었는가?
  • 종료해도 복구할 수 없는 데이터가 남아 있지 않은가?

Kubernetes의 축소 정책만으로 이러한 조건이 모두 보장되지는 않는다. 게임 서버 자체의 상태를 수집하고 종료 가능 여부를 판단하는 드레이닝 정책이 필요하다.

종료되는 인스턴스의 연결 처리

Graceful Shutdown

Graceful Shutdown은 서버를 종료할 때 진행 중인 작업을 가능한 한 안전하게 마무리하는 절차다.

게임 서버가 종료될 때는 프로세스를 즉시 죽이는 대신 다음 과정을 수행하도록 설계할 수 있다.

  • 서버를 신규 Room 배정 대상에서 제외한다.
  • 신규 게임 세션의 생성을 중단한다.
  • 기존 Room의 상태를 확인한다.
  • 진행 중인 게임을 완료하도록 기다리거나 명시적인 종료 정책을 적용한다.
  • 필요한 상태와 결과를 저장한다.
  • 클라이언트에 종료 또는 재접속 안내를 전송한다.
  • WebSocket 연결을 정리하고 프로세스를 종료한다.

여기서 중요한 것은 종료 신호를 받은 시점과 실제 프로세스가 종료되는 시점 사이에 충분한 정리 시간이 있어야 한다는 점이다.

Kubernetes에서는 Pod 종료 시 preStop 훅과 terminationGracePeriodSeconds 등을 활용할 수 있다. 다만 preStop 훅이 실행되는 시간도 전체 종료 유예 시간에 포함되므로, 실제 종료 절차에 맞춰 시간을 설정해야 한다.

신규 연결 차단

서버 종료가 예정되어 있다면 해당 서버에 신규 Room을 배정하지 않아야 한다.

이를 위해 서버의 상태를 다음과 같이 구분할 수 있다.

상태의미
Starting초기화 중이며 준비되지 않음
Ready신규 Room 배정 가능
Draining신규 Room 배정을 중단하고 기존 작업 정리
Terminating연결 및 리소스를 정리하는 중
Stopped종료 완료

배치 관리자는 Ready 상태의 서버만 신규 Room 배정 대상으로 선택한다.

Kubernetes의 Readiness Probe는 Pod가 일반적인 트래픽을 받을 준비가 되었는지 판단하는 데 활용할 수 있다. 하지만 Readiness가 실패했다고 해서 기존 WebSocket 연결이 자동으로 다른 서버로 이전되지는 않는다.

이미 수립된 연결과 신규 연결의 처리 정책을 분리해야 한다.

기존 Room 처리 전략

종료되는 서버에 활성 Room이 남아 있다면 게임의 특성에 따라 전략을 선택해야 한다.

기존 게임 완료 후 종료

진행 중인 Room을 유지하고 모든 게임이 종료된 뒤 서버를 종료한다. 상태 동기화가 복잡한 실시간 대전 게임에서 유용하다.

단점은 일부 장시간 게임 때문에 서버 종료가 지연될 수 있다는 것이다. 따라서 최대 대기 시간과 초과 시 처리 정책이 필요하다.

Room 이전

기존 Room의 상태를 다른 서버로 이전한다. 이를 위해서는 게임 상태 직렬화, 이전 시점의 입력 처리, 메시지 순서 보장, 플레이어 연결 재수립 등을 설계해야 한다.

게임 서버의 메모리 상태를 단순히 복사하는 것만으로는 안전한 이전을 보장할 수 없다. 이전 중에 발생한 입력과 상태 변경을 어느 서버가 처리할지 명확해야 한다.

게임 종료 및 재매칭

기존 게임을 종료하고 플레이어를 로비로 돌려보내거나 재매칭을 진행한다. Room 이전이 어렵거나 게임을 중단하는 비용이 상대적으로 낮은 경우 고려할 수 있다.

체크포인트 기반 복구

주요 게임 상태를 주기적으로 저장하고 서버 장애 발생 시 다른 서버에서 복구한다. 단, 마지막 체크포인트 이후의 입력과 이벤트를 어떻게 처리할지에 따라 복구 시점의 상태가 달라질 수 있다.

게임의 공정성과 복구 비용을 고려하여 체크포인트 주기와 복구 정책을 결정해야 한다.

종료와 장애의 차이

예정된 종료와 예기치 않은 장애는 동일하게 처리할 수 없다.

예정된 종료에서는 서버가 종료 신호를 받아 신규 작업을 차단하고 기존 Room을 정리할 시간이 있다.

반면 프로세스 강제 종료, 노드 장애, 네트워크 단절에서는 해당 서버가 정리 절차를 수행하지 못할 수 있다.

따라서 별도의 장애 감지와 복구 정책이 필요하다.

  • 서버 상태와 마지막 Heartbeat를 확인한다.
  • 일정 시간 동안 응답이 없으면 장애 가능성을 판단한다.
  • 기존 서버가 실제로 작업을 중단했는지 확인한다.
  • Room의 소유권과 재배치 가능 여부를 결정한다.
  • 복구 가능한 상태를 불러오거나 게임을 종료한다.
  • 중복 보상 지급이나 중복 결과 처리를 방지한다.

특히 장애가 발생한 서버가 완전히 종료되었는지 확신할 수 없는 상황에서는 두 서버가 동시에 같은 Room을 처리하는 문제를 방지해야 한다. 이를 위해 소유권 임대, 세대 번호(epoch), fencing token 등으로 오래된 서버의 쓰기 작업을 차단하는 설계를 검토할 수 있다.

단순한 Redis 락만으로는 모든 장애 상황에서 안전한 소유권 이전을 보장할 수 없다는 점도 중요하다.

실전 아키텍처 예시

실시간 멀티플레이 게임 플랫폼은 다음과 같은 구조로 구성할 수 있다.

                   Game Client
                        │
                        ▼
                 Load Balancer
                        │
                        ▼
                  API / Gateway
                        │
              ┌─────────┴─────────┐
              ▼                   ▼
         Matchmaking         Session Router
              │                   │
              └─────────┬─────────┘
                        ▼
                 Room Scheduler
                        │
          ┌─────────────┼─────────────┐
          ▼             ▼             ▼
      Game Pod A    Game Pod B    Game Pod C
       Room A        Room B        Room C
          │             │             │
          └─────────────┼─────────────┘
                        ▼
                 Shared Services
             ┌──────────┼──────────┐
             ▼          ▼          ▼
           Redis     Message     Database
                     Broker

각 구성 요소는 책임을 명확하게 분리한다.

  • Load Balancer: 외부 연결을 적절한 진입점으로 전달한다.
  • API / Gateway: 인증, 접속 제어, 연결 관리 등을 담당한다.
  • Matchmaking: 함께 플레이할 사용자를 결정한다.
  • Session Router: Room을 담당하는 서버를 찾고 연결 경로를 제공한다.
  • Room Scheduler: 게임 서버의 상태를 기반으로 Room을 배치한다.
  • Game Pod: 실제 게임 로직과 Room 상태를 처리한다.
  • Redis: 세션 메타데이터와 빠른 조회가 필요한 임시 상태를 관리한다.
  • Message Broker: 서버 간 이벤트를 전달한다.
  • Database: 게임 결과, 계정 정보, 포인트 원장 등 영속 데이터의 정합성을 관리한다.

이 구조는 하나의 정답이 아니다. 게임 규모와 연결 방식에 따라 Gateway를 생략하거나, 매칭 서버와 배치 관리자를 통합하거나, 게임 서버 오케스트레이션 전용 시스템을 도입할 수 있다.

핵심은 연결 라우팅, 게임 상태의 권위, 서버 간 통신, 영속 데이터 관리를 하나의 책임으로 뭉치지 않는 것이다.

설계 시 반드시 검증할 항목

실제 운영 환경에서는 정상적인 연결과 게임 진행뿐 아니라 서버가 추가되거나 종료되는 상황을 검증해야 한다.

  • 서버 인스턴스가 추가되었을 때 신규 Room이 정상적으로 배치되는가?
  • 기존 WebSocket 연결이 서버 확장 과정에서 불필요하게 끊기지 않는가?
  • 서버가 Draining 상태가 된 이후 신규 Room이 배정되지 않는가?
  • 종료 유예 시간 내에 Room 정리와 데이터 저장이 완료되는가?
  • 서버가 강제 종료되었을 때 세션과 Room의 상태를 판별할 수 있는가?
  • 장애 복구 중 두 서버가 동일한 Room을 동시에 처리하지 않는가?
  • 재접속 과정에서 플레이어의 인증과 Room 소속이 올바르게 복구되는가?
  • 게임 결과와 보상 지급이 재시도 과정에서 중복 처리되지 않는가?
  • 활성 Room이 많은 상황에서도 스케일 인이 안전하게 진행되는가?

특히 부하 테스트에서는 동시 접속자 수만 측정하지 말고 Room 생성과 종료 속도, 메시지 처리 지연, 재접속 성공률, 종료 과정에서의 연결 손실률도 함께 측정해야 한다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다