게임 서버 - 서버 권한 구조

반짇고리·2026년 1월 16일

게임 서버 제작기

목록 보기
1/15

게임 서버를 만들겠다고 마음먹고 가장 먼저 떠올린 건 몬스터도, 아이템도, 화려한 스킬도 아니었다...
가 아니라 사실은 다 떠올렸다.

어쩌겠는가. 사람은 원래 시각에 약하다. 움직이는 플레이어, 스킬에 죽는 몬스터들, 사방으로 흩어지는 아이템들, 그걸 또 잽싸게 튀어 주워 먹는 장면을 먼저 상상하는 게 자연스럽다. 나도 그랬다. 심지어 그럴싸한 포트폴리오 영상을 상상하며 마음속으로는 이미 박수까지 조금 받았다.

그런데 막상 서버를 만들려고 앉아보니, 생각보다 지루하고 현학적인 질문이 먼저 튀어나왔다.

이 게임에서 누가 진실을 말할 수 있는가?

처음엔 너무 철학적인 질문 아닌가 싶었다. 지금 게임 서버를 만들려는 거지, 갑자기 존재론 철학 강의를 준비하려는 게 아닌데 말이다.

하지만 게임 서버에서는 이 질문이 꽤 중요하다. 아니, 사실상 여기서 모든 게 시작된다.

클라이언트가 “나 템 먹었어”라고 말했을 때 서버가 아무런 의심 없이 “응, 알겠어”라고 답해도 되는가. 클라이언트가 “내 공격으로 저 몹한테 대미지를 줬어”라고 말했을 때 서버가 “그렇구나” 하고 몬스터의 체력을 아무 의심 없이 깎아도 되는가. 클라이언트가 “나는 아직 살아있어”라고 말했을 때 서버는 그 말을 어디까지 믿어야 하는가.

이런 질문에 대충 답하게 된다면 조금만 구현을 쌓아도 다시 앞을 막을 것이다. 상상만 해도 너무나도 곤란하지 않겠는가.


앞서

이 글은 가제: Loot of Legends 게임 서버 제작기의 첫 번째 글이다.

당연하게도 첫 글에서 모든 문제를 풀 생각은 없다.애초에 풀 수도 없거니와. 아직 모르는 부분도 많고, 만들다 보면 다시 고칠 생각도 분명 생길 것이다. 이 시리즈는 문제를 마주치는 순서대로 쌓아가는 개발 기록에 가깝다.

그래서 첫 글에서 말하려는 주제도 거창한 완성이 아니라 생각보다 더 초입에 위치한 결정이다.

게임 상태의 최종 판단권은 클라이언트가 아니라 서버가 가져야 한다.

다르게 말하면, 클라이언트는 결과를 통보하지 않고 입력 의도를 보낸다. 서버는 그 입력을 검증한 뒤에만 상태를 바꾼다.

이 원칙을 보통 서버 권한 구조(Server Authoritative Architecture) 라고 부른다고 한다.

뭐 어렵게 들릴 수도 있지만, 결국 이런 이야기다.

클라이언트: 나 이 템 먹음.
서버: ㅇㅇ 그래도 내가 판단한다.

조금 매정해 보이지만 게임 서버는 원래 그래야 한다고 생각한다.

왜?


클라이언트를 믿으면 빨라진다

처음 멀티플레이 게임을 만들 때 제일 빠른 길은 클라이언트에게 많은 걸 맡기는 것이다.

클라이언트가 이동을 계산한다.
클라이언트가 공격 결과를 계산한다.
클라이언트가 아이템 획득 결과를 계산한다.
서버는 그 결과를 받아서 다른 클라이언트에게 전달한다.

이러면 화면은 빨리 나온다. 개발하는 사람 입장에서도 분명히 기분이 좋을 것이다. 무언가 움직이고 있으니 프로젝트가 꽤 진행된 것처럼 보인다.

문제는 그 다음이다.

나는 아이템 A를 획득했다.
내 공격이 몬스터에게 명중했다.
내 체력은 아직 풀피다.
나는 이 방에 들어갈 수 있다.

