메가여치님 게임서버 개발 자문

개발과 게임 서버에 대한 자문 정리

이번에 메가 여치님께 개발과 게임 서버에 대해 여러 가지 이야기를 들었다.

처음에는 서버 구조나 네트워크 쪽을 물어보려고 했다. 그런데 막상 이야기를 시작해보니 서버 기술 자체보다 개발할 때 어떤 기준으로 판단하는지가 더 많이 나왔다.

나도 그동안 공부하면서 나름대로 기준을 세우고 있다고 생각했는데, 이야기를 듣다 보니 조금 다르게 생각해볼 부분이 있었다.

가장 먼저 느낀 건 개발에는 생각보다 정답이 없다는 것이었다.

서버를 꼭 여러 개로 나눠야 하는가

예전에는 서버 한 대에서 여러 가지 일을 처리하는 구조도 많이 사용했다.

Client -> Server

규모가 커지면 로그인 서버, 게임 서버, 채팅 서버 같은 식으로 나누기도 한다.

Client -> Game Server / Login Server / Chat Server / DB Server / ...

그런데 서버를 여러 개로 나누는 게 무조건 좋은 건 아니라고 했다.

처음 이 이야기를 들었을 때는 당연한 말이라고 생각했는데, 다시 생각해보니 내가 공부하면서 은근히 반대로 생각하고 있었던 것 같다.

큰 서비스의 서버 구조를 계속 보다 보면 자연스럽게 처음부터 저런 구조로 만들어야 하는 것처럼 느껴진다.

하지만 작은 게임을 만드는 상황이라면 굳이 그럴 이유가 없다.

서버 하나로 충분하다면 그냥 하나로 만들면 된다.

처음부터 서버를 여러 개로 나눠놓으면 코드만 늘어나는 게 아니라 배포나 운영, 서버 간 통신 같은 것도 같이 신경 써야 한다. 아직 문제가 생기지도 않았는데 미래의 문제를 전부 해결하려고 하면 오히려 개발 속도만 떨어질 수 있다.

필요해졌을 때 나누는 방법도 있다.

현재 요구사항 -> 구현 -> 운영 -> 문제 발생 -> 필요한 부분 확장

결국 구조를 잘 만드는 것보다 지금 상황에 맞는 구조를 선택하는 게 먼저라는 이야기였다.

게임 프로토콜은 직접 만들 수도 있다

게임에서 사용하는 네트워크 프로토콜도 게임마다 다를 수 있다.

HTTP처럼 이미 만들어진 프로토콜을 사용할 수도 있고, 게임에서 필요한 형태에 맞춰 별도의 통신 규약을 만들 수도 있다.

게임에서는 패킷 크기나 처리량, 지연 시간 같은 것도 신경 써야 한다. 그래서 게임에 맞는 프로토콜을 따로 만드는 경우가 있다고 했다.

내가 생각했던 것보다 게임 서버의 통신 규약을 직접 설계하는 범위가 넓다는 느낌을 받았다.

대략적으로 보면

Application Layer -> Protocol Layer -> Transport Layer

정도로 생각할 수 있다.

패킷을 받으면 크기나 헤더를 확인하고 어떤 메시지인지 구분한 다음 실제 데이터를 처리한다.

예를 들어 프로토콜 ID를 사용한다면

Packet -> Protocol ID -> Message Type -> Deserialize -> Handler

같은 흐름이 될 수 있다.

서버와 서버 사이에서도 결국 어떤 데이터를 주고받을 것인지 먼저 정해야 한다.

Server -> Server

공부할 때는 네트워크 프로토콜을 주로 이론적으로 봤는데, 실제 게임에서는 게임의 요구사항에 맞춰 통신 규약 자체를 설계한다는 점이 조금 다르게 느껴졌다.

TCP와 UDP

TCP와 UDP에 대해서도 비슷한 이야기를 했다.

TCP는 신뢰성 있는 전송을 제공한다.

그게 게임에서 필요한 조건을 만족한다면 TCP를 사용하면 된다. 게임이라고 해서 무조건 UDP를 써야 하는 건 아니다.

