이전에 보았던 Tucker님의 게임 네트워킹의 이해 영상을 마저 시청하였다. 오늘은 Socket Programming과 MMORPG 서버 구조 편까지 전부 시청하여 마무리 정리까지 해보려고 한다.

 

https://youtu.be/aWq9C7RARJI

 

 

1. Socket Programming

컴퓨터의 네트워킹은 NIC에서 이루어진다. NIC에서 데이터를 주고 받는 것을 제어할 수 있게 만들어주는 것을 Socket이라고 하고, 이 Socket을 이용하여 Programming을 하는 것을 Socket Programming이라고 한다. 자세히 보면 복잡하긴 하지만 궁극적으로 추구하는 것은 Input과 Output을 다루는 것이다. 이 I/O를 다룰 떄 중요한 기능 2가지가 Read와 Write이다. Read와 Write를 하는 방식은 크게 2가지가 있는데, 동기식과 비동기식이다. 동기식은 Read를 요청하면 완료될 때까지 대기하는 방식이다. C언어에서 scanf를 사용하면 입력을 받아올 수 있는데, 입력이 될 때까지 프로그램이 잠시 멈춘다. 입력이 들어오면 해당 지점에서 프로그램을 재개하는 식이다. 이 방식의 문제점은 커넥션이 1개 뿐이라면 기다려도 상관없지만, 커넥션이 많아진다면 여러 커넥션을 처리해야하는 서버의 입장상 하나의 커넥션을 위해 계속 대기하고 있는 것이 굉장히 비효율적이라는 것이다. 때문에 입력이 올 때까지 기다리는 것이 아니라, 요청을 보내놓고 다른 일을 처리하다가 입력이 들어오면 일을 처리하도록 해야 한다. 이것이 바로 비동기식이다. 비동기식에는 크게 2가지 방법이 있는데, Select와 WIndows의 IOCP이다. Select 방식은 Read와 Write를 할 수 있는지 지속적으로 확인을 하고, 사용할 수 있는 상태라면 직접 처리하는 방식이다. IOCP 방식은 이런 작업을 OS에게 요청하고 위임하는 방식이다. 요청을 보낼 때 OS에게 요청을 하고, 해당 요청에 대한 응답이 오면 OS가 해당 응답을 받아서 Queue에 쌓아놓는다. 작업을 진행하기 위해서 Queue를 확인하고, 응답이 왔다면 바로 처리할 수 있다. Select 방식은 구현하기 쉽다는 장점이 있고, IOCP 방식은 성능면에서 효율적이라는 장점이 있다. 두 방식 모두 많이 사용하지만, 커넥션 수가 많아지면 IOCP 방식을 사용하는 것이 좋다고 한다.

 

 

2. MMORPG 서버 구조

MMORPG 서버 구조에 대한 이야기는 싱글 쓰레드/멀티 쓰레드와 싱글 프로세스/멀티 프로세스에 대한 이야기가 전부인 것 같다. 그런데 이 이야기가 약간 복잡하다. 먼저 MMORPG 서버의 특징은 작업량이 많다는 것이다. 플레이어가 적게는 수십, 많게는 수천 명을 한꺼번에 컨트롤해야하고, NPC라든지, 흔히 바닥이라고 얘기하는 Control Zone 등 아주 많은 정보를 처리해야한다. 서버는 패킷 처리는 물론이고, 매 프레임마다 Actor(플레이어, NPC 등)들을 업데이트시키는 Tick작업을 해줘야하고, 그 외 경매장 등의 시스템도 처리해야한다. 게임을 10프레임으로 돌린다고 하면 초당 10번의 Tick작업을 해줘야하고, 그 말은 100ms의 시간 안에 모든 작업을 끝내야 한다는 것이다. 만약 5000명의 Actor를 처리해야 한다면 하나의 Actor당 처리시간이 0.02ms밖에 주어지지 않는다. 이것은 굉장히 어려운 일이기 때문에 이를 보완하기 위해 여러 방법들을 사용한다. 먼저 멀티 쓰레드를 활용하는 방식이다. 쓰레드 수를 4개로 늘린다면 1개당 100ms라는 시간이 주어지는 것이기 때문에, 4개의 쓰레드를 동시에 돌린다면 400ms의 시간이 주어지는 셈이다. 서버의 부담이 1/4로 줄어든다. 물론 멀티 쓰레드를 돌리면 신경써야 할 것들이 많아서 4개로 늘린다고 성능이 4배로 늘어나진 않겠지만, 잘 구현해놓면 싱글 쓰레드에 비해 부담이 훨씬 줄어들 것이다. 다만 문제가 있다면, 멀티 쓰레드는 방금 언급했다시피 신경써야 할 것들이 상당히 많아서 구조를 잘 짜놔야 한다는 것이다.

 

