글 목록으로

온라인 게임 개발자가 말하는 렉과 서버비

게임 서버는 무엇을 하고, 핑이 낮아도 왜 버벅일까요? 혼자 만든 웹 MMORPG를 예로 렉이 생기는 과정과 서버 비용의 구조를 정리했습니다.


안녕하세요. 혼자 웹 MMORPG 모닥 온라인을 만들고 있습니다.

지난 글에서는 갑자기 사람이 몰린 뒤 서버와 사냥터를 손본 이야기를 했습니다. 게임을 소개하면서 어떻게 만들었는지, 서버비는 얼마나 드는지 질문을 받기도 했습니다. 이번에는 서버가 하는 일부터 시작해서 렉이 생기는 이유와 서버비가 나가는 구조까지 정리해보려고 합니다.

전공자가 아닌 제가 독학하며 만든 게임을 바탕으로 쓴 글입니다. 온라인 게임 전체의 정답이라기보다, 작은 게임 하나가 이렇게 돌아가는구나 하고 읽어주시면 좋겠습니다. 잘못 설명한 부분이 있다면 지적도 환영합니다.

게임을 하다 보면 옆에서 같이 달리던 사람이 갑자기 멈췄다가 멀리 순간이동할 때가 있습니다. 핑은 괜찮은데 버벅일 때도 있고요. 이게 왜 그런지 보려면 먼저 서버와 내 컴퓨터가 각각 무슨 일을 하는지 봐야 합니다.

서버가 보내는 것은 화면이 아니라 데이터입니다

게임 서버라고 하면 큰 컴퓨터 안에서 마을이 돌아가고, 우리는 거기에 접속해서 캐릭터를 하나씩 움직이는 모습이 떠오릅니다. 그런데 모닥 서버가 보내주는 것은 이런 데이터입니다.

서버의 위치·체력 데이터와 클라이언트의 그림을 합쳐 게임 화면을 그리는 과정

우리가 캐릭터로 보는 것도 서버에서는 영어와 숫자로 된 데이터입니다. 누가 어디 있고, 체력이 얼마고, 지금 무엇을 하는지 같은 값들이죠.

우리가 실행하는 게임 프로그램을 클라이언트라고 부릅니다. 클라이언트는 갖고 있던 캐릭터와 건물 그림을 꺼내 저 숫자에 맞춰 화면에 그립니다. 내가 누른 키나 이동 목표를 서버에 보내는 것도 클라이언트의 일입니다.

서버는 화면에 이미지를 그리는 대신 좌표, 체력, 이동과 공격 같은 데이터를 계산합니다. 성능이 같은 외형 스킨만 100개 추가한다고 이동·전투 계산이 100배 늘어나지는 않습니다. 주로 늘어나는 것은 클라이언트에 제공할 이미지 용량입니다.

그렇다면 캐릭터가 움직이고 싸울 때는 어떤 데이터가 오갈까요?

실제 전투 화면 아래에 이동·공격·스킬 요청과 서버 응답을 나란히 표현한 설명 도식
위는 실제 플레이, 아래는 송수신 구조를 설명하기 위해 구성한 예시입니다. 패킷을 그대로 녹화한 화면은 아닙니다.

왼쪽이 보내는 명령, 오른쪽이 받는 데이터입니다. 이동 목표를 지정하면 world:goal, 공격 대상을 고르면 combat:target, 스킬을 누르면 combat:skill을 보냅니다. 서버에서는 좌표와 타격 결과, 남은 체력 같은 정보가 돌아옵니다. 모닥은 실제로 MessagePack을 사용하며, 그림에서는 읽기 쉽게 일부 필드를 풀어 썼습니다.

공격은 요청하고, 판정은 서버가 합니다

내 컴퓨터가 “보스에게 100만 데미지를 넣어서 한 방에 잡았다”고 보냈는데 서버가 그대로 받아주면 게임이 엉망이 될 겁니다.

공격 요청을 받은 서버가 사거리를 확인하고 피해량과 남은 체력을 계산하는 예시