반대로 지연이나 전송 특성 때문에 TCP가 맞지 않는 상황이라면 UDP를 고려할 수 있다.

요즘에는 UDP 위에서 동작하는 QUIC도 있고 HTTP/3가 QUIC을 기반으로 동작한다는 것도 같이 알아두면 좋을 것 같다.

UDP -> QUIC -> HTTP/3

예전에는 TCP와 UDP를 비교하면서 “게임은 UDP” 같은 식으로 외우려고 했던 것 같다.

그런데 이번에 이야기를 듣고 나서는 조금 생각이 바뀌었다.

먼저 어떤 통신이 필요한지 보고 그다음에 기술을 고르는 게 맞다.

책에 나온 코드가 정답은 아니다

개발 공부를 하다 보면 책이나 강의에서 좋은 구조를 많이 보게 된다.

나도 처음에는 그런 구조를 보면 좋은 코드라고 생각하고 그대로 적용하려는 경우가 있었다.

특히 디자인 패턴 같은 걸 공부하면 더 그렇다.

하지만 어떤 책에서 특정 패턴을 사용했다고 해서 내가 만드는 게임에서도 똑같이 써야 하는 건 아니다.

프로젝트 규모도 다르고 팀 구성도 다르고 일정도 다르다. 코드 자체도 다르다.

그래서 패턴을 먼저 정해놓고 거기에 코드를 맞추는 것보다는 지금 해결해야 하는 문제가 뭔지를 먼저 보는 게 맞다고 했다.

문제 -> 요구사항 확인 -> 해결 방법 찾기 -> 선택

디자인 패턴을 공부하는 건 분명 도움이 된다.

다만 패턴을 적용했더니 코드가 더 복잡해졌다면 굳이 사용할 필요가 있는지도 생각해봐야 한다.

간단한 문제를 간단하게 해결할 수 있다면 그냥 그렇게 하는 것도 방법이다.

이건 앞으로 공부할 때도 꽤 신경 써야 할 것 같다.

중복 코드는 어디까지 없애야 하는가

중복 코드에 대한 이야기도 나왔다.

수학으로 생각하면

a + a + a -> 3a

처럼 같은 것을 하나로 묶을 수 있다.

코드도 비슷하다. 반복되는 코드를 함수나 다른 형태로 묶을 수 있다.

그런데 중복이라고 해서 전부 없애야 하는 건 아니다.

몇 줄짜리 코드가 두 번 나왔다고 바로 함수로 빼버리면 오히려 코드를 따라가기가 어려워질 수도 있다.

지금은 똑같아 보여도 나중에 서로 다른 방향으로 수정될 코드일 수도 있다.

그런데 억지로 하나로 묶어놓으면 한쪽을 수정하기 위해 조건문을 계속 추가하게 될 수도 있다.

그래서 중복을 발견했다고 바로 없애는 것보다는 정말 같은 의미의 코드인지 먼저 보는 게 중요하다고 했다.

이건 직접 코드를 많이 작성해봐야 감이 생길 것 같다.

변수 이름에 너무 오래 고민하지 않기

변수 이름은 당연히 중요하다.

코드를 읽는 사람이 봤을 때 이 값이 무엇을 의미하는지 알 수 있어야 한다.

그렇다고 변수 이름 하나를 정하는 데 너무 많은 시간을 쓰는 것도 좋은 방법은 아닌 것 같다.

이름만으로 설명하기 어려운 경우도 있다.

그럴 때는 주석을 쓸 수도 있다.

적절한 이름 + 필요한 주석

결국 코드를 보는 사람이 이 값이 무엇인지, 왜 이런 처리를 하는지 이해할 수 있으면 된다.

이름을 잘 짓는 것도 중요하지만 이름 하나를 완벽하게 만드는 것보다 전체 코드를 이해하기 쉽게 만드는 게 더 중요하다고 생각한다.

자원은 사용한 만큼 관리해야 한다

프로세스가 종료되면 운영체제가 프로세스가 사용하던 자원을 정리한다.

그렇다고 프로그램을 만드는 사람이 자원 관리를 신경 쓰지 않아도 되는 건 아니다.