먼저 싱글 쓰레드는 Work Thread가 1개인 것을 이야기한다. 즉, 네트워크의 I/O를 처리하는 쓰레드는 따로 있고, 들어온 데이터들을 Queue에 계속 저장한다. Work Thread에서는 이것들을 활용해서 Tick 작업을 해주는 것이다. 단점이 있다면 잘 구현된 멀티 쓰레드에 비해서 처리 가능한 Actor 수가 적을 수 있다는 점이지만, 굉장히 Simple 구조를 가지고 있기 때문에 멀티 쓰레드 환경에서 일어나는 많은 문제들이 일어나지 않는다. 그렇기 때문에 싱글 쓰레드라고 해서 무조건 나쁜 것은 아니라고 한다. 이 싱글 쓰레드에 Dedicate Thread를 추가하여 DB나 경매장만 담당하는 Thread를 추가하는 방법이 있다. 이 방법은 싱글 쓰레드의 Simple함을 유지하면서 싱글 쓰레드의 부담을 줄여줄 수 있다는 장점이 있지만, 여전히 처리 가능한 Actor 수의 한계가 있다.

멀티 쓰레드는 Work Thread가 여러 개인 것을 이야기한다. 싱글 쓰레드와 마찬가지로 네트워크의 I/O를 처리하는 쓰레드는 따로 있고, 저장된 Queue에서 데이터를 가져와 Tick 작업을 여러 쓰레드에서 하는 것이다. 멀티 쓰레드에서는 많은 문제들이 발생할 수 있는데, 한 쓰레드에서 공유 자원에 접근할 때, 다른 쓰레드에서 접근하지 못하도록 Locking을 걸어줘야 한다. 가령 hp가 50인 상태에서 동시에 두 적에게 10데미지, 20데미지를 받는다고 치자. 그러면 체력이 20이 되어야겠지만, hp가 50인 상태를 동시에 가져가버려서 한 쓰레드는 10데미지를 입혀 체력을 40으로 갱신하고, 다른 쓰레드는 현재 hp를 50으로 받았기 때문에 20 데미지를 입혀서 체력이 30이 된 것을 거기에 덮어씌운다면 정상적으로 처리되지 않을 것이다. 때문에 다른 쓰레드에서 접근하지 못하도록 Locking을 해줘야한다. 그런데 MMORPG는 Actor간에 Interaction이 굉장히 빈번하게 일어나기 때문에 이 문제를 어떻게 다루느냐가 굉장히 중요한 문제이다. 여기서 구역을 나누는, 구획화를 어떻게 하느냐가 중요하다.

 

이를 쉽게 해결할 수 있는 방법이 멀티 프로세스이다. 싱글 쓰레드, 멀티 쓰레드는 각각 하나의 프로세스 안에 존재하는 쓰레드의 수를 이야기한다. 즉, 프로세스를 여러 개 돌리고 각각의 프로세스를 싱글 쓰레드로 돌리면 멀티 쓰레드와 같이 성능을 향상시킬 수 있고, 각각의 프로세스는 자원을 공유하지 않기 때문에 멀티 쓰레드에서 일어나는 문제들도 발생하지 않게 된다. 다만, 이 경우 문제가 있는데, 각 프로세스에서 처리하는 정보들에 바로 접근할 수 없기 때문에, A프로세스에 존재하는 유저는 B프로세스에 존재하는 유저를 공격할 수 없다는 문제가 있다. 이것은 대륙을 나눠서 각각의 대륙을 하나의 프로세스가 처리하게 하고, 유저가 넘어갈 때 로딩을 적용해서 다른 프로세스로 넘겨주는 방식을 사용하여 해결할 수 있다. 그게 아니라 만약 하나의 대륙에서 구획을 나누는 방식을 사용한다면 구획과 구획이 만나는 가장자리에 서있는 유저들을 처리하기 위해서 각종 눈속임을 써야한다는 문제가 있다. 때문에 로딩 등 끊김이 없는 Seamless 월드를 만들 계획이 있다면 멀티 쓰레드를 활용하는 것이 좋다고 한다.

 

