게임 서버 : Stateful vs Stateless
게임 서버 개발은 크게 Stateful과 Stateless 두 가지 방식으로 나뉜다. 이 두 기술은 각각 실시간 온라인 게임과 비동기 온라인 게임에 적합하며, 서로 다른 특징과 장단점을 가지고 있다. 두 기술의 차이점과 적용 사례를 살펴보자.
1. 실시간 온라인 게임 (Stateful 서버)
[ 특징 ]
실시간 상호작용: 다른 유저와의 실시간 상호작용이 핵심으로 대표적인 장르로는 MMORPG, MOBA, FPS 등이 있다.
연결 유지: 클라이언트와 서버는 지속적으로 연결을 유지하며, 모든 게임 로직은 서버에서 처리된다.
서버 메모리 사용: 모든 유저의 데이터와 게임 상태는 서버 메모리에 저장되어 실시간으로 처리된다.
빠른 처리 속도: C/C++과 같은 고성능 언어를 주로 사용하여 빠른 처리가 가능하다.
확장의 한계: 하나의 게임 플레이 단위에서는 Scale-Out(수평 확장)이 어렵기 때문에, 동시 접속자 수에 제한이 있다.
왜 동시 접속자 수에 제한이 있을까? 개인적으로 생각하는 Stateful 서버에서 Scale-Out이 힘든 이유는 2개이다.
첫째, TCP 연결은 상태를 유지하는 연결 지향적 프로토콜이기 때문에, L4 로드 밸런서가 초기 연결을 특정 서버로 라우팅하면 해당 연결은 종료될 때까지 동일한 서버로 유지된다. 이로 인해 실시간으로 부하를 분산하기 어렵다.
L4 로드 밸런싱은 TCP/UDP 레벨에서 동작하며, 초기 연결을 특정 서버로 라우팅한 후에는 해당 연결이 유지되기 때문에 동적 부하 분산이 안된다.
(MMORPG에서 L4 로드 밸런싱이 된다고 하는 사람은 해당 게임이 TCP로 작동하지 않거나 실시간 게임이 아닐 것이다. 만약 된다면 포트폴리오를 만들어서 제출해보자. 연봉 1억도 그냥 받을 것이다.)
둘째, 섹터 기반 MMORPG에서는 실시간 처리를 위해 하나의 CPU에서 모든 연산이 이루어져야 한다. 다른 하드웨어와의 통신이 가능하지만, 이는 다른 서버와 연산을 분산하는 것과 유사하며, 하나의 CPU 내에서 모든 연산을 처리하는 것보다 속도가 느리다.
MMORPG와 같은 실시간 게임에서는 지연 시간(latency)이 매우 중요하며, 여러 CPU나 서버 간의 통신은 추가적인 오버헤드를 발생시켜 실시간 성능을 저하시킬 수 있다.
두번째의 경우 NUMA(Non-Uniform Memory Access) 기반 하드웨어를 사용하면 일종의 Scale-Out이 이론적으로 가능하다. 그러나 NUMA는 구성 비용이 높고, 성능을 최적화하기 위해 각 CPU의 메모리 접근 패턴을 신중하게 설계해야 한다. 이는 유지보수와 프로그래밍 복잡성을 증가시킨다. 과거에는 하드웨어의 한계로 인해 NUMA를 사용하여 동시 접속자 수를 늘리는 방법이 사용되었지만, 현대의 하드웨어는 충분한 성능을 제공하므로 초기 서버 구축 시 NUMA를 사용할 필요가 거의 없다.
NUMA는 멀티코어 시스템에서 메모리 접근 시간을 최적화하기 위한 아키텍처이지만, 이를 효과적으로 활용하려면 애플리케이션과 메모리 할당 전략을 세밀하게 조정해야 한다. 이는 개발 및 유지보수 비용을 증가시키는 요인이다.
이 밖에 보편적인 이유에 대해선 아래쪽에 추가로 작성한다.
개인적인 이유가 아닌 보편적인 이유를 좀 더 설명하자면 다음과 같다.
1. 상태(State)의 유지와 동기화 문제
- Stateful 서버는 클라이언트와의 연결 상태, 세션 데이터, 게임 내 객체 상태 등을 메모리나 저장소에 유지한다. Scale-Out을 위해 여러 서버로 트래픽을 분산하면, 이러한 상태 정보를 모든 서버 간에 동기화해야 한다.
- 상태 동기화는 네트워크 오버헤드를 발생시키고, 실시간으로 데이터를 일관되게 유지하기 위해 복잡한 알고리즘이 필요하다. 특히, 분산 시스템에서의 데이터 일관성(Consistency)을 보장하는 것은 매우 어렵다.
ex) MMORPG에서 플레이어의 위치나 아이템 상태를 모든 서버에서 동기화하려면, 지연 시간(latency)과 데이터 충돌 문제가 발생할 수 있다.
2. 데이터 일관성(Consistency)과 동시성(Concurrency) 문제
- 여러 서버가 동일한 데이터를 공유할 때, 데이터의 일관성을 유지하는 것이 어렵다. 특히, 동시에 여러 서버가 같은 데이터를 수정하려고 하면 충돌이 발생할 수 있다.
- 분산 락(Distributed Lock)이나 분산 트랜잭션(Distributed Transaction)과 같은 기술을 사용해야 하지만, 이는 성능 저하를 초래하고 구현이 복잡하다.
- 일단 lock을 사용하거나 DB를 통하는 것 자체가 느리다. 오죽하면 성능을 위해 Redis등 메모리 기반 DB를 사용하기도 하는데 여러 서버가 같은 데이터를 사용하고 있으면 답이 없다.
ex) 은행 시스템에서 여러 서버가 동일한 계좌의 잔액을 동시에 수정하려고 할 때, 데이터 일관성을 유지하기 위해 복잡한 동기화 메커니즘이 필요하다.
3. 세션 관리의 복잡성
- Stateful 서버는 클라이언트의 세션 정보를 서버 메모리에 저장한다. Scale-Out을 위해 여러 서버를 사용하면, 클라이언트가 다른 서버로 연결될 때 세션 정보를 이전 서버에서 가져와야 한다.
- 세션 정보를 공유하려면 별도의 세션 저장소(예: Redis, Memcached)를 사용해야 하며, 이는 추가적인 네트워크 지연과 복잡성을 초래한다.
- 2번 문제와 연결되어 있기에 여러 서버가 동일한 데이터에 대한 접근을 안하도록 설계하는 것이 제일 좋다.
ex) 웹 애플리케이션에서 사용자가 로그인한 상태를 유지하려면, 모든 서버가 동일한 세션 데이터를 공유해야 한다.
4. 트랜잭션의 원자성(Atomicity) 보장 문제
- Stateful 서버는 트랜잭션(Transaction)을 사용하여 데이터의 원자성을 보장한다. Scale-Out을 위해 여러 서버로 트래픽을 분산하면, 트랜잭션의 범위가 여러 서버로 확장되면서 원자성을 보장하기 어려워진다.
- 분산 트랜잭션은 구현이 복잡하고, 성능 저하를 초래한다. 또한, 트랜잭션의 롤백(Rollback)이나 커밋(Commit)을 모든 서버에서 동기화해야 한다.
- 따로 분류하긴 했는데, 2,3번 문제의 혼합이라 보면 된다.
ex) 주문 시스템에서 결제와 재고 관리를 동시에 처리할 때, 여러 서버 간의 트랜잭션을 일관되게 관리해야 한다.
5. 네트워크 지연 및 대역폭 문제
- Stateful 서버는 실시간으로 데이터를 주고받아야 하기 때문에, 네트워크 지연이 성능에 큰 영향을 미친다. Scale-Out을 위해 여러 서버를 사용하면, 서버 간의 통신이 증가하면서 네트워크 지연과 대역폭 문제가 발생할 수 있다.
- 특히, 글로벌 서비스의 경우 지리적으로 분산된 서버 간의 데이터 동기화는 더 큰 지연을 초래할 수 있다.
- L2 ~ L4 하드웨어 장비의 특징과 성능에 대한 이해가 필요하다. 여러 테스트를 거쳐 최적의 비용을 계산해야 하지만 대기업이 아닌 이상 투자를 할 수 있을까? 전문가를 불러오는 편이 좋다. (그럼 신입은 어디서 이런 경험을 쌓을 수 있을까... 이론상 이해는 하지만 슬프다.)
ex) 실시간 채팅 애플리케이션에서 메시지를 여러 서버로 전파할 때, 네트워크 지연으로 인해 메시지 순서가 바뀌거나 지연될 수 있다. 이는 TCP를 사용하는 주된 원인이다. 만약 순서가 중요하지 않고 지연이 신경쓰인다면 UDP를 사용하자.
6. 장애 복구(Failover)의 복잡성
- Stateful 서버는 상태 정보를 메모리에 저장하기 때문에, 서버가 장애가 발생하면 해당 상태 정보가 손실될 수 있다. Scale-Out 환경에서는 장애 복구가 더 복잡해진다.
- 상태 정보를 다른 서버로 실시간으로 복제하거나, 장애 발생 시 빠르게 상태를 복구하는 메커니즘이 필요하다.
ex) 게임 서버에서 특정 서버가 다운되면, 해당 서버의 플레이어 데이터를 다른 서버로 이전해야 하는데, 이 과정에서 데이터 손실이나 지연이 발생할 수 있다. 그래서 여유가 되는 회사들은 같은 내용의 서버를 2개를 운용한다. 이로인해 하나의 서버에 문제가 생겼을 시 다른 서버로 유저들이 이어서 접속할 수 있도록 한다. (하지만 비용이 2배가 든다. 카카오도 비용에 문제가 있어 사용하지 않았다 화재 이후 도입했다. 단, 천재지변이 아닌 다른 이유(잘못된 코드)로 인한 원인은 다시 문제가 발생할 수 있다.)
7. 비용 증가
- Stateful 서버를 Scale-Out하기 위해서는 상태 정보를 공유하기 위한 추가적인 인프라(예: 분산 데이터베이스, 캐시 서버)가 필요하다. 이는 비용을 증가시키는 요인이다.
- 특히, 실시간 동기화를 위해 고성능 네트워크와 저장소를 사용해야 하면 비용이 더욱 증가한다.
ex) Redis 클러스터를 사용하여 세션 데이터를 공유할 경우, 클러스터 유지보수와 확장에 추가 비용이 발생한다.
8. 애플리케이션 설계의 복잡성
- Stateful 서버를 Scale-Out하기 위해서는 애플리케이션 자체를 분산 환경에 맞게 설계해야 한다. 이는 기존의 단일 서버 아키텍처와는 완전히 다른 접근 방식이 필요하다.
- 개발자와 운영팀이 분산 시스템에 대한 깊은 이해가 필요하며, 디버깅과 문제 해결이 더 어려워진다.
ex) 마이크로서비스 아키텍처로 전환할 때, 각 서비스 간의 상태 공유와 통신을 어떻게 설계할지 고민해야 한다.
이런 이유들이 있다. 복잡해보이지만 하나하나 살펴보면 다 연결되어 있고 전부 같은 말(동기화 문제, 설계의 복잡함, 오류 수정 및 예외 대처 힘듦, 비용 소모 큼)을 하고 있다.
[ 대표 게임 ]
MMORPG: 리니지 2M
MOBA: 리그 오브 레전드 (LOL), 오버워치
FPS: 배틀그라운드
[ 장점 ]
실시간 상호작용이 가능하며, 빠른 처리 속도로 다수의 유저와의 동시 플레이가 가능하다.
모든 게임 로직이 서버에서 처리되기 때문에 핵 사용이 어렵다.
[ 단점 ]
개발이 복잡하며, 서버 다운 시 롤백 가능성이 있다.
동시 접속자 수에 제한이 있어 다른 서버나 채널로 분산해야 한다. 이로인해 멀티스레드를 활용하는 것은 필요가 아닌 필수가 됬다.
2. 비동기 온라인 게임 (Stateless 서버)
[ 특징 ]
비실시간 상호작용: 유저 간의 실시간 상호작용이 없거나 제한적이다. 대표적인 장르로는 수집형, 방치형, 퍼즐 게임 등이 있다.
연결 유지 없음: 클라이언트와 서버는 필요할 때만 연결하며, 평소에는 연결이 끊어진 상태로 유지된다. TCG를 예로 들면 현재 필드에 있는 모든 정보를 한꺼번에 전송하고, 그에 대한 정보를 다시 받아오는 형식으로 진행되기에 진행도중 연결이 끊어져도 다시 접속하면 이전의 상태를 이어서 진행할 수 있다.
데이터베이스 의존: 모든 데이터는 데이터베이스에 저장되며, 서버 메모리에 데이터를 보관하지 않는다.
웹 기술 활용: HTTP 프로토콜과 웹 프로그래밍 언어(Node.js, Python, PHP 등)를 주로 사용한다. UDP를 사용하기도 한다.
확장 용이: Scale-Out이 쉽기 때문에 서버 대수를 늘려 확장할 수 있다.
[ 대표 게임 ]
수집형 게임: 확산성 밀리언아서
방치형 게임: 오늘도 환생
퍼즐 게임: 캔디크러시 사가
[ 장점 ]
개발이 비교적 쉽고, 서버 다운 시에도 롤백이 발생하지 않는다.
웹 기술을 활용하여 서버 확장이 용이한 편이다.
[ 단점 ]
실시간 상호작용이 어렵기 때문에, 실시간 컨텐츠를 구현하기에는 한계가 있다.
또한 클라이언트 단독 로직으로 인해 핵 사용 가능성이 있다. 이는 진행시 모든 클라이언트의 정보를 한꺼번에 서버측으로 보내 연산 결과만 가져오는 방식으로 진행되기 때문이다.
3. 두 기술의 혼합 사용
최근에는 게임 컨텐츠가 다양해지면서 Stateful과 Stateless 기술을 혼합하여 사용하는 경우가 많다.
비동기 게임 + 실시간 채팅: Stateless 게임서버에 Stateful 채팅 서버를 추가.
실시간 게임 + 비동기 컨텐츠: Stateful 게임서버에 Stateless 랭킹 시스템 추가.
이렇게 두 기술을 혼합하여 게임의 장점을 극대화할 수 있다.
4. 결론
Stateful과 Stateless 서버는 각각 실시간 온라인 게임과 비동기 온라인 게임에 적합한 기술로, 어떤 기술을 선택할지는 게임의 장르와 컨텐츠에 따라 결정된다. 두 기술은 서로 경쟁하는 관계가 아니라, 각자의 장/단점이 있으며 상호 보완적인 관계로 발전하고 있다.
게임 개발자라면 이 두 기술의 특징을 잘 이해하고, 게임의 목적에 맞는 기술을 선택하는 것이 중요하다.