프로그램이 실행되는 동안에는 메모리 외에도 파일, 소켓, 락, DB 연결 같은 여러 자원을 사용한다.

그래서 기본적으로

Resource Acquire -> Use -> Release

이 흐름은 생각해둘 필요가 있다.

언어마다 관리 방법은 다르지만 자원을 얻었다면 언제 어떻게 해제할지도 같이 생각해야 한다.

특히 서버처럼 계속 실행되는 프로그램이라면 이런 부분이 더 중요할 것 같다.

자료구조와 DB 설계

게임 서버에서 콘텐츠를 하나 만든다고 해서 게임 로직만 작성하면 끝나는 것도 아니다.

어떤 데이터를 메모리에 가지고 있을지 결정해야 하고, DB에는 어떻게 저장할지도 봐야 한다. 클라이언트에 어떤 데이터를 보낼지도 생각해야 한다.

Game Logic -> Data Structure -> Database Schema -> Network Message

따로 보면 각각 다른 문제처럼 보이지만 실제로는 연결되어 있다.

처음 서버 개발을 하면 이런 판단이 쉽지 않다.

“이걸 List로 할까?”

“Dictionary가 맞나?”

“DB에는 어떻게 저장하지?”

“클라이언트에는 어디까지 보내야 하지?”

이런 건 실제 프로젝트를 해보면서 경험이 쌓여야 판단하기 쉬워지는 부분이라고 생각한다.

그래서 처음부터 전부 혼자 결정하려고 하기보다는 기존 프로젝트가 있다면 그 구조부터 보는 게 중요하다는 이야기도 들었다.

팀에서 이미 정해놓은 기준이 있다면 일단 그걸 이해하는 게 먼저다.

혼자 판단하기 어려운 부분은 선임 개발자에게 물어보면 된다. 이것도 개발 과정의 일부라고 했다.

멀티스레드에서 발생하는 이상한 버그

멀티스레드 이야기를 하면서 하이젠버그 버그에 대한 이야기도 나왔다.

평소에는 문제가 발생하는데 디버깅을 시작하면 문제가 사라지는 경우가 있다.

디버거를 붙이거나 로그를 추가하는 것만으로도 실행 타이밍이 달라질 수 있기 때문이다.

예를 들어

Normal Execution -> Bug

였던 게

Debug / Observation -> Timing Change -> Bug Disappears

가 될 수 있다.

경쟁 조건이나 실행 순서 때문에 발생하는 문제라면 특히 찾기 어렵다.

그래서 테스트를 몇 번 해봤는데 문제가 안 생겼다고 해서 안전하다고 판단하면 안 된다.

공유 데이터에 여러 스레드가 접근하는지, 동시에 접근할 수 있는 부분이 어디인지부터 확인해야 한다.

이런 문제는 재현이 안 된다는 것 자체가 문제를 어렵게 만드는 요소인 것 같다.

Unity 초기화 순서도 결국 비슷하다

이 이야기를 듣다가 Unity에서 겪었던 Singleton 문제도 생각났다.

예를 들어 A의 Awake()에서 Singleton을 초기화하고 B의 Awake()에서 그 Singleton을 가져온다고 하자.

원하는 순서대로 실행된다면

A.Awake() -> Singleton.Instance 초기화 -> B.Awake() -> Instance 참조

가 된다.

그런데 다른 객체의 초기화 순서에 의존하고 있다면 문제가 생길 수 있다.

B.Awake() -> Instance 참조 -> Instance == null -> NullReferenceException

이런 식이다.

이 경우 Singleton 코드만 보고 있으면 문제를 찾기 어려울 수도 있다.

실제로는 객체 사이의 초기화 의존성과 Unity의 생명주기 실행 순서가 원인일 수 있기 때문이다.

예전에는 이런 문제가 발생하면 에러가 난 줄부터 확인했다.

그런데 지금 생각해보면 더 중요한 건 “왜 그 시점에 값이 null이었는가”를 따라가는 것이다.

에러가 발생한 위치와 실제 원인이 항상 같은 건 아니다.

최신 기술이 항상 필요한 것은 아니다

개발 공부를 하다 보면 최신 기술을 사용하는 게 더 좋은 개발처럼 보일 때가 있다.