싱글 프로세스에서는 기기 성능을 향상시키기 위해서는 좋은 기기를 한 대 구입해야한다. 이를 Scale Up이라고 한다. 그러나 기기의 성능이 점점 좋아질수록 성능이 조금만 좋아져도 가격이 기하급수적으로 올라가고, 머신의 성능과 처리 성능이 비례하여 증가하지 않기 때문에 이 방법에는 한계가 있다. 아마 처리하는데 고정적으로 들어가는 비용들이 있다보니 머신 성능과 처리 성능이 비례하지 않는 것 같다. 보통 머신 성능이 좋아질수록 처리 성능의 증가폭은 줄어든다고 한다. 반면에 멀티 프로세스는 적당한 성능과 가격의 기기를 여러 대 들여놓는 Scale Out 방식을 사용하면 되는 것이기 때문에, 싱글 프로세스에 비해 비용 부담이 상당히 적다. 머신은 싸다 라는 말이 있다는데, 비싼 돈을 주고 코딩을 잘 하는 프로그래머를 고용하는 것 보다, 머신의 수를 늘리고 싼 프로그래머를 기용하는 것이 더 효율적이라는 얘기가 있다고도 한다. 멀티 프로세스를 구현하기 위해서 실제로 머신을 구입할 필요는 없고, AWS와 같이 클라우드를 통해 비용을 지불하고 머신의 성능을 가져다가 쓰는 방법이 있다고 한다. 또한 가용성이라는 것이 있는데, 가령 싱글 프로세스로 서버가 돌아갈 때 이 프로세스가 죽으면 해당 서버는 완전히 다운되어버린다. 멀티 프로세스로 서버를 돌린다면 하나의 프로세스가 죽어도 나머지 프로세스로 서버가 돌아갈 수 있다는 것이다. 구현 내용에 따라 속도가 느려지거나 일부 지역이 봉쇄될 수는 있겠지만, 전체 서버의 다운은 막을 수 있따는 것이다. 이것이 가용성이 높다고 한다. 즉 싱글 프로세스는 가용성도 낮고 비용도 높지만, 멀티 프로세스는 가용성도 좋고 비용도 적다. 때문에 Scale Up 방식에서 Scale Out 방식으로 변화하고 있다고 한다. 물론 싱글 프로세스의 장점도 있는데, 구조가 단순하고, 위에서 언급한 Seamless 월드, 즉 하나의 통합된 월드를 만들 수 있다. 더욱이, 버전업을 할 때 각 프로세스에 배포를 해야하는 멀티 프로세스와는 달리 프로세스 하나만 버전업을 하면 되고, 수십~수백 개의 프로세스를 사람이 직접 관리하기는 힘든 멀티 프로세스와 달리 싱글 프로세스는 사람이 관리할 수 있을 정도의 규모가 될 수 있다는 것이다. 멀티 프로세스는 이렇게 많은 프로세스를 가진 서버가 여러 개 있을 수 있기 때문에 사람이 직접 관리하기는 힘들고, 이를 자동으로 관리해줘야한다고 한다. 이를 위한 툴들이 Jenkins, Ansible, Chef 등 아주 많다고 한다. 이런 것들을 전문으로 하는 DevOps라는 직업군까지 생겼다고 할 정도이다. 하지만 이렇게까지 해서라도 가용성과 비용적인 측면 때문에 멀티 프로세스를 많이 사용한다고 하고, 마찬가지로 이런 문제 때문에 싱글 프로세스도 완전히 안 좋은 방법은 아니라는 것이다. 멀티 프로세스를 사용하는 방법은 아까 말했듯이 구역을 나누는 Zone 방법이 있고, 하나의 구역에 채널을 여러 개 두는 Channel 방법이 있다고 한다. 또, 두 가지 방법을 모두 사용할 수 있다. 이 경우 프로세스들이 너무 많아져서 이 프로세스들을 관리하는 서버가 따로 필요할 수도 있고, 배포나 빌드, 이슈를 모니터링하여 판단하고 복구하는 작업 등 많은 문제들이 발생한다고 한다. 이런 것들을 잘 컨트롤해야 좋은 멀티 프로세스 서버를 만들 수 있다고 한다.

추가로, 멀티 프로세스에서는 각각의 프로세스에 싱글 쓰레드를 쓸 수도 있고, 멀티 쓰레드를 쓸 수도 있지만, 싱글 프로세스 방식에서는 아무래도 성능 상의 문제 때문에 멀티 쓰레드를 사용할 수 밖에 없다고 한다.

 