클라이언트가 이런 결과를 보내고, 서버가 그걸 그대로 믿는다면 사실상 게임 상태는 클라이언트 손에 있다.

그렇다면 서버는? 조금 비싼 확성기가 될 뿐이다.

이 구조에서는 치트 방어도 어렵고, 상태 일관성도 흔들린다. 특히 멀티플레이 게임에서는 같은 아이템을 두 명이 동시에 먹으려고 하거나, 이미 끝난 전투에 한 템포 늦게 패킷이 도착하거나, 연결이 이미 끊긴 플레이어의 상태가 남는 일이 생긴다.

그때마다 서버가 “클라이언트가 그렇게 말했는데요?”고 답할 수는 없다.
서버는 심판이어야지, 목격자여서는 안 된다는 것이 이 설계의 원칙이다.


사소한 선택이 오래 따라온다

나는 개발에 발을 담그기 전에도 문제를 해결하는 일을 했다. 그때 지겹도록 배운 사실이 하나 있다.

대부분의 큰 문제는 처음부터 큰 얼굴을 하고 오지 않는다.

처음에는 아주 사소한 이상 징후처럼 보인다. 센서 하나가 이상한 값을 뱉는다든가, 어떤 장비가 아주 가끔, 진짜 몇 시간에 한 번씩 늦게 반응한다든가, 로그에 뭔가 찜찜한 메시지가 한 줄 남는다든가. 그걸 대충 넘기면 조금 뒤에는 하루에 수억, 수십억짜리 손해를 끼칠 수 있는 문제로 돌아온다.

게임 서버도 비슷하지 않을까?

처음에는 “일단 클라이언트가 보낸 결과를 믿자”가 별일 아닌 선택처럼 보인다. 하지만 이 선택은 조금 뒤 전투, 드랍, 인벤토리, 정산을 붙이려는 순간부터 계속 따라다닐 것이다.

아이템 획득을 예로 들면 이렇다.

클라이언트가 결과를 보내는 구조라면:

클라 -> 서버: 아이템 획득
서버 -> 다른 클라들: 이 클라가 아이템 획득했다는 결과를 전파

서버가 권한을 갖는 구조라면:

클라 -> 서버: 나 이 템 먹으려고 시도함
서버:
	- 유효한 세션이야?
    - 플레이어는 살아있어?
    - 템은 아직 존재하고?
    - 그 방은 여전히 유효해?
서버 -> 클라들: 아이템 획득 결과 전파

겉으로 보면 두 구조는 비슷하다. 둘 다 결국 “누군가 아이템을 먹었다”는 결과로 이어진다.

하지만 책임의 위치가 다르다.

첫 번째는 클라이언트가 결과를 만든다. 두 번째는 서버가 결과를 만든다.


두 가지 선택지

처음 구조를 잡을 때 선택지는 두 가지였다.

1. 클라이언트 권한 구조

클라이언트가 대부분의 결과를 계산하고 서버는 중계한다.

장점은 빠르다. 특히 화면을 먼저 보여줘야 하는 프로토타입에서는 매력적이다. 내가 원하는 것도 사실은 빨리 움직이는 화면이었다. 앞에서도 언급했듯이, 사람 마음이 다 그렇다. 테스트 결과의 PASS 글자보다는 Unity 클라에서 보이는 스킬 이펙트가 더 맛있어 보이는 건 당연지사다.

하지만 게임 서버 포트폴리오로는 좋지 않다고 판단했다.

  • 클라이언트 치트에 취약하다.
  • 같은 상태를 여러 클라이언트가 다르게 볼 수 있다.
  • 아이템 경합, 전투 판정, 정산 같은 핵심 로직을 서버가 설명할 수 없다.
  • 뒤늦게 서버 권한 구조로 바꾸려면 프로토콜과 테스트를 다시 갈아엎어야 한다.

빠른 길처럼 보이지만, 되돌아오는 길이 너무 길다.

2. 서버 권한 구조