나도 새로운 기술이 나오면 일단 알아둬야 할 것 같은 느낌을 받을 때가 있다.

그런데 실제 프로젝트에서는 꼭 그렇지는 않다.

게임 하나를 만드는 데에도 여러 기술이 들어가고, 기술을 하나 추가할 때마다 배워야 할 것도 생기고 코드도 늘어난다.

Requirement -> Technology Selection -> Implementation -> Testing -> Optimization -> Product

최신 기술이 정말 필요한지부터 봐야 한다.

이미 충분히 검증된 기술로 원하는 결과를 만들 수 있다면 굳이 새로운 기술을 넣지 않는 게 나을 수도 있다.

새로운 기술을 공부하는 것과 프로젝트에 실제로 사용하는 것은 다른 문제다.

이 둘을 구분하는 게 생각보다 중요할 것 같다.

엔진과 콘텐츠를 분리하는 이유

엔진과 게임 콘텐츠를 분리하는 이야기도 나왔다.

Engine -> Rendering / Physics / Input / Audio / Network / ...

Game Content -> Character / Map / Quest / Item / Story / ...

엔진과 콘텐츠가 강하게 묶여 있으면 나중에 엔진의 일부를 바꿀 때 영향을 받는 범위가 커질 수 있다.

그래픽 기술만 해도 계속 바뀐다.

DirectX 9 -> DirectX 11 -> DirectX 12 -> Ray Tracing -> ...

장기간 서비스할 게임이라면 이런 변화에 대응할 수 있는 구조를 생각해볼 필요가 있다.

그런데 게임 하나를 만들어서 판매하고 그 이후 엔진 자체를 계속 발전시킬 계획이 없다면 이야기가 다르다.

굳이 엔진과 콘텐츠를 지나치게 분리할 필요가 없을 수도 있다.

이것도 결국 프로젝트의 목적을 먼저 봐야 한다.

GPU를 사용하는 방법

GPU 이야기도 잠깐 나왔다.

GPU는 화면을 그리는 데만 사용하는 것은 아니다.

CUDA처럼 GPU의 병렬 연산 능력을 일반적인 계산에 활용할 수도 있고, 그래픽에서는 여러 종류의 셰이더를 사용한다.

지오메트리 셰이더 같은 기술도 있고, 구름이나 물 같은 효과를 만드는 방법도 하나만 있는 게 아니다.

이쪽은 이야기를 들으면서 내가 모르는 게 아직 많다는 생각이 들었다.

그래픽 기술도 하나를 알고 있다고 해서 그게 유일한 방법은 아니다.

같은 결과를 만들더라도 여러 가지 방법이 있을 수 있고, 결국 상황에 맞는 방법을 찾아야 한다.

게임을 왜 만드는가

기술적인 이야기만 있었던 건 아니다.

게임 자체에 대한 이야기도 했다.

게임은 단순히 프로그램을 만드는 것과는 조금 다른 것 같다.

현실에서는 직접 해보기 어려운 경험을 게임 안에서 만들어줄 수도 있다.

비행기를 직접 조종하지 않아도 비행 시뮬레이션을 할 수 있고, 실제로 배를 타고 여행하지 않아도 게임 안에서 항해를 경험할 수 있다.

현실에서 하기 어려운 경험 -> 게임으로 구현 -> 플레이어의 경험

이런 식으로 생각하면 게임 기술을 보는 방식도 조금 달라진다.

그래픽이 좋아졌다는 사실만 보는 게 아니라 그 기술을 이용해서 개발자가 어떤 경험을 만들려고 했는지를 보는 것이다.

결국 기술은 수단이고 게임에서 중요한 건 그걸 통해 무엇을 경험하게 할 것인가에 가까운 것 같다.

사람을 모으는 게임

게임 안에서 사람들이 서로 이야기할 수 있는 공간을 만드는 것에 대한 이야기도 있었다.

생각해보면 꼭 복잡한 기술이 필요한 건 아닐 수도 있다.

사람 -> 공간 -> 대화 -> 경험