이제 마지막으로 멀티 쓰레드에 대한 얘기이다. 멀티 쓰레드를 사용할 때, 코어 수가 늘어날수록 더 많은 쓰레드들이 자원을 점유하려고 하기 때문에 성능 증가 폭이 상당히 줄어들고, 코딩 하기에 따라 오히려 성능이 줄어들 수도 있다. 떄문에 싱글 쓰레드도 현명한 선택일 수 있다. 그럼에도 불구하고 멀티 쓰레드를 사용하려면 이런 문제로 인해 공유 자원에 대한 접근을 잘 관리할 필요가 있다. 만약 무식하게 구현하여 자원에 마구 접근하고, 접근할 때마다 마구 Lock을 건다면 성능도 떨어지고, 무분별한 접근과 Lock으로 인해 대기 시간도 늘어나고, Deadlock이 발생할 가능성도 높아진다. 또, Race Condition이라는, 어떤 쓰레드가 먼저 접근하는지에 따라 다른 문제가 발생하여 Timing 문제가 발생할 수도 있다. 때문에 공유 자원을 어떻게 분배할 것이냐에 대한 기준을 세워야 한다. 첫 번째로는 Actor를 기준으로 나눌 수 있다. AKKA 방식이라고 하는데, Actor를 기준으로 쓰레드를 나눈다. 때문에 각각의 Actor가 할 일은 해당 Actor를 가진 쓰레드가 처리하면 되는 것이다. 상호작용을 할 때는, 다른 Actor에게 메세지를 보내고, 해당 Actor가 그에 대해 응답을 한다. 즉, 직접적으로 자원에 접근하는 것이 아니라 공유 자원에 대한 여러 문제가 발생하지 않는다. 하지만 메세지를 보내고 그에 대한 응답이 와야 처리를 할 수 있기 때문에 비동기식이라는 문제점이 있다. 또한, A가 B를 공격하기 위해 데이터를 받아오고 처리하는 도중에 C가 B를 공격하기 위해 데이터를 받아온다면, A의 공격이 B에게 적중하기 전의 데이터가 들어갈 것이기 때문에 Timing 관련 이슈가 생긴다는 것이다. 마찬가지로 Lock을 걸면 이런 문제가 해결되겠지만, Lock을 걸면 성능이 저하되고, Deadlock이 발생할 수도 있고 많은 문제들이 발생한다. 메세지를 사용한다면 복잡도가 올라가고 비동기식이 된다는 문제점이 있다. 결국 모든 것은 일장일단인 것이다. Trade Off라고도 하는데, 어떤 것의 장점을 취하려고 한다면 그에 대한 단점도 따라오는 것이고, 어떤 것의 단점을 없애기 위해 장점을 포기해야할 수도 있는 것이다. 안정성을 올리면 성능이 떨어지는 것이 대표 예라고 할 수 있겠다. ECS 방식도 있는데, Entity, Component, System 방식이라고 한다. Entity는 Conponent를 갖고 있는 객체이고, Component는 데이터만을 가지고 있다. 그리고 System이 각 Entity가 가진 Component의 데이터를 사용해서 기능을 처리하는 식으로 나누는 것이다. 이 경우 객체에 어떤 Component를 붙이느냐에 따라서 플레이어, NPC, 몬스터, 자유자재로 만들 수 있기 떄문에 확장성이 좋아지고, 데이터의 Locality가 좋아진다고 한다. 데이터의 Locality란, 같은 데이터가 얼마나 같은 공간에 밀집해있느냐이다. 가령 Render System이 화면을 그릴 때, 화면을 그리는 것과 관련된 Component들을 전부 반복문을 돌려 한꺼번에 처리하면 같은 공간에 데이터들이 모여있게 될 것이다. 캐시에 데이터를 올릴 때, 일정 영역의 데이터를 한꺼번에 올리기 때문에 이렇게 데이터의 Locality가 좋아지면 성능이 향상된다는 것이다. 특히 게임에서는 화면을 그리는 것이 성능의 90%를 차지할 정도로 많은 성능을 요구하기 때문에, 이런 성능의 향상이 크게 작용할 수 있다. 다만, 문제가 있다면 하나의 시스템이 하나의 Component만 가지고 있는 것이 아니라, Component를 공유할 수 있기 때문에 마찬가지로 점유의 문제가 생길 수도 있고, 다른 시스템에게 물어봐야 하는 상황이 올 수 있기 때문에 AKKA 방식과 마찬가지로 메세지로 처리해야할 수도 있다. 또한 ECS 방식이 만들어진지 오래 되지 않아서 아직까지 많이 사용되지 않고 정보도 그렇게 많지는 않은 것 같다.

 

 

 

드디어 정리가 끝난 것 같다. 이로써 게임 네트워킹의 이해를 전부 시청하고 정리하게 되었다. 내 기억에 의존하거나, 기억이 안 나는 부분은 다시 듣기도 하고, 내 방식대로 풀어서 설명했기 때문에 잘못된 부분이 있을 수 있다. 그래도 들으면서 이렇게 글로 나름대로 풀어서 쓰는 것이 이해에 상당히 도움이 된 것 같다. 코드를 짜거나 본 것도 아니고, 아마 많은 부분 중에서도 빙산의 일각, 하물며 겉핥기 식일 테지만 이런 기초적인 개념이라도 있는 것과 이것조차 없는 것은 굉장히 큰 차이라고 생각하기 때문에 나로서는 굉장히 만족스러운 시간이었다. 또 많은 키워드를 얻은 것 같다. 당장에는 필요없을지 모르겠지만, 어떤 형태로든 향후에 도움이 될 것이라고 생각한다. 아무튼 이로써 게임 네트워킹의 이해 정리를 마무리하겠다.

+ Recent posts