서버가 상태를 소유하고, 클라이언트는 입력 의도만 보낸다.

클라이언트가 “아이템을 먹었다”고 말하는 대신 “아이템을 먹으려고 했다”고 말한다. 클라이언트가 “공격이 맞았다”고 말하는 대신 “공격을 시도했다”고 말한다. 이후 판단은 서버가 한다.

장점은 분명하다.

  • 치트 저항성이 좋아진다.
  • 상태 일관성을 서버 기준으로 유지하기 쉽다.
  • 테스트가 “무엇을 보호하는지” 명확해진다.
  • 설명 가능한 서버 책임 경계가 생긴다.

단점도 있다.

비교적 느리다. 화면보다 프로토콜, 세션, 유효성 검증, 디스패치 같은 지루하고 현학적인 단어가 계속 나온다. 어쩌면 그래서 더 해야 한다. 재미없어 보이는 부분이 보통 뒤쪽 문제를 크게 줄여준다.


결국

나는 서버 권한 구조를 선택했다.

클라이언트는 입력 의도를 보내고, 서버가 검증한 뒤 상태 변경을 확정한다.

이 결정으로 최소한의 구조를 정하자면,

  • Session은 연결 또는 런타임 참조 상태를 나타낸다.
  • SessionManager는 세션 생성, 조회, 만료 같은 lifecycle 정책을 담당한다.
  • Dispatcher는 패킷 type을 보고 handler로 넘기는 경계가 된다.
  • RoomPlayer는 서버가 소유해야 할 game state의 시작점이 된다.
  • item, combat, inventory, settlement도 서버가 승인한 결과만 반영해야 한다.

아직 구현되지 않은 게 너무 많았기 때문에 먼저 최소한의 기준을 잡아야 했다.

누가 입력을 보내는가?
누가 검증하는가?
누가 상태를 바꾸는가?
누가 결과를 알리는가?

나는 이 질문들의 답을 서버 쪽에 모았다.

그게 첫 번째 결정이었다.


처음 잡은 흐름

당연하게도 처음이라 미래의 모든 구현을 알고 있다고 말하긴 어렵다.

오히려 반대에 가깝다. 어떤 전송이 먼저 안정될지, room 처리는 어디까지 쪼개야 할지, 실제 테스트는 어떤 모양으로 닫힐지 아직 많이 흐렸다. 그래도 방향만큼은 분명했다.

처음부터 네트워크 루프, 세션 수명주기, 패킷 수신/검증/디스패치의 최소 경계를 먼저 잡으려 했다. 성능, 신뢰성, 보안을 우선하고 서버 권한 원칙을 고수해야 한다는 생각도 같이 붙어 있었다.

흐름은 대략 이랬다.

packet 수신
-> header 검증
-> session 찾기 또는 생성
-> 중복 / window 밖 packet 폐기
-> PacketType 기반 dispatch
-> 필요 시 response 또는 ack 송신
-> session timeout cleanup

구조도 비슷하게 갈라졌다. 실행 흐름을 담당하는 영역, packet과 transport를 다루는 영역, game state를 다루는 영역을 분리하려 했다. 그 안에 Server, Session, SessionManager, Dispatcher, PacketParser, Room, Player 같은 책임 후보가 놓였다.


아직 남은 것들

처음 잡은 기준은 방향을 잡는 데 의미가 있었지만, 지금 보면 한계도 분명하다.

첫째, 처음에는 RUDP 프로토콜 중심 구상이 강했다. 그리고 조금 뒤 프로젝트 범위를 다시 좁히면서 먼저 TCP MVP 범위로 제한했다.

둘째, 서버 권한 원칙은 packet parser, session lifecycle, room state, loot resolution, settlement boundary를 테스트로 세워야 한다.

셋째, 당연하지만 “서버가 판단한다”는 말만으로는 부족하다. 서버가 어떤 데이터를 신뢰하고, 어떤 입력을 거부하고, 어떤 순서로 상태를 변경하는지까지 프로토콜과 테스트로 고정해야 한다.

profile
Sapere Aude!

0개의 댓글