사람들이 현실에서 특정 장소에 모여 이야기를 나누는 것처럼 게임에서도 사람들이 자연스럽게 모일 만한 공간을 만들 수 있다.

메타버스도 이런 관점에서는 생각해볼 만하다.

메타버스라는 단어에 여러 기술이나 사업 모델을 붙이는 것과, 사람이 가상의 공간에서 다른 사람과 경험을 만드는 것은 다른 문제다.

이야기를 듣고 나니 게임을 만들 때 기술부터 생각하기보다는 그 기술로 뭘 하려고 하는지 먼저 보는 것도 필요하겠다는 생각이 들었다.

개발자가 공부해야 하는 것

모든 것을 깊게 공부할 필요는 없다는 이야기도 기억에 남았다.

알고리즘이나 자료구조를 잘 모른다고 해서 게임 개발을 못 하는 건 아니다.

물론 알면 좋다.

운영체제나 컴퓨터 구조도 마찬가지다.

게임 개발을 하다 보면 메모리, CPU, GPU, 스레드, 프로세스, 캐시, 네트워크 같은 것들이 실제 문제로 등장한다.

이런 기반 지식이 있으면 문제가 생겼을 때 원인을 이해하기가 조금 쉬워진다.

그렇다고 모든 걸 공부한 다음 개발을 시작해야 하는 건 아니다.

오히려 개발하다가 문제가 생기면 그때 필요한 지식을 찾아서 공부하는 방식도 괜찮다고 했다.

개발 -> 문제 발생 -> 필요한 지식 공부 -> 적용

나도 그동안 이것저것 한꺼번에 알아두려고 했던 부분이 있었다.

그런데 실제 프로젝트에서 필요한 것부터 공부하고, 막히는 부분이 생기면 그걸 해결하기 위해 더 깊게 들어가는 방식도 같이 가져가려고 한다.

자문을 받고 나서

이번 자문에서 새로운 기술 하나를 배웠다는 느낌은 아니었다.

오히려 내가 개발을 공부하는 방식을 한번 다시 보게 됐다.

공부하다 보면 어떤 기술이나 패턴을 알게 되고, 그걸 사용하는 게 좋은 개발이라고 생각하기 쉽다.

나도 비슷했다.

서버는 이렇게 구성해야 하고, 이 상황에서는 이런 패턴을 써야 하고, 중복은 최대한 제거해야 한다는 식으로 어느 정도 답을 정해놓고 공부했던 것 같다.

그런데 실제 프로젝트에서는 상황이 다 다르다.

서버 하나로 시작할 수도 있고 필요하면 나눌 수도 있다.

TCP를 사용할 수도 있고 UDP를 사용할 수도 있다.

디자인 패턴을 사용할 수도 있고 그냥 단순하게 구현할 수도 있다.

자료구조도 마찬가지다.

중요한 건 어떤 기술을 사용했는지가 아니라 왜 그렇게 결정했는지를 설명할 수 있는가에 더 가까운 것 같다.

문제 -> 제약 조건 -> 선택지 -> 판단 -> 구현 -> 검증

개발하다 보면 이 과정을 계속 반복하게 된다.

처음부터 답을 알고 만드는 경우는 생각보다 많지 않다.

일단 만들어보고 문제가 생기면 원인을 찾는다.

필요하면 구조를 바꾼다.

이번 자문을 듣기 전에는 공부한 내용을 실제 개발에 적용하는 과정에도 어느 정도 정답이 있을 거라고 생각했던 것 같다.

지금은 조금 다르게 생각한다.

기술을 많이 알고 있는 것과 그 기술을 적절한 곳에 사용하는 것은 별개의 능력이다.

앞으로 공부할 때도 “이 기술은 이렇게 사용한다”에서 끝내지 않고, “왜 이렇게 만들었는지”, “다른 방법은 없는지”, “굳이 이 기술이 필요한 상황인지”까지 같이 생각해보려고 한다.

이번 자문에서 가장 많이 남은 것도 결국 이 부분이다.

기술을 선택하는 것보다 먼저 해결해야 할 문제가 무엇인지 보는 것.

그게 개발할 때 생각보다 중요하다는 걸 다시 느꼈다.