그래서 내가 보내는 것은 “이 대상을 공격하겠다”는 요청입니다. 서버는 공격할 수 있는지 확인하고, 가능하면 피해를 계산해서 결과를 알려줍니다. 위 코드는 그중 사거리 검사만 간단히 보여준 것입니다. 실제로는 공격 간격이나 캐릭터 상태 같은 조건도 확인합니다.

이렇게 서버가 최종 판정을 맡는 구조를 Authoritative Server라고 부릅니다. 내 컴퓨터에서 공격력이나 체력을 고쳐도 서버가 그대로 인정하지 않으니, 이런 종류의 조작을 막는 데 도움이 됩니다. 대신 서버가 전투와 각종 규칙을 처리해야 하므로 사람이 늘면 계산 부담도 커집니다.

이동을 어디까지 클라이언트에 맡길지는 게임과 조작 방식에 따라 다릅니다. 모닥도 일부 수동 이동은 클라이언트가 계산한 위치를 보내고 서버가 이동 거리와 지형 등을 검사합니다. 클라이언트가 계산을 한다는 것과 최종 결과를 마음대로 정할 수 있다는 것은 다른 이야기입니다.

핑이 100ms면 결과도 0.1초 만에 올까요?

핑은 서버에 신호를 보내고 답이 돌아오기까지 걸린 시간입니다. 100ms면 왕복에 0.1초가 걸렸다는 뜻입니다.

그런데 핑이 100ms라고 게임에서 처리한 결과까지 꼭 0.1초 만에 받는 것은 아닙니다. 서버가 언제 계산하고, 언제 결과를 보내는지도 봐야 합니다.

모닥 서버는 초당 20번, 50ms 간격으로 게임 속 상황을 갱신하도록 되어 있습니다. 누가 어디로 움직였는지, 공격할 거리에 들어왔는지, 체력이 얼마나 남았는지 확인합니다. 이런 계산 한 차례를 틱이라고 부릅니다.

예를 들어 몬스터가 나를 쫓아오는데 서버가 초당 4번만 계산한다면 이동과 공격 가능 여부를 0.25초 간격으로 확인하게 됩니다. 도망가라는 입력을 보내도 다음 계산 차례까지 기다리는 시간이 길어질 수 있습니다. 초당 20번 계산하면 그 간격이 0.05초로 짧아지고요.

하지만 계산한 위치와 체력을 사람들에게 알려주는 기본 주기는 초당 4번입니다. 계산할 때마다 모두 보내면 통신량이 늘어나니, 상황은 자주 계산하고 현재 상태는 간격을 두고 보냅니다.

50ms마다 계산하고 약 다섯 번 계산할 때마다 현재 상태를 전송하는 도식

점 하나가 계산 한 번입니다. 대략 다섯 번 계산하는 동안 현재 상태를 한 번 찍어 보내는데, 이렇게 특정 시점의 상태를 담은 것을 스냅샷이라고 부릅니다. 맞았다거나 스킬을 썼다는 사건은 이 주기와 별도로 더 자주 모아 보냅니다. 전투 반응까지 오래 기다리게 하면 답답하니까요.

이동 요청 한 번을 따라가보겠습니다.

핑 100ms인 상황에서 이동 요청이 100ms 틱에 처리되고 250ms 상태가 300ms에 도착하는 예시

그림은 왕복 100ms가 양쪽에 50ms씩 걸리고, 요청이 50ms 틱의 처리 차례를 막 놓쳤다고 가정한 예시입니다. 계산과 전송의 상대적인 시점도 설명을 위해 정했습니다.

내가 “x 200으로 가겠다”고 보내면 50ms 뒤 서버에 도착하고, 다음 계산 차례인 100ms 틱에서 처리됩니다. 처음 위치는 x 100이었는데, 처리 후 틱마다 105, 110, 115, 120으로 이동합니다. 250ms에 그때 위치인 120을 보내고 돌아오는 데 50ms가 더 걸려 내 컴퓨터는 300ms에 받습니다.

목적지는 200이어도 아직 가는 중이니 돌아온 위치는 120일 수 있습니다. 핑에 더해 계산 차례와 전송 차례를 기다리는 시간이 붙는 겁니다. 모든 요청이 300ms 걸린다는 뜻은 아닙니다.

계산 자체가 밀릴 수도 있습니다. 50ms마다 해야 하는 일에 80ms가 걸리면 정해진 주기를 따라갈 수 없습니다. 핑이 낮아도 서버 계산이 밀리거나 내 컴퓨터가 화면을 늦게 그리면 버벅일 수 있습니다.

그래서 렉이 걸린다는 이야기를 들으면 클라이언트의 화면 그리기, 통신, 서버의 틱 처리 중 어느 단계가 늦어지는지 함께 확인해야 합니다.

멈췄다가 순간이동하는 이유

아래는 받은 좌표를 그대로 화면에 적용하는 모습을 단순화한 예시입니다. O는 위치 데이터를 받고 있는 상태, X는 해당 캐릭터의 새 위치를 받지 못하는 구간을 뜻합니다.

좌표를 받는 순간에만 움직이는 캐릭터가 새 위치를 받지 못하면 멈췄다가 멀리 이동하는 설명 애니메이션

옆 사람의 새 위치를 모르면 클라이언트는 마지막으로 받은 위치에 그 캐릭터를 남겨둘 수 있습니다. 그러다 최신 좌표를 받았는데 지금 그린 위치와 차이가 크면 한 번에 맞춰버립니다. 우리 눈에는 순간이동처럼 보이죠. 화면에서 서 있었다고 서버에서도 계속 그 자리에 있었던 것은 아닐 수 있습니다.

이런 끊김을 덜 느끼게 하는 방법이 있습니다. 통신 자체를 빠르게 만들기보다는 데이터가 오는 사이를 클라이언트가 자연스럽게 그려주는 방식입니다.

내 캐릭터는 내가 누른 입력을 아니까 서버 답을 기다리는 동안 먼저 움직여 보여줄 수 있습니다. 나중에 서버 결과와 차이가 나면 맞춥니다. 클라이언트 예측과 보정의 기본 아이디어입니다.

남의 캐릭터는 다음 입력을 모르니 받아둔 위치 사이를 이어서 보여줄 수 있습니다. 100에 있다가 120에 있었다면 그 사이를 105, 110, 115처럼 채워서 움직이게 그립니다. 이것이 보간, Interpolation입니다. 두 위치를 이미 받아야 사이를 채울 수 있으므로, 보통 화면을 조금 늦춰 보여주는 대가로 부드러움을 얻습니다.

최신 위치 이후에도 잠깐 데이터가 안 오면 가던 방향과 속도로 조금 더 움직일 것이라 예상해 그리기도 합니다. 이것을 Extrapolation이라고 부릅니다. 오래 끊겨도 계속 맞힐 수는 없어서, 예상해서 움직이는 시간에는 제한을 둡니다.

궁수는 보간으로 움직이고 기사는 보간에 제한된 예상 이동을 더한 뒤 새 좌표에 맞추는 비교 애니메이션

앞의 Raw에서는 둘 다 수신 좌표로만 그려 뚝뚝 움직였습니다. 이번에는 궁수에게 보간을 적용해 받아둔 위치 사이를 잇고, 기사에게는 Extrapolation까지 더했습니다. 기사는 X인 동안에도 Predict 화살표 방향으로 잠깐 더 움직이다가, 새 데이터를 받으면 O로 바뀌고 Sync!가 뜨면서 차이를 맞춥니다.

이 애니메이션은 짧은 구간을 느리게 늘린 설명용 연출입니다. 캐릭터별 O/X는 기법을 비교하기 위한 표시이며, 하나의 WebSocket 연결에서 특정 캐릭터의 패킷만 유실되는 현상을 재현한 것은 아닙니다.

사람이 늘면 보내는 양은 더 빠르게 늘어납니다

이제 서버 비용 이야기입니다. 모닥불 주변에 사람들이 모여 있다고 해보겠습니다.

플레이어가 늘면서 서로에게 전달해야 하는 상태 정보가 증가하는 도식

새 사람의 정보를 기존 사람들에게 보내고, 새 사람에게도 기존 사람들의 정보를 보내야 합니다. 한 공간 안에서 모두의 상태를 모두에게 보내는 구조라면 사람이 늘 때 전송량은 더 빠르게 늘어납니다.

한 사람의 상태가 100바이트라고 가정하면, 100명분을 100명에게 초당 4번 보내는 양은 초당 약 4MB입니다. 같은 조건에서 10명이면 0.04MB입니다. 사람은 10배인데 보내는 양은 100배가 됩니다. 압축과 통신 부가 정보를 제외한 단순 계산입니다.

전투 결과를 한 번 계산하고 한 번 포장해 여러 사람에게 공유하더라도, 실제로 데이터를 전달할 연결은 계속 늘어납니다. 이 예시는 모두가 같은 공간에 있을 때의 이야기입니다. 지역을 나누거나 주변에 필요한 정보만 보내면 전체 동접에 대해 항상 이렇게 증가하는 것은 아닙니다.

모닥은 월 트래픽이 5TB 안팎인 규모로 운영하고 있습니다. 아래는 10월 26일 기준으로 확인한 사용량 화면으로, 4.32TB가 표시되어 있습니다.

10월 26일 기준 트래픽 사용량 4.32TB

운영하면서 보는 트래픽은 대부분 서버에서 유저에게 보내는 쪽입니다. “여기로 가겠다”는 짧게 보내도 서버는 주변 사람과 몬스터의 상태를 계속 알려줘야 하니까요.

이렇게 데이터를 보내는 것도 비용입니다. 기본 요금에 일정량 포함되기도 하고, 넘으면 추가 요금이 붙기도 합니다.

예를 들어 AWS EC2 미국 동부의 일반 인터넷 송신 요금으로 계산하면 첫 유료 구간은 GB당 0.09달러입니다. AWS 서비스 전체에서 공유하는 월 100GB 무료분을 모두 쓸 수 있다고 보면, 5TB 안팎을 보낼 때 전송료만 대략 400~450달러가 됩니다. 지역과 요금제에 따라 조건은 달라집니다.

그래서 서버를 고를 때 CPU와 메모리뿐 아니라 포함 트래픽과 초과 요금도 봐야 합니다. 게임에서도 굳이 보낼 필요 없는 정보는 줄여야 하고요.

슬라임의 이름을 매번 다시 보낼 필요는 없습니다

슬라임이 한 발짝 움직였다고 그 슬라임의 모든 정보를 다시 보낼 필요는 없습니다.

몬스터가 등장할 때 보내는 초기 정보와 이후 반복 전송하는 위치·체력 정보 비교

처음 등장할 때는 전달할 정보가 많습니다. 식별자, 종류, 이름, 레벨, 최대 체력, 보스 여부, 사용할 그림까지 알아야 제대로 표시할 수 있습니다.

그다음부터는 “아까 그 7번이 지금 여기 있고 체력은 80이다”라고 보내면 됩니다. 이름과 생김새는 이미 알고 있으니까요. 모닥도 초기 정보와 반복 상태를 나눠두었습니다. 상태 효과나 전투 정보가 필요하면 더 붙고, 몬스터 목록이나 표시 정보가 바뀌면 초기 정보를 다시 보냅니다.

이것은 이름처럼 자주 바뀌지 않는 정보를 반복 상태에서 분리한 것입니다. 매번 바뀐 몬스터만 골라 보내는 델타 전송과는 다릅니다. 현재 모닥의 기본 스냅샷은 해당 지역의 몬스터 상태 목록을 함께 보냅니다.

게임 화면에서는 캐릭터가 몇 발짝 움직이고 몬스터를 한 대 때렸을 뿐인데, 그 뒤에서는 계산과 전송, 화면 그리기가 각자의 주기로 이어집니다. 작은 게임을 운영하면서도 이 사이를 맞추는 일이 꽤 많았습니다.

긴 글 읽어주셔서 감사합니다. 직접 플레이하고 의견을 보내주신 분들께도 감사드립니다.