Windows에서 Linux 머신을 구성하는 방법들


게임을 개발하다 보면 여러 가지 서버 환경을 준비해야 한다. 개발자가 사용할 개인용 개발 서버부터 팀원들이나 클라이언트 개발자가 꾸준히 접근할 팀 개발 서버, QA를 위한 테스트 서버, 상용화 환경을 준비하기 위한 것까지 최소한 서너가지의 서버 환경은 필수적이다. 또한 의외로 꼭 있으면 좋은 것이 기획자들의 데이터 테스트를 위한 기획자별 테스트 서버인데, 이 경우 전문 개발자가 아닌 사람들을 위한 배포용 서버 환경을 어떻게 구축 할지가 매우 중요해진다.


업무 환경에서 가장 많이 사용하는 운영체제는 대부분의 경우 Windows이다. 하지만 게임은 물론 다수의 프로젝트에서 사용하는 서버는 리눅스에서 동작하는 경우도 많다. 이 경우 전문 개발자가 아닌, 혹은 리눅스 서버 사용 경험이 거의 없는 사람과의 협업이 필요할 때 어떻게 이들을 위해 환경을 설정하고 공유하며 배포를 준비할 수 있을까. 이번에는 이에 대해서 이야기를 해보려고 한다.

 1. Hyper-V

1
(출처: https://www.microsoft.com/)


Hyper-V는 Windows에서 기본으로 지원하는 가상 컴퓨터 관리 기능이다. 다른 운영체제를 사용하기 위하여 추가로 프로그램을 설치하거나 할 필요가 없다는 것이 장점이다.


이후에 설명할 다른 가상 환경도 마찬가지 이지만, Hyper-V 역시 지정된 가상 컴퓨터 이미지를 공유할 수 있다. 이를 통하여 다른 사람들에게 쉽게 서버를 배포할 수 있다.


Hyper-V는 이와 같이 사용이 간단하고 배포가 손쉽지만, 아쉬운 부분이 있다. 대표적으로 NAT설정이 번거롭다.[1] 기본 기능을 통하여 바로 만들 수 있는 외부 스위치를 활용하는 경우, 새로운 하드웨어 주소가 가상 컴퓨터에 설정된다. 그러므로 하드웨어 주소를 통해 접근을 제한하는 네트워크 정책을 사용하는 회사의 경우, 배포가 쉽다는 장점과는 반대로 네트워크 시스템 담당자를 매우 귀찮게 만들게 된다. 즉, 전문 개발자가 아닌 사람과 협업하기 위한 서버 환경 설정 용으로 Hyper-V는 매우 편리하지만, 해당 가상 서버 환경에서 외부 네트워크 접속을 허용하기 위해서는 추가 작업이 필요하다.


하지만 서버가 외부 접속을 지원할 필요가 없다면, Hyper-V로 만든 가상 컴퓨터는 매우 쉽게 공유할 수 있는 리눅스 머신이다. 특히 기획자 개별 데이터 테스트용 서버 머신이라면, 가장 편리한 방법이 아닐까 생각한다.

 
2. Docker

2
(출처: https://www.docker.com/)


Docker는 리눅스 위에 얇은 추상화 레이어를 더해서 프로그램을 구동시킬 수 있는 컨테이너를 만들고, 그 위에서 리눅스 프로그램을 구동시킬 수 있도록 해주는 소프트웨어이다. 다시 설명하자면, Docker는 사용자가 직접 가상 운영체제를 설치하거나 유지보수하지 않더라도, 원하는 머신에 Docker만 깔면 동일한 환경에서 실행할 수 있도록 배포, 관리가 가능하게 만들어준다. 그리고 이 기능은 VM을 이용하는 것이 아니라, 각각의 운영체제에 직접 연동되어 동작한다. 현재 Docker가 지원하는 운영체제는 Mac, Windows, Linux이다.


이 글의 첫 부분에서 이야기한 다양한 서버 환경을 준비하는데 있어서, Docker는 Hyper-V보다 좀 더 나은 관리 방식을 제공한다. 실제로 Docker는 가상 컴퓨터의 관리보다는 소프트웨어 배포용 소프트웨어이기 때문이다. 출시를 위한 환경은 보통 단일한 서버보다는 여러 가지 종류의 서버가 상호 작용하는 형태를 취하게 되는데, Docker를 이용하면 각각의 기능에 따른 가상 환경을 운영체제부터 프로그램까지 전체를 관리하지 않아도 되기 때문에 상대적으로 편하게 유지보수가 가능해진다. 엔터프라이즈 환경에서 검증되었다는 것 역시 훌륭한 점이다.


물론 Docker도 진입장벽이라는 단점이 존재한다. 개발자들에게는 의아한 이야기일 수도 있지만 개발자가 아닌 사람들도 사용하기에는 GUI가 빈약하며 교육을 위한 시간과 비용이 필요하다.

 

 3. 그 외 가상화 소프트웨어

Hyper-V가 나오기 전부터 이미 여러 회사에서 VirtualBox나 VMWare 같은 가상화 관련 소프트웨어를 만들어왔다. 이들은 역사가 깊은 만큼 다양한 인프라스트럭처 관련 툴을 지원하기도 하고, 사용자의 규모에 따라 분리된 프로그램을 제공하기도 한다. 또한 Hyper-V의 약점이었던 NAT지원 등도 손쉽게 설정할 수 있다. 배포 역시 마찬가지로 이미지를 활용하면 어렵지 않다.


하지만 먼저 설치해야 할 프로그램이 존재한다는 것은 개발자가 아닌 사람들이 사용하기에 단점이며, 개발자들이 사용하는 상황에서도 특별한 경우가 아니라면 이러한 가상화를 사용하는 경우는 많지 않다.


편의성이 좀 더 뛰어난 경우가 많아 관련 기능을 사용해야 하는 경우라면 고려해 볼만 하다.

 

 4. 정리하며

서버 개발은 물론이요, 하다 보니 어느새 시스템 엔지니어링까지 해야 하는 수 많은 개발자 분들을 위해 빈약하나마 가상화 프로그램을 소개하는 내용을 정리해 보았다. 다양한 환경을 준비하고 공유하는 방법에 대한 의의도 있겠지만, 개발된 코드와 컨텐츠를 보관하는 것 만큼 이나 개발 환경과 구동 환경을 백업해두는 것도 중요한 만큼, 앞으로 오래오래 개발할 수 있는데 조금이나마 도움이 되었으면 하는 마음에 미약한 글을 여기서 정리한다.

아이펀팩토리   박근환 아이펀 엔진 테크니컬 디렉터


[1]: Hyper-V NAT 네트워크 설정, https://msdn.microsoft.com/ko-kr/virtualization/hyperv_on_windows/user_guide/setup_nat_network

네트워크 관련 용어들의 개념적 정리

영어권이 아닌 우리 한국 사람들은 영어로 표현된 단어의 미묘한 느낌을 캐치하기 보다는 그걸 전공 용어로 취급해서 그대로 외워버리는 경우가 많다. 하지만, 단어의 개념을 명확히 이해하면 그 개념이 다른 상황에서 사용되더라도 쉽게 이해할 수 있다는 장점이 있다. 아래 내용은 다들 알고 있는 단어들을 조금 다른 각도에서 설명을 시작하여 최근 많이 회자되는 Software Defined Networking (SDN) 과 관련된 기본 용어들까지에 대한 내용이다.

네트워크 : 일반화된 개념으로 네트워크는 점 들의 연결 관계를 선으로 표시한 것을 의미한다. 여기서 점과 선이 무엇을 의미하는지는 네트워크가 표현하려는 것에 따라 달라지는데, 점을 개인으로, 선을 개인간 관계로 생각하면 소셜 네트워크가 되는 것이고, 점을 옥천 허브 같은 물류 기지, 선을 도로라고 생각하면 물류 네트워크가 된다. 그리고 점을 컴퓨터로, 선을 회선으로 생각한다면 우리가 익숙한 컴퓨터 네트워크가 되는 식이다. 네트워크는 굳이 복잡한 모습일 필요는 없다. 단지 점 두 개가 이어진 것도 네트워크고 복잡하게 얽히고 설킨 것도 네트워크다.
8%ed%9a%8c%ec%bb%ac%eb%9f%bc-%ec%9d%b4%eb%af%b8%ec%a7%801

그림1 : 점들의 연결 관계가 네트워크이다. 그리고 단 2개의 점을 연결하더라도 마찬가지다. (아이콘 출처: WPZOOM, iconfinder.com, Creative Commons License)

스위치: 네트워크 상에서 한 점이 다른 둘 이상의 점에 연결되는 일은 흔하게 발생한다. 이 때 그 점은 들어온 입력을 여러 방향 (즉, ‘선’)으로 보낼 수 있고, 이처럼 경로를 결정한다는 의미에서 스위치라고 이야기한다. 마치 교차로에서 길을 선택하는 것과 같다고 생각하면 된다. 우리가 컴퓨터 네트워크에서 일반적으로 말하는 스위치는 OSI 모델상 제2계층인 “데이터링크 계층”에 해당하는 Ethernet 상의 데이터들의 경로를 결정한다고 해서 Layer 2 (또는 L2) 스위치라고 부른다. “스위치와 허브의 차이는..” 하는 식의 지식을 외우고 있는 사람들도 많을 것이다. 그것도 나쁘지 않지만, 경로의 결정을 하는 것이 스위치라고 이해하면 “ATM 스위치”, “패킷 스위칭” 이런 표현들이 무엇을 뜻하는 것인지 보다 쉽게 감이 올 것이다.

8%ed%9a%8c%ec%bb%ac%eb%9f%bc-%ec%9d%b4%eb%af%b8%ec%a7%802
그림2 : 어떤 점들은 여러 점들과 연결될 수도 있다. 그렇게 되면 그 점은 여러 경로를 선택할 수 있게 된다. 이런 점을 스위치라고 한다. (아이콘 출처: WPZOOM, iconfinder.com, Creative Commons License)

브릿지 : 브릿지는 ‘다리’라고 번역된다. 하지만, 브릿지를 ‘다리’가 아닌 ‘이어주는 것’ 이라고 생각하는 것이 더 편리하다. 그렇게 이해하면 치과의 ‘치아 브릿지’ 라는 시술이 더 이상 암호 같은 의미가 아니라, 치아가 중간에 빠져서 떨어져 있던 두 치아사이에 보철물을 끼워넣는 것으로 이해될 수 있기 때문이다. 그리고 이렇게 이해하면, 앞서 설명한대로 여러 점을 ‘이어주는’ 스위치는 브릿지라고 쉽게 이해할 수 있다. 위의 그림에서 보면 왼쪽의 (서브)네트워크와 오른쪽의 (서브)네트워크 를 가운데 스위치가 이어주고 있다는 것을 알 수 있다. 그리고 “IP 네트워크를 브릿징한다” 라는 의미도 쉽게 이해될 것이다. 그렇다. 두 개의 IP 네트워크를 어떤 장비로 이어주는 것이다.

8%ed%9a%8c%ec%bb%ac%eb%9f%bc-%ec%9d%b4%eb%af%b8%ec%a7%803
그림3: 위의 스위치는 양쪽의 네트워크를 이어주는 역할을 한다. 이어주는 역할을 하는 것을 브릿지라고 한다. (아이콘 출처: WPZOOM, iconfinder.com, Creative Commons License)

그럼 XenServer 와 같은 가상 서버에서 사용하는 네트워크 브릿지라는 표현은 어떤 의미일까? 이는 가상 서버안의 네트워크 카드와 물리 서버의 네트워크 카드를 이어주는 역할을 하는 매개체로 생각할 수 있다. VMware 의 경우 “가상 스위치” 라는 이름으로 같은 개념이 존재한다. 이와 관련해서는 아래에서 다시 설명하겠다.
8%ed%9a%8c%ec%bb%ac%eb%9f%bc-%ec%9d%b4%eb%af%b8%ec%a7%804

그림4 : 가상 솔루션들은 가상서버의 네트워크 카드와 물리 서버의 네트워크 카드를 연결하기 위해서 브릿지를 사용한다. (아이콘 출처: WPZOOM, iconfinder.com, Creative Commons License)

라우팅과 포워딩: 우리는 크게 신경을 쓰고 있지 않지만, router 나 switch 라고 말하는 장비들은 엄밀히 말해 크게 두 가지 일을 한다. 특정 목적지로 가기 위해서는 어떤 경로들이 있는지에 대한 정보들을 수집하는 것과, 그 정보들 중에 실제로 선택된 경로를 기반해서 패킷을 전달하는 일. 전자를 경로(route) 를 알아내는 일이라고 해서 라우팅(routing) 이라고 하고 후자를 실제로 패킷을 전달(forward) 하는 일이라고 해서 포워딩(forwarding) 이라고 한다. 우리가 보통 패킷을 전달하는 것을 라우팅이라고 부르는 경우가 많은데 이는 틀린 표현이다. 라우팅은 IP 패킷을 전달하는 것이라고 이해하고 있다면 이 역시 잘못된 것이다. 물론 많은 경우에 라우팅이 IP 패킷 전달을 의미하는 것으로 쓰이지만, 라우팅과 포워딩이라는 단어는 IP 와 같은 Layer 3 에 국한되는 것이 아닌 일반적인 개념으로 이해하는 것이 더 바람직하다.

Control plane과 data plane : Control plane 에 해당하는 라우팅은 경로 정보를 수집하는 것이라고 했다. 그리고 이들이 경로를 수집하는 방식은 자신이 알고 있는 정보를 이웃과 교환하는 것이다. 교환하는 정보가 선에 대한 정보 (Link state) 일 수도 있고, 자신이 알고 있는 경로 (Distance Vector) 에 대한 것일 수도 있다. 전자의 예로는 OSPF 같은 것이 있고 후자의 예로는 RIP 같은 것을 들 수 있다.

어쨌거나 자신이 알고 있는 정보는 그렇게 자주 바뀌지 않는다. 그때문에 정보의 교환 역시 빈번하게 발생하지 않는다. 이때문에 경로를 알아내는 라우팅 자체는 고성능일 필요는 없다. 그래서 대부분의 경우 control-plane 인 라우팅은 소프트웨어로 구현한다. 대신 라우팅에서는 각 지점들이 주고 받은 정보를 통해서 각 지점들이 최종적으로 똑같은 정보에 도달하는 것이 매우 중요하다. 그렇지 않으면 중간에 어떤 지점에서는 잘못된 경로를 알게 되기 때문이다. 즉, 라우팅은 각 지점들이 서로 주고 받은 경로 정보를 이용해서 독립적으로 연산을 수행해서 전체 경로들에 대해서 동일한 정보를 가지게 되어야 하는 것이다. 동일한 정보를 가지게 되는 것을 수렴한다고 말한다. 그리고 독립적인 연산을 하기 때문에 라우팅 알고리즘들은 분산 알고리즘이다. 라우팅 알고리즘들은 어떻게 하면 더 빨리 수렴할 수 있을까 하는 고민을 한다.

그러나 data plane 인 패킷 포워딩의 경우는 이야기가 달라진다. 얼마나 빨리 패킷을 전달할 수 있는지가 관건이다. 이를 소프트웨어로 구현을 하면, 매 패킷에 대해서 처리를 해줘야 되기 때문에 CPU 가 병목이 되게 된다. 그래서 이를 ASIC 형태의 하드웨어로 만들어버리게 되었다. 특히나 스위칭 패브릭이라고 하는 핵심 부분이 더욱 그렇다. 그리고 그 분야에서 가장 두각을 보인 것이 시스코다. (시스코는 샌프란시스코 도시 이름에서 시스코라는 이름을 따왔고, 회사 로고 역시 샌프란시스코의 금문교를 모델로 하고 있다.)

그러나 이런 하드웨어 접근 방식은 주어진 네트워크 환경에서 효율적인 성능을 낸다는 장점은 있었지만, 동시에 새로운 것들을 시도해볼 수 없다는 심각한 문제가 발생했다. 일례로, IPv4 에서 IPv6 로 넘어가기 위해서는 장비가 IPv4 스위칭 뿐만 아니라 IPv6 스위칭까지도 지원해야 되는데 하드웨어로 이미 박혀있는 것을 바꿀 수 있는 방법이 없는 것이다. 그리고 비록 control-plane 인 라우팅 부분을 하드웨어로 구현을 할 필요가 없지만 그 소프트웨어가 같은 장비 안에 있는 flash 메모리 등에 박혀있는 한 그걸 마음대로 바꾸기도 어려웠다. 이 때문에 네트워크는 기존 프로토콜들 관련해서는 10Mbps, 100Mbps, 1Gbps, 10Gbps 등 높은 고도화를 이루었지만, 새로운 프로토콜 관련해서는 상당 기간 정체를 앓게 된다.

2000년대 중후반에는 Internet Architecture 연구가 한창 붐을 이루었다. “우리가 새로 인터넷을 설계한다면 어떤 내용들이 포함되어야 할까?” 라는 주제의 연구들이었는데, 이는 기존의 인터넷이 설계될 때와 오늘날 가장 중요하게 생각하는 요소들이 많이 바뀌었기 때문에 생긴 자연스러운 움직임이었다. 하지만 대부분의 노력은 탁상 공론으로 끝나고 말았다. 바로 현재의 하드웨어 중심의 네트워크 구성 때문이었다.

이런 하드웨어 중심의 네트워크는 새로운 것을 시도하지 않는 한 크게 문제가 되지 않았다. 그렇지만 예전에 만들어진 인터넷이라는 옷은 지금에는 맞지 않은 것들이 많아서 새로운 기능들이 종종 필요해졌다. 예를 들어 데이터 센터 안에는 많은 수의 스위치가 들어가는데, 네트워크 구성을 기존의 단순 tree 형태로 하게 되면 최상위 스위치가 병목이 되어 링크가 아무리 빨라도 그만큼을 못 쓰게 된다.

그리고 데이터 센터 안에는 수 많은 스위치가 들어가는데 그 중 하나가 죽으면 다른 스위치들이 경로 정보를 주고 받고 수렴할 때까지 기다려야 된다. 그리고 라우팅 정보가 수렴하지 않으면 포워딩 역시 제대로 이루어질 수 없게 된다. 데이터센터 안의 네트워크 구성은 너무나 뻔해서 굳이 분산 알고리즘을 돌릴 필요가 없는데도 말이다.

이런 여러 이유 때문에 하드웨어 중심의 네트워크를 벗어나기 위한 노력이 시도되었다. 이름하여 Software Defined Networking 이다.

Software Defined Networking (SDN) : SDN 의 기본 아이디어는 “패킷 포워딩에 특화된 프로그래밍 가능한 더미 하드웨어” + “라우팅과 더미 하드웨어의 포워딩 테이블을 제어하는 현명한 외부 소프트웨어 (컨트롤러)” 로 정리할 수 있다. 이는 기존의 완전 소프트웨어 기반 솔루션과 완전 하드웨어 기반 솔루션의 하이브리드 형태로 이해할 수 있다. 소프트웨어 기반 솔루션은 포워딩 성능이 나오기 힘들었다. 하드웨어 기반 솔루션은 프로그래밍이 안됐다. 그래서 그 둘을 섞어서 포워딩 규칙을 프로그래밍할 수 있는 포워딩에 특화된 하드웨어와 이 포워딩 규칙을 제어하는 소프트웨어라는 방식을 채택한 것이다.

그리고 SDN 는 기존에 장비들이 분산 알고리즘을 돌리던 것에서 벗어나 외부 소프트웨어가 모든 하드웨어를 중앙 집중식으로 관리하게 된다. 그 때문에 오랜 시간이 걸릴 수 있는 분산 알고리즘의 수렴 과정을 피할 수 있게 되었다. 외부 소프트웨어 (콘트롤러) 가 바로 어떻게 하면 되는지 알려주면 되기 때문이다. 그리고 외부 소프트웨어가 네트워크를 정의한다(제어한다) 는 뜻에서 Software-Defined 라는 이름을 쓰게 된 것이다.

8%ed%9a%8c%ec%bb%ac%eb%9f%bc-%ec%9d%b4%eb%af%b8%ec%a7%805

그림5: SDN 는 포워딩에 특화된 하드웨어들을 외부의 소프트웨어 중앙 집중방식으로 통제하는 형태이다. (아이콘 출처: WPZOOM, iconfinder.com, Creative Commons License)

잠깐. 그런데 포워딩에 특화된 하드웨어이면서 프로그래밍 가능하다고 했는데 어떻게 이런 게 가능할까? 앞서 하드웨어로 포워딩을 구현하면 바꾸는 것이 안된다고 했는데 말이다. 사실 여기서 말하는 하드웨어는 기존 ASIC 기반의 하드웨어와는 다르다. SDN 에서의 하드웨어는 입력으로 들어오는 패킷의 패턴 매칭 하드웨어라고 이해하는 것이 더 좋을 것이다. “이 패턴의 패킷은 여기로, 저 패턴의 패킷은 저기로” 이런 방식이다. 그 때문에 하드웨어는 특정 프로토콜에 종속되지도 않고, 외부의 소프트웨어는 패킷 패턴에 따른 경로만 프로그래밍하면 되는 것이다. 신박하지 않은가?

OpenFlow : 이처럼 SDN 에서 가정하는 하드웨어는 기존의 ASIC 기반 하드웨어와는 확연하게 달랐고, 이렇게 패턴 매칭 형태로 프로그래밍 가능한 스위치에 대한 Spec 이 OpenFlow 라는 이름으로 이루어졌다. 그리고 HP, DELL, IBM 등 메이저 스위치 제조업체들이 이 규격에 따라 OpenFlow 스위치들을 만들기 시작했다. 보다 자세한 내용은 여기를 참고하기 바란다. http://www.openflow.org/

가상화 (Virtualization) : 나는 virtualization 을 가상화로 번역하는 것이 상당히 많은 오해를 불러 일으키고 있다고 생각한다. 가상화는 왠지 가짜의 느낌을 주기 때문이다. 실은 Virtualization 이 가짜라기 보다 “나만이 쓰는 것처럼” 라는 의미에 가까운데 말이다. Server virtualization 은 나만이 쓰는 것 같은 서버, Storage virtualization 은 나만이 쓰는 것 같은 스토리지 이런 식이다. 그러니 Network virtualization 은 나만이 쓰는 것 같은 네트워크라는 뜻이 된다.

가상화 기술이 “나만이 쓰는 것처럼” 하는 기술이기 때문에, 필연적으로 자원의 공정한 분배와 분리를 지원해줘야 된다. 예를 들어 서버 가상화 기술은 각 가상 서버들이 공정하게 자원을 분배 받고, 다른 가상 서버가 자원을 쓰는 것 때문에 또 다른 가상 서버가 영향 받지 않게끔 하는 것이 핵심이다. 그리고 스토리지 가상화는 각 가상 스토리지가 공정하게 디스크 IO 를 분배 받을 수 있게끔 하고 다른 가상 스토리지 때문에 내 가상 스토리지의 IO 성능이 영향 받지 않게 해줘야 된다. 네트워크 가상화도 마찬가지다. 내 가상 네트워크가 일정 수준의 밴드위스를 보장 받아야 되고 이는 다른 가상 네트워크때문에 영향을 받아서는 안 된다.

이런 가상화 기술은 물리 자원 위에 소프트웨어적으로 만들어 주는 것이다 보니, 자원의 할당량을 변경하거나 다른 곳으로 이동 시키는 것이 자유롭다는 장점이 있다. 예를 들어 가상 서버에 CPU 를 추가한다거나 다른 물리 기계로 이동하는 것이 가능한 것처럼 말이다. 그리고 가상 네트워크에 밴드위스를 추가한다거나 다른 물리 네트워크로까지 가상 네트워크를 확장하는 것 등이 가능한 것처럼 말이다.

가상 스위치 : 이처럼 네트워크 자원의 배정과 분리, 그리고 제어가 가능하게 하려면 네트워크 가상화를 위해서 뭔가가 더 필요한 것은 당연하다. 바로 가상 스위치라는 개념이다. 가상 스위치는 보통 가상화 프로그램 안에 내장된다.

가상 스위치가 없는 경우 어떤 일이 벌어질까? 아래 그림을 보기 바란다.

8%ed%9a%8c%ec%bb%ac%eb%9f%bc-%ec%9d%b4%eb%af%b8%ec%a7%806

그림6: 만일 가상 스위치가 존재하지 않는다면 같은 물리 머신안 가상 머신들이라도 통신을 위해서는 외부 스위치까지 갔다가 돌아와야 된다. (아이콘 출처: WPZOOM, iconfinder.com, Creative Commons License)


가상 서버는 물리 서버와 마찬가지로 각각의 IP 를 받을 것이다. 그런데 같은 기계 안에 있더라도 가상 서버들끼리 통신을 하기 위해서는 물리 기계를 벗어나 외부 스위치 까지 가야되는 일이 발생한다. 이는 엄청나게 비효율적이다. 그럼 가상 스위치가 있는 경우는 어떨까? 아래를 보면 알겠지만 훨씬 더 효율성이 높아진다.

8%ed%9a%8c%ec%bb%ac%eb%9f%bc-%ec%9d%b4%eb%af%b8%ec%a7%807그림 7 : 가상 스위치가 존재하는 경우 같은 물리 장비 안의 가상 서버들간의 통신이 훨씬 효율적으로 된다. (아이콘 출처: WPZOOM, iconfinder.com, Creative Commons License)

그 외에도 같은 물리 머신의 네트워크 카드로 통신하더라도, 가상 스위치를 분리함으로써 가상 서버들이 다른 네트워크에 존재하게 하는 것도 가능하고, 다른 기계에서 도는 가상 서버들이 같은 네트워크에 있도록 하는 것도 가능하다.

8%ed%9a%8c%ec%bb%ac%eb%9f%bc-%ec%9d%b4%eb%af%b8%ec%a7%808

그림 8 : 가상 스위치를 분리해서 가상 서버들이 다른 네트워크에 존재하게 만들 수 있다. 맨 왼쪽의 가상 서버는 다른 두 가상 서버와 다른 네트워크에 포함된다. (아이콘 출처: WPZOOM, iconfinder.com, Creative Commons License)

8%ed%9a%8c%ec%bb%ac%eb%9f%bc-%ec%9d%b4%eb%af%b8%ec%a7%809
그림 9 : 가상 스위치는 서로 다른 물리 서버에 있는 가상 서버들간에 같은 네트워크 안에 있게 만드는 것도 가능하다. 이때 물리서버간 통신은 가상 스위치간 터널링으로 이루어진다. (아이콘 출처: WPZOOM, iconfinder.com, Creative Commons License)

이처럼 가상 스위치는 네트워크 가상화를 구현하기 위한 필수적인 기술이라고 할 수 있다. 그 때문에 최근 가상화 솔루션들은 모두 가상 스위치 기능들을 포함하고 있다. 그 중에서 Open vSwitch 는 오픈 소스 기반의 가상 스위치 구현이며, XenServer 의 가상 브릿지 구현은 Open vSwitch 을 활용하고 있다. (OpenFlow 와 Open vSwitch 는 이름도 유사하지만, 실제로도 같은 사람들이 만들었다)

이번 컬럼에서는 네트워크 관련 용어들을 정리해보았다. 서두에 이야기한 것처럼 비영어권인 우리는 많은 용어를 그냥 전공 용어겠거니 하며 외우는 경우가 많은데, 용어의 의미를 좀 더 명확히 이해하면 해당 용어의 확장이나 응용 역시 쉽게 이해할 수 있는 경우가 많으니 다소 이단적인 이번 컬럼에서의 용어 설명이 도움이 됐길 바란다.

아이펀팩토리 문대경 대표

게임 서버 – 클라이언트 안전하게 통신하기

상황 요약

게임 서버와 클라이언트 사이의 통신을 누가 엿보거나 수정할 수 있다면 어떤 일이 생길까? 특히 게임 서버와 클라이언트 중간에 끼어들 수 있다면 여려가지 시도를 해볼 수 있다. 예를 들어,

  • 클라이언트의 로그인 메시지를 가로채서 대신 로그인하거나
  • 클라이언트가 보낸 결제 토큰을 가로챈다거나
  • 해당 유저인척 다른 유저에게 메시지를 보낸다거나 (혹은 보내는 메시지를 엉뚱하게 위조하거나) 같은 일을 할 수도 있다.

어떻게 하면 중간 공격자가 있어도 열어볼 수 없는 메시지를 보낼까? (feat. 국정원)

예를 들어, 엄청난 인기를 끌고 있는 게임에서 딱 하나 존재하는 아이템을 가지고 있는 유저가 있다고 치자. 그리고 국정원 직원 한 명이 이 아이템이 너무 가지고 싶어서 중간에 도청+조작을 해서 뺏어가려는 상황이라고 치자. 국정원에선 통신사 장비를 감청할 수 있으니 중간에 끼어들어서 로그인 메시지를 가로채려는 상황이라고 해보면 어떻게 하면 이 아이템을 지킬 수 있을까?

일반적으로 다음과 같은 처리를 한다:

  1. 서버와 클라이언트 양쪽에서만 알고 있는 공유 비밀 을 만들고
  2. 공유 비밀 에서 암호화 키를 생성하고
  3. 이 암호화 키로 암호화 알고리즘을 적용한다.

중간에 국정원 직원이 듣고 있는 상황에서, 어떻게 하면 이 공유 비밀 을 만들까?

디피 헬만 키 교환

%ec%bb%ac%eb%9f%bc-%ec%9d%b4%eb%af%b8%ec%a7%801

여기서 디피-헬만 키 교환 알고리즘 을 쓴다.

  1. 양쪽에서 미리 공통 염료를 정한다. (여기선 노란색)
  2. 각자 비밀 염료를 한가지씩 고른다. (각각 다홍색과 청록색을 골랐다)
  3. 2에서 고른 염료를 공통 염료와 섞는다.
  4. 3에서 섞은 결과물을 서로 교환한다. (이건 누가 알아도 상관없다)
  5. 전달 받은 상대방의 혼합 염료에 자기가 고른 비밀 염료를 섞는다.
  6. 이제 결과물로 나온 색이 공유 비밀 이 된다.

여기선 색을 섞는 것으로 비유했는데, 실제 알고리즘에서는 “정수의 거듭제곱을 소수로 나눈 나머지” 를 이용한다.

상대방 확인하기

하지만 위 알고리즘엔 문제가 있다. 국정원 직원이 단순히 엿듣기만 하는게 아니라, 주고 받는 염료 통을 가로챈 뒤에 자기가 고른 거랑 바꿔치기하면 어떻게 될까? 그리고 서버에겐 자기가 클라이언트인 척, 클라이언트한텐 서버인 척 속이는게 가능해진다. 어떻게 하면 이런 문제를 피해갈까?

다음과 같은 방법이 가능하다:

  1. 믿을 수 있는 인증서 서비스 (PKI) 를 쓴다. 그리고 이걸 가지고 전자 서명한 공개키(=3에서 섞은 염료)를 보낸다.
  2. 3에서 서버가 보내는 결과물을 항상 미리 알고 있는 값으로 한다.

보통은 1을 쓰겠지만, 국정원에서 한국의 루트 인증서 서비스 제공자인 KISA에 영향력을 행사할 수 있을지 누가 아는가? [^2] 영향력을 행사 못할거라 생각하면 1을, 아니라면 2를 추천한다. 혹은 1의 방법과 유사하지만, 공인 루트 인증서 대신 서명키를 스스로 만든걸 쓰고, 미리 서명키(=공개키)를 클라이언트에 넣어두는 방법도 있다.


메시지 암호화하기

여기까지 진행했다면,

  • 상대방이 내가 연결하려는 서버임이 확실하고 (PKI 혹은 미리 전달한 공개키 이용)
  • 서로 같은 공유 비밀을 가진 상태가 되었다.

이제 공유 비밀에서 비밀키 암호화에서 쓸 비밀키를 만든다. [^3] 여기서부터는 비밀키와 몇 가지 추가 정보 (nonce 등등)으로 보통 말하는 암호화를 수행한다. HTTPS 같은 걸 쓸 때 들어봤을 3DES, RC4, AES, ChaCha20 같은 알고리즘이 여기에 속한다. [^4]

상대가 보낸 메시지가 맞는지 확인하기

국정원처럼 중간에 메시지를 들여다보고, 수정까지 할 수 있는 경우, 실제로 메시지를 수정하거나, 암호화된 메시지인척 추가로 메시지를 보내는 공격도 가능하다. 이런 경우를 어떻게 발견할까?

SSL/TLS 에서는 메시지 인증 코드 (Message Authentication Code; MAC ) 를 쓴다. 보낸 메시지에 대해서 공유 비밀을 이용해서 추가적인 인증 정보를 붙여서 보낸다.

모바일 환경에선 어떻게 해야할까? (feat. 종량제 통신 요금 & 배터리)

유선 인터넷 환경과 달리, 모바일 환경에서는 대부분 종량제 요금을 사용한다. 그래서 암호화 때문에 데이터 요금이 더 나온다면 그걸 좋아할 사람은 별로 없다. 그리고 유선 전원 대신 배터리에서 전원을 얻어와야 하기 때문에, 배터리가 빨리 닳는 것 역시 암호화로 인한 문제가 될 수 있다. 그래서 이 두 가지를 고려하면서 어떤 알고리즘이 좋을지 얘기해보겠다.

키 교환

국가 수준의 돈과 노력을 들이는 상대로부터 안전한 수준으로 권고하는 값이 있다. 대략 AES-128 정도의 보안을 제공하는 수준인데, NIST 권고안에선 다음과 같은 값을 갖는다.

  • 위에서 언급한 디피-헬만 알고리즘의 경우 적어도 3072-bit (384 bytes) 크기의 메시지를 서로 주고 받아야 한다
  • RSA 알고리즘을 이용해서 키를 보내려고 하면 역시 3072-bit 크기의 메시지가 가야한다.
  • 타원곡선 디피-헬만 알고리즘을 쓴다면 이 크기가 256-bit (32 bytes) 로 줄어든다.

그래서 요즘 모바일 환경에서는 타원곡선 디피-헬만 알고리즘 (ECDH)를 많이 쓴다.

비밀키 암호화

현재 널리 쓰이는 알고리즘 몇 가지를 Unity3D + C 라이브러리로 만들어서 테스트 해본 결과가 아래와 같다. 64 bytes 길이의 메시지를 100,000 번 반복 암호화하는데 걸린 시간을 ms 단위로 측정했다.

%ec%bb%ac%eb%9f%bc-%ec%9d%b4%eb%af%b8%ec%a7%802

구글 크롬 모바일 버전이 구글과 통신할 때 괜히 ChaCha20 을 이용하는게 아니다. 각 기기에서 AES-128 보다 ChaCha20 이 10배-30배 정도 빠르다.

메시지 인증 코드

  • 널리 쓰이는 해시 MAC (HMAC) 으로 SHA-256을 쓴다면 32 bytes 만큼 추가 태그를 붙인다.
  • AES128 에서 주로 쓰는 GCM 태그의 경우 16 bytes 만큼 추가 태그가 붙는다.
  • Poly1305 를 이용한다면 역시 16 bytes 추가 태그가 붙는다.

모든 메시지에 대해서 위 만큼 길이가 늘어나야하는 것이니 HMAC 계열은 모두 불가. (최소 32바이트) AES128-GCM 의 경우 하드웨어 가속이 없는 대부분의 모바일 기기에서 느리다. 그러면 현재로써 남은 선택지는 Poly1305 MAC을 쓰는 정도.

그래서 어떻게 해야하는가?

요약하자면,

  • ECDH 로 키를 교환한다
  • ChaCha20 으로 메시지를 암호화한다
  • Poly1305 인증 코드를 사용한다

… 이걸 실제로 만드려면 아래와 같은 기능을 구현해야한다:

  • (암호학적으로)믿을 수 있는 랜덤 함수
  • 타원곡선 연산 함수
  • ChaCha20 암호화 함수
  • Poly1305 인증 코드 계산 함수

를 만들어야 한다.

그런데 만들면서 주의를 기울여야 할 부분도 많다:

타이밍 공격

모든 함수 내에서 메시지 길이 처리와 관련된 부분이 아닌 곳에서 if 문을 쓸 수 없다. 만약 메시지 내용에 따라 if 문으로 처리하게 되면 어떤 값을 다루고 있는지 추측할 수 있게 된다. 2003년 즈음의 OpenSSL에 포함된 RSA 알고리즘이 이런 식으로 구현되어서 RSA 비밀키를 추측할 수 있는 문제가 있었다.

올해 초에도 OpenSSL에 포함된 DSA 알고리즘이 타이밍 정보를 누출해서 키를 추측할 수 있는 문제가 다시 발견되기도 했다.

잘못 만든 랜덤함수

랜덤 함수 출력값이 예측 가능하거나, 일부 랜덤 값을 보고 다음 값을 추측할 수 있는 경우 안전하지 않은 암호화 알고리즘이 많다. Debian linux 포함된 OpenSSL에 들어있는 랜덤 함수를 잘못 수정했다가 수 많은 서버들의 보안 키가 불안전하게 생성된 적이 있다. (대략 2006년 – 2008년 동안)

재사용하면 안되는 값을 재사용

타원 곡선 디지털 서명 알고리즘 (ECDSA) 의 경우 재사용하면 안되는 난수 (nonce) 값을 쓴다. 만약 이 값을 재사용하면 서명키가 어떤 값인지 알 수 있다. 2010년에 Sony가 PS3 전자 서명할 때 같은 nonce 값을 썼다가 전자 서명키 값이 공개되서 망신당했다.

이런 문제를 다 해결하면서 만들 수 있는가? 그렇지 않다.

대안: SSL/TLS를 쓴다

HTTPS 처럼 사용 시나리오가 명확하다면 SSL/TLS 를 쓰고, 위에서 언급한 알고리즘을 지정해서 쓰는 방법이 있다.

대안: libsodium

위에서 언급한 알고리즘 대다수를 좀 더 고수준 API 형태로 제공하는 라이브러리다. 대부분의 모바일 OS와 서버 환경에서 사용할 수 있다. (Windows, linux, macOS, Android, iOS 지원)

  • 랜덤 함수: OS 별로 제공하는 암호학적으로 적당한 랜덤 함수를 래핑해서 제공한다
  • 키교환: 타원 곡선 알고리즘을 제공한다. 이 알고리즘으로 ECDH 를 구현할 수 있다.
  • 암호화 + MAC: AEAD 란 분류로 해당 알고리즘을 제공한다. (AES128-GCM / ChaCha20-Poly1305)

요약

  • 충분한 시간과 악의가 있는 공격자에 대해서도 게임에서 사용하는 메시지를 읽지 못하게 막을 수 있다
  • 모바일 환경에서 쓰기에 적당한 현대적인 알고리즘들이 존재한다 (ECDH, ChaCha20, Poly1305 MAC)
  • 해당 알고리즘을 안전하게 구현한 라이브러리 널리 쓰이는 라이브러리들이 있으니 가져다 쓰자: OpenSSL, libsodium

[^1]: 중간자 공격 (MITM; man-in-the-middle-attack) 이라고 부른다.

[^2]: KISA Root CA 같은 한국의 루트 인증서 제공자가 있다. 국정원 같은 국가 기관이 여기에 영향력을 행사 할 수 있다고 상상하는 사람은 이 곳을 신뢰하지 않는 루트 인증서로 처리하는게 좋을 것이다.

[^3]: Key Derivation Function. 랜덤한 여러 바이트를 가지고 특정 길이의 키, nonce 등을 생성하기 위해 쓴다.

[^4]: 미국 국립 표준기술연구소 (NIST) 에서는 3DES 대신 AES를 사용할 것을 권고하고 있다. TLS에 대한 표준안 갱신판인 RFC 7465에서 RC4 알고리즘 사용을 금지하고 있다.

아이펀팩토리 김진욱 CTO

글로벌 서비스를 위한 시간 정보 관리

몇 년 전만 해도 게임 개발 시 처음부터 해외 서비스를 고려하는 경우는 드물었다. 그만큼 국내 시장이 빨리 성장하기도 했고, 해외 서비스를 위해서는 클라이언트 배포, 서버 설치를 위한 IDC 확보 등의 문제가 있었기 때문이기도 하다. 그러나 COC, 도탑전기 등의 글로벌 서비스를 통해 크게 성공하는 게임들의 등장과, 각종 마켓 및 클라우드 서비스의 활성화, 국내 시장 내의 경쟁 심화 등으로 인해 개발 시작 시점부터 글로벌 서비스를 염두에 두는 게임들이 많아지고 있다.

1
▲ 가장 성공한 글로벌 서비스 게임 중 하나인 COC
(출처 : http://clashofclans.co.kr/)

글로벌 서비스를 위해서는 언어 문제, 현지화 문제 등 고려해야 할 사항들이 많지만, 의외로 간과하는 경우가 많으면서도 대처하기 쉽지 않은 문제가 시차, 정확히 말하면 시간대와 관련된 문제이다. 단순히 클라이언트에서의 시각 표시 정도의 문제부터 각종 게임 내 이벤트의 일정 설정 문제, 운영 위탁 등의 이유로 특정 시간대에 맞춰 시간을 표시해야 하는 경우 등, 게임 내에서 시간 표시가 사용되는 경우는 적지 않다. 본 칼럼에서는 시간대를 다루는 것이 어려운 이유와 개발 시 고려해야 할 사항들을 정리하려 한다.

시간대란, 정치적이나 경제적인 이유 등으로 같은 기준으로 시간을 계산하는 지역을 말한다. 정오라는 단어의 의미를 생각한다면 태양이 가장 높이 떠 있을 때, 즉 낮 12시이겠지만, 한국의 경우 일본에 맞춰진 시간대를 사용하기 때문에 실제로는 일본 – 정확히는 일본 내의 특정 지역 – 에서 태양이 가장 높게 떠 있는 시간이 정오가 된다.

2

▲ 시간대가 다르기 때문입니다. (출처 : 나무위키 )

따라서 시각을 정확히 기록하려면 해당 시각이 어느 시간대를 기준으로 하였는지에 대한 정보가 필요하다. 매일 한 번씩 진행되는 이벤트가 있을 경우 시작 시간을 단순이 오전 9시로만 기록한다면 중국이나 동남아에서 접속하는 사용자들에게 해당 이벤트가 현지 기준으로 몇 시에 시작하는지 표시해 줄 수 없다. 최소한 9시가 한국이 속한 시간대를 기준으로 한 시각이라는 정보라도 있어야 중국 시간대와의 차이를 계산하여 보여줄 수 있다.

그렇다면 시간대에 대한 정보는 어떻게 기록해야 하는가? 우선 생각해 볼 수 있는 방법은 모든 시각 관련 정보를 기록 시, 해당 시각 산정에 사용된 시간대에 대한 정보와 함께 기록하는 것이다. 하지만 이는 한 서비스 내에서 두 개의 시각 정보를 비교 시 반드시 양쪽의 시간대 정보를 확인해야 하기에 운영 중 실수가 발생할 여지가 많고, 저장 공간이나 통신 용량도 더 차지하며 무엇보다도 귀찮다. 어차피 동일 서비스에서 사용할 거라면, 사용할 시간대 정보를 지정해 두고 모든 시각 정보를 해당 시간대 정보에 맞추는 편이 혼선을 줄일 수 있을 것이다.

그렇다면 어느 시간대를 기준으로 삼는 것이 좋을까? 이 칼럼을 읽는 독자분들은 대부분 한국에서 게임 개발을 진행하실 테니 한국 시간대를 사용하는 게 편하리라 생각하시는 분들도 있을 것이다. 하지만 대부분의 환경에서 제공되는 시간대 관련 개발 라이브러리에서는 시간대 별 시차 정보 등을 모두 UTC를 기준으로 하여 계산, 제공한다. 이러한 상황에서 한국 시간대를 사용한다면 필요한 시간대로의 변환을 위해 UTC로 변환하는 과정이 한 번 더 필요할 것이다. 요즘에는 해외 서비스 시 운영이나 이벤트 진행 등을 현지 파트너와 진행하는 경우 등 동일 서비스를 운영하더라도 둘 이상의 시간대 관리가 필요한 경우도 많아지고 있으며, 최소한 클라이언트에서 표시되는 시각은 대부분 변환이 필요한 만큼 보다 변환이 쉬운 UTC 시간대를 이용하는 것을 추천한다.

UTC로 시각을 기록 및 전송한다면, 필요에 따라 특정 시간대에 맞게 변환하는 과정도 필요하다. 그렇다면 시간대는 어떻게 관리하는 것이 좋을 지에 대해서도 알아보자. 시간대를 표시하는 가장 흔한 방법은 영국 표준시를 나타내는 시간대인 GMT 시간대와 몇 시간 차이가 나는지 기술하는 것이다. 예를 들자면 한국이 속한 시간대는 GMT+9, 미국 서부 지역이 속한 시간대는 GMT-7 로 표시한다. 하지만 이러한 시간 차이에 대한 정보만으로는 변환할 수 없는 경우가 있다.

우선 작은 문제부터 살펴보자면, 모든 시간대가 GMT와 시간 단위로 차이가 나지 않는다는 것이다. 예를 들어, 아마 게임을 서비스하려는 분은 없겠지만, 북한이 GMT+8:30 시간대를 사용한다. 이 칼럼을 작성하는 시점에, 이러한(시간 단위로 시차가 딱 떨어지지 않는) 시간대를 사용하는 지역 중 게임 서비스를 고려할 만한 시장은 별로 없기에 큰 문제는 아닐 수 있다. 하지만 시간대는 변할 수 있기 때문이 미리 고려해서 손해 볼 부분은 없을 것이다. 예를 들자면, 최근 높은 스마트폰 보급률로 주목을 받는 이란은 GMT+4:30 시간대를 사용한다.

좀 더 큰 문제는 일광절약 시간제가 적용되는 지역의 경우이다. 일 년 중 특정 기간에만 GMT와의 시차가 바뀌며, 하루 중에도 시차가 바뀐다. 일 년 중 일광절약 시간제가 적용되는 기간도 나라마다 다르다. 같은 국가 내에서 같은 시간대를 사용하더라도 특정 지방에서만 시행하는 경우도 있다. 따라서 단순히 시간 차이만을 기록해서는 정확한 변환은 불가능하다. 일광절약 시간제는 미국이나 유럽 등 글로벌 서비스 시 고려할 만한 주요 지역들에서도 사용되므로 무시하기도 어렵다.

따라서 일광적약 시간제 관련 정보를 포함하는, 별도의 시간대 표시 방식을 사용하는 것이 좋다. 이러한 용도로 널리 사용되는 시간대 표시 방법에는 크게 영문 대문자 3~4글자로 시간대를 표현하는 time zone abbreviation 과 주로 지역/도시 or 국가 형태로 시간대를 표현하는 IANA time zone, 일명 Olson time zone이 있다.

time zone abbreviation 은 다음과 같은 문제점이 있기에 권장하지 않는다. 우선 동일 지역, 동일한 시간대라도 일광절약 시간제의 적용 여부에 따라 별도의 약어가 존재하는 문제가 있다. 예를 들면 호주 중부 시간대의 경우 일광절약 시간제가 적용되는 경우에는 ACDT, 적용되지 않는 경우에는 ACST라 표시하며, 미국 서부의 시간대의 경우에는 적용 여부에 따라 PDT/PST로 표시한다. 그리고, 몇몇 시간대의 경우 분명 다른 시간대임에도 이름이 같은 경우가 있다. CST라는 시간대의 경우, Central Standard Time( 미 중부 시간대 ), CST( China Standard Time )등 4개( 일광절약 시간제를 고려하면 5개! )의 다른 시간대를 나타내는 약어이다.

IANA timezone은 위에서 언급한 대로 일반적으로 지역/도시 형태로 해당 지역에서 사용하는 시간대를 표시한다. 예를 들면 유럽 내 프랑스 파리의 시간대는 Europe/Paris, 아시아 내 싱가포르의 시간대는 Asia/Singapore 로 표시하는 식이다. 따라서 사용하려는 시간대와 일치하는 이름을 찾아서 지정해 줘야 하는 문제가 있지만 위의 abbreviation 과는 달리, 해당 지역의 일광절약 시간제 관련 정보( 연중 적용 기간, 하루 중 적용 시간 등 )를 포함하고 있기 때문에 적용 여부와 관계 없이 동일한 이름을 사용한다. 그 외에 윤초 등 시각 표시에 관련된 많은 정보들을 포함하여 관리되기 때문에 수고를 들일 만한 가치가 있다.

정리하자면 글로벌 서비스를 목표로 하는 게임에서 시간 관련 정보를 다룰 때에는
1. DB나 통신 프로토콜 내에서의 시각 표시는 UTC 를 사용을 권장
2. 변환 등을 위해 시간대를 지정할 경우에는 IANA timezone 사용을 권장
정도로 정리할 수 있겠다.

글로벌 서비스를 고려하지 않더라도 시간과 연관된 개발은 복잡하다. 특히 시각만이 아닌, 날자 요일 등을 같이 고려해야 하는 경우, 시간대가 바뀌면 날자 역시 바뀔 수 있기 때문에 더욱 더 어려운 문제가 된다. 모든 기능이 마찬가지이지만, 시간대 관련 기능은 구현 시작 시부터 고려하지 않으면 변경 비용이 매우 비싸질 수 있다. 당장은 국내 서비스만을 고려하더라도 시간과 관련된 기능 구현 시 시간대와 관련된 내용을 고려한다면 이후의 상황 변화에 좀 더 유연하게 대처할 수 있을 것이다.

아이펀팩토리 아이펀 디플로이 테크니컬 디렉터 민영기

사례 연구: ‘Pong’을 만들어보자

처음 쓰는 컬럼을 이런 진부한 설명으로 시작하고 싶지는 않았지만, 어쩔 수 없을 것 같다. ‘Pong’은 게임 역사상 처음으로 흥행에 성공한 게임이다. 게임 자체는 지금도 오락실 등에서 볼 수 있는 아이스하키 게임과 비슷하다. 두 명의 사람이 각자 단순한 컨트롤러를 사용하여 상대방 쪽으로 공을 넘겨 통과시키면 이기는 게임이다.

그림1

▲(Pong, 출처: 위키피디아)

하고 많은 게임 중에 이 게임을 선택한 이유는, 실시간 동기화를 구현하기 좋은 게임이라고 판단했기 때문이다. 다르게 말하자면 이 게임은 재미있고, 구현하기도 쉬워 보인다. 그렇다면 어떤 게임이 실시간 게임으로 만들기 쉬운지 어떻게 판단해야 할까? 이 컬럼은 이에 대한 부분을 다룰 예정이다. 추가로 사례 연구를 통하여 어떤 식으로 동기화를 만들어 가는지, 그 꼼수와 우회로 이루어지는 향연에 있어서 전채의 맛을 살짝 느끼는 것도 목표로 삼는다.

실시간 멀티플레이를 구현하기 위해 가장 먼저 고려해야 할 부분 중 하나는 바로 무엇을 동기화 할 것인가 하는 것이다. ‘Pong’은 아마도 화면을 돌아다니는 ‘공’, 그리고 플레이어가 공을 튕겨내기 위해 움직이는 ‘막대’ 부분이 될 것이다. 이 컬럼에서는 공을 어떻게 동기화 할 것인가에 대하여 집중적으로 이야기할 것이다.

공은 게임의 시작부터 끝까지 계속 움직이는 물체이다. 이는 동기화 해야 하는 정보가 쉬지 않고 변한다는 것을 의미한다. 현재의 좌표, 속도, 벽이나 막대에 충돌했는지 끊임없이 파악하고 동기화 해야 한다. 보편적으로 이렇게 끊임없이 변화하는 데이터를 가진 객체를 동기화 하는 것은 매우 어려운 일이다.

하지만 ‘Pong’은 물리 엔진을 사용할 수 있는 게임이다. 어느 한 쪽 막대에 공이 충돌했을 때의 좌표와 속도만 동기화 하면, 같은 환경에서 같은 물리 엔진임을 활용해, 이후의 진행을 추가 작업 없이 자동으로 똑같이 맞출 수 있다. 실시간 게임을 만들 때 동기화 해야 하는 정보와 순간을 최소화하는 것은 매우 중요하다. ‘Pong’은 이를 극단적으로 줄일 수 있기 때문에 상당히 구현하기 쉬운 편이다.

그림3

▲(직접 만든 멀티플레이 Pong의 모습, 양쪽의 위치가 조금은 다를 수 밖에 없다.)

 그러나 네트워크를 통한 데이터의 송수신은 시간을 소모한다. 특히 모바일 기기의 무선망은 최소 수십 밀리세컨드 이상의 지연 시간을 감안하는 것이 정석이다. 이 것도 국내는 아주 좋은 편이라 이 정도, 만약 미국이나 심지어 중국이라면 더 긴 시간이 걸린다고 보는 것이 맞다. 이런 상황에서 Player A의 공 정보를 아무리 빠르게 전송한다 한들, Player B의 정보가 맞춰지는 시간에는 차이가 생기기 마련이다.

그렇다면 어떻게 해야 할까? 가장 먼저 떠오르는 아이디어는, 양 쪽이 같은 정보를 공유할 때까지 잠시 기다리는 것이다. 흐름을 생각하면 다음과 같다.

  1. Player A > 서버 : ‘나의 공이 지금 좌표 (100,100)에 있다’
  2. 서버 > Player B : ‘A의 공이 지금 좌표 (100,100)에 있다고 한다’
  3. B > 서버 : ‘공의 좌표를 (100,100)으로 설정했다’
  4. 서버 > A : ‘B의 공 좌표가 업데이트 되었다’
  5. 이제 다시 A의 공 좌표를 업데이트 한다

여기서 센스 있는 개발자 분들은 바로 ‘어 이러면 안되지 않나?’하고 생각하실 것이다. 맞다. 이렇게 끊임없이 정보를 갱신하는 객체, 특히 그게 바로 화면에 보이는 것은, 이렇게 여러 번 통신을 하면서 지연 시간이 길어지면 바로 플레이의 불편함(랙)으로 이어진다. 위와 같은 방식은 상대적으로 덜 활동적인 장르나 게임 시스템 내부의 처리가 턴제 형태로 이루어지는 게임에서 사용한다.

그렇다면 다시, 어떻게 해야 할까? 다음으로 할 수 있는 방법은 네트워크를 통해 수신하는 데이터에 보정을 가하는 방법이다. 네트워크의 지연 시간을 측정 혹은 추정하고, 그 만큼의 시간이 지난 후의 상태를 계산해서 ‘적절히’ 맞춰 놓는 식이다. 이 것은 실제로 자주 쓰이는 방법이지만, 경험이 많은 개발자라면 알 것이다. ‘적절히’라는건 개발자에게 세상에서 구현하기 제일 어려운 것 중 하나 이다. 마치 ‘플레이어가 느끼기에 적절한 난이도로 몬스터 AI를 만들어 주세요’ 같은 요구랄까..

이제 마지막으로, 그럼 어떻게 하면 좋을까? 실시간 게임을 만들어보지 않은 개발자라면 이 결론이 아주 어색할지도 모른다. 그건 바로 ‘굳이 딱 맞출 필요 없다’이기 때문이다. 어차피 물리 엔진이 계산해주는 공의 이동과 충돌은, 같은 위치에 같은 방향만 맞춰 놓는다면, 결국 같은 곳으로 움직이기 마련이다. 어느 한 쪽의 화면이 조금 늦을 수는 있지만, 최종적으로 보는 화면이 같아진다면 문제가 되지는 않는다.

하지만 여기서 구현을 마무리하면 문제가 생긴다. 바로 앞의 이야기를 뒤집는 것 같지만  ‘최종적으로 보는 화면이 같아’져야 한다는 것이 중요한 기준이 된다. 예를 들어 공의 속도가 매우 빨라진 상태에서 네트워크 지연 시간이 늘어나는 경우, A의 화면에서는 이미 승리한 상태로, B의 화면에서는 아직 공이 다가오는 상태로 보일 수 있다. 이런 상황을 방지하는 것을 보장할 필요가 있다. 단, 네트워크 지연 시간이 수백 밀리세컨드 이상 비정상적으로 계속 늘어나거나, 아예 끊기는 경우는 여기서 고려하지 않는다.  물론 이런 상황의 예외 처리도 게임의 상용화를 위해 꼭 필요하지만, 네트워크에 문제가 생긴 다른 플레이어를 기다릴 지, 패배 처리를 할지, AI를 투입할지 같은 식으로 기술보다는 게임 정책의 문제가 된다.

그렇다면 네트워크 지연 시간을 보정하는 것은 어떻게 접근해야 할까. 이 것 역시 개발자가 단독으로 처리하기 보다는 기획과 유기적으로 이야기를 나누며 진행하는 것이 더 좋은 결과물을 만드는 경향이 있다.

다시 ‘Pong’으로 돌아오자. 플레이어 A가 공을 보내 B가 반응 해야 하는 거리까지 도달하는 동안, 벽에 두 번 부딪히면서 가는 일반적인 상황을 생각해보자. 네트워크 전송이 제대로 이루어지고 있는지, 네트워크 지연 시간은 얼마나 되는 지를 확인하기 위해 벽에 충돌할 때에도 정보를 전송한다고 가정하면, 흐름은 다음과 같다.

  1. 공이 A의 막대에 충돌했을 때의 위치와 속도의 변화를 서버를 통해 B로 보냄
  2. B는 해당 정보를 물리 엔진에 적용
  3. 공이 벽에 충돌한 위치와 속도를 서버를 통해 B에 보냄
  4. B는 해당 정보를 물리 엔진에 적용
  5. 공이 벽에 충돌한 위치와 속도를 서버를 통해 B에 보냄
  6. B는 해당 정보를 물리 엔진에 적용
  7. 이제부터는 B가 충돌했을 때의 위치와 속도의 변화를 A에게 보냄

위 흐름에서 네트워크 지연은 모든 부분에서 발생할 가능성이 있다. 1)에서 상대적으로 높은 네트워크 지연이 발생하고, 3), 5)에서 점점 더 빠르게 도착하는 경우에는, 공이 벽에 충돌할 즈음에 순간 이동을 하여 예상보다 더 빠르게 움직이게 되고, 1)보다 3), 5)에서 네트워크 지연이 커지는 경우, 진행하던 공이 다시 벽으로 돌아가는 현상이 발생한다.

솔직히 말하자면, 실시간 게임에서 종종 순간 이동이 일어나는 것은 사실 심각한 문제는 아니다. 모든 실시간 게임은 일정 부분 이런 문제를 가지고 있고, ‘Pong’역시 마찬가지로 볼 수 있다. 하지만 잦은 순간 이동은 플레이를 불편하게 만든다. 좀 더 보기 좋게 만드는 방법은 없을까?

여기서 게임의 특성, 재미 요소와의 조율이 이루어진다. 네트워크 지연 시간으로 인한 순간 이동이 피할 수 없는 것이라면, 그건 가능한 플레이어가 막대로 반응하기 최대한 이전에 이루어지는 것이 좋다. 플레이어가 막대로 반응하기 바로 직전, 벽에 충돌하면서 보정이 일어나 순식간에 공이 앞으로 다가온다면 당연히 짜증이 날 수 밖에 없을 것이다.

이에 따라 B에서 1)은 정보가 날아오는 순간 바로 적용하고, 3)과 5)는 내용과 현재 공의 위치에 따라 큰 차이가 나는 것이 아니라면 적용을 하지 않고 넘어가는 것도 좋은 방법이다. 다시 말하자면, 어떤 상황에서는 정보의 업데이트를 최대한 빠르게 하기보다는 오히려 뒤로 미루는 것이 더 나은 게임성을 제공하기도 한다는 것이다. 아이러니하지 않은가. 이와 같이 실시간 게임의 동기화 정책은 기본적으로 기술적인 문제이지만, 결정은 개발자 뿐 아니라 기획자는 물론, 팀원 전체가 플레이를 통해 다듬어 가는 것이 가장 좋은 결과물을 만드는 길이다.

지금까지 나온 이야기를 세줄 요약한다면, 다음과 같이 할 수 있겠다.

  • 실시간 멀티 플레이 게임은 동기화 해야 하는 정보와 빈도가 적을 수록 만들기 쉽다.
  • 네트워크 지연 시간이 존재하기 때문에, 이론적으로 완벽한 동기화는 불가능하다.
  • 완벽한 동기화를 추구하기 보다는, 게임을 플레이하기 좋은 방향으로 접근해야 한다.

물론 이 외에도 개선해야 할 사항은 얼마든지 남아있다. 이는 꾸준한 고민과 테스트를 통해서 찾아내고 다듬는 작업이 된다. 그 것은 개발 초기에 발견할 수도 있고, 런칭 직전에나 떠오를 수도 있으며, 심지어 출시하고 꽤 긴 시간이 지난 이후에 알게 될 수도 있다. 시기와 상관 없이, 위의 큰 틀을 따른다면, 아마도 좋은 게임을 만들 수 있지 않을까 생각한다.

아이펀팩토리는 곧 위에서 설명한 ‘Pong’ 게임의 소스코드 전체를 공개할 예정이다. 또한 향후 개발자 세션을 통하여 어떤 식으로 구현했는지, 이 컬럼에서 언급하지 않는 막대는 어떻게 다루었는지, 추가로 최적화 할 수 있는 여지는 어디에 있는지도 함께 이야기 나누는 시간을 가질 예정이다.

또한 아이펀 엔진을 정식으로 사용한다면, 위와 같은 초기 아키텍팅에 대한 고민도 개발자와 함께 나눈다.

감사합니다.

아이펀팩토리 박근환 아이펀 엔진 테크니컬 디렉터

게임 서버 구축 방법 비교: GBaaS vs. 설치형 게임 서버 엔진

구글 등의 검색 엔진에 게임 서버 엔진을 검색해보면 GameSparks, Playfab, Fireline 등의 서비스들을 찾아볼 수 있다. 이들은 공통적으로 “게임의 백엔드 시스템을 제공” 하며 인증, 빌링, 매치메이킹이나 랭킹 등의 기능이 이미 만들어져 있다고 설명한다. 이런 류의 서비스들은 “백엔드 시스템을 이미 갖춰두고 이를 필요한 만큼 쓸 수 있게 한다” 라는 의미에서 Backend-as-a-Service, 줄여서 BaaS 라고 부른다. (“-as-a-Service” 는 “클라우드 형태로, 필요한 만큼 쓰고 비용을 지불할 수 있도록 서비스화 한 것” 으로 이해하면 된다.) 그리고 이 중에서도 특히 게임 서비스를 위한 백엔드 서비스를 Game Backend-as-a-Service, 줄여서 GBaaS 라고 부른다. 앞에서 언급된 서비스들은 모두 현재 서비스되고 있는 GBaaS 들이다. 그럼 이런 GBaaS 와 아이펀 엔진과 같은 설치형 게임 서버 엔진은 어떤 점에서 다를까?

먼저 설치형 게임 서버 엔진과 GBaaS가 어떤 방식으로 동작하는지를 구성도로 살펴보겠다.

1. 설치형 서버 엔진을 사용하는 경우
설치형 서버 엔진을 사용하는 경우, 서버 프로그래머는 엔진이 제공하는 기능들을 이용하여 게임 콘텐츠를 구현하게 된다. 서버 엔진은 여러 게임에서 사용될 수 있는 공통의 기능을 제공하기 때문에, 구현하려는 콘텐츠에 따라서 1) 제공되는 기능이 정확히 입맛에 맞는 경우가 있을 수 있고, 경우에 따라서는 2) 서버 프로그래머가 엔진이 제공하는 공통 기능을 이용해 게임에 특화시킨 콘텐츠를 구현해야 될 수도 있고, 3) 필요한 기능이 아예 제공되지 않거나 아니면 구현하려는 콘텐츠와 맞지 않는다면 별도로 기능을 구현해야 될 수도 있다.

설치형 서버 엔진과 게임 콘텐츠 코드는 하나의 실행 파일로 묶여서 한 프로세스로 동작하게 되어 동작 속도가 빠르며, 이렇게 만들어진 게임 서버는 클라이언트와의 통신을 제외하고는 외부 통신을 거의 하지 않고, 모든 처리를 해당 클라우드 계정 혹은 해당 데이터 센터 안에서 끝낼 수 있기 때문에 서버를 보다 안전하게 관리할 수 있고, 클라와의 통신을 제외한 별도의 네트워크 트래픽에 따른 과금 역시 걱정하지 않아도 된다.

또한 유저데이터가 저장되는 디비 서버 역시 같은 클라우드 나 같은 데이터센터 안에 저장할 수 있고, 이에 따라 운영이나 사업상의 이유로 유저 디비를 접근해야 되는 경우 손쉽게 처리할 수 있다.

22-gbaas001

▲그림1: 설치형 서버 엔진의 동작 방식

위의 그림은 유저가 아이템을 사용하기 위해서 화면을 터치한 경우 발생하는 일들에 대한 예시이다. 이렇게 아이템 사용이 발생하는 경우 클라는 서버에 아이템 사용 요청을 전송하고, 게임 서버는 아이템 사용에 따른 효과를 적용하고 아이템을 인벤토리에서 삭제 한 후에 이 데이터를 디비에 저장한다. 그리고 아이템 효과에 따라 유저의 상태값이 바뀐 경우 이를 클라에게 전송해준다.

2. GBaaS 를 사용하는 경우
GBaaS 는 유저의 인벤토리, 게임 내 재화 등을 포함해서 유저데이터의 전체 혹은 일부를 GBaaS 상에 저장하고, 게임 서버가 이 데이터들에 접근할 수 있도록 REST API 를 제공한다. 그리고 매치메이킹처럼 공통적으로 많이 사용되는 게임 콘텐츠 기능들 중 일부도 GBaaS 상에 구현하고 이를 쓸 수 있도록 REST API 를 제공한다. REST API 는 쉽게 말해서 HTTP 를 이용해서 컴퓨터 간에 통신을 하게끔 구현한 것이라고 생각하면 된다. (우리가 웹 브라우저 주소창에 http:// 라고 쓰는 것도 HTTP 방식으로 사이트를 접근하겠다는 의미인데, HTTP 는 이렇게 사람이 볼 수 있게 웹사이트를 구축할 때도 쓰이지만, 컴퓨터끼리 통신을 할 때도 이용된다.) GBaaS 는 중요 유저 데이터가 GBaaS 상에 저장되고, 매치 메이킹이나 랭킹 같은 주요 기능이 아예 GBaaS 상에서 구현되어 제공되기 때문에, GBaaS 가 지원하는 기능에 잘 들어맞는 게임의 경우 정말 손쉽게 게임을 구현할 수 있다는 장점이 있다. 또한 GBaaS 사업자가 자기들 서버를 관리하기 때문에 유저 데이터 저장을 위한 DB 관리, 그리고 기능이 구현된 서버 관리를 별도로 하지 않아도 된다는 것도 큰 장점이 될 수 있다. 하지만, GBaaS 가 제공하지 않는 기능을 사용하는 콘텐츠는 아예 구현할 수 없게 되거나 혹은 직접 구현하고 싶으면 GBaaS 에 저장되는 데이터 외에 별도로 디비 서버를 만들어서 저장해야 된다는 문제점이 있다. 그리고 경우에 따라서는 별도의 디비 서버를 둔다고 하더라도 GBaaS 상에 저장되는 유저데이터를 같이 접근해야 되는 콘텐츠 구현은 굉장히 복잡해진다는 단점이 있다. 그 때문에 GBaaS 는 해당 기능에 잘 들어 맞는 양산형 게임에는 적합하지만, 실시간 대전이나 창의적인 게임 콘텐츠의 게임 구현에는 적합하지 않을 수 있다. 또한 중요 유저 데이터가 GBaaS 에 저장되기 때문에, 보안 때문에 유저 데이터 관리에 민감한 경우 곤란할 수 있고, GBaaS 사업자가 폐업하는 경우 게임을 내려야 되는 일이 발생할 수도 있다. 그리고 GBaaS 역시 클라우드 상에 존재하는 서버들이기 때문에, GBaaS 를 통해 유저 데이터를 접근하거나 매치메이킹 같은 기능을 호출해야 된다면 게임 서버가 매번 외부에 있는 GBaaS 서버들과 통신을 해야 됨을 의미한다. 이는 클라-서버 간의 트래픽에 따른 네트워크 비용 외에 서버-GBaaS 간의 트래픽에 따른 네트워크 비용의 발생을 의미하고, GBaaS 서버가 어떤 지역의 클라우드에 있느냐에 따라 높은 딜레이를 겪을 수도 있음을 의미한다. (게임 서버가 돌게 될 클라우드에 GBaaS 서버들을 추가하면 쉽게 해결될 수 있다고 생각할지도 모르겠지만, GBaaS 사업자 입장에서도 확장하려는 클라우드에 충분한 고객 (자사의 GBaaS 를 쓰는 게임들) 이 없는 경우 비용상의 문제로 쉽게 확장을 하지 못할 수도 있다) 이런 딜레이 문제 때문에 GBaaS 는 비동기 방식의 게임에 좀 더 적합하고 실시간 게임 지원에는 한계가 있을 수 밖에 없다.

23-gbaas002

▲그림2: GBaaS 를 사용하는 경우 동작 방식

위의 그림은 GBaaS 가 제공하는 유저 인벤토리 기능을 이용했을 때, 아이템을 사용하면 어떤 단계들을 밟게 되는지를 보여준다. 먼저 클라가 게임 서버에 아이템 사용 요청을 보내면, 서버는 GBaaS 가 제공하는 REST API 를 호출한다. 이 때, REST API 들은 공통 기능별로 작게 잘라서 제공되기 때문에, 인벤토리에서 아이템을 삭제하는 API 와 아이템의 효과에 따라 유저 데이터를 갱신하는 API 를 따로 호출해 줘야 된다. (GBaaS 가 특정 게임의 특정 아이템의 특성을 모두 대응해 줄 수 없기 때문에, 이처럼 “인벤토리에서 아이템을 소모한다”, “유저 데이터를 갱신한다” 등과 같이 작은 단위로 쪼개진 API 가 제공된다) 이렇게 API 를 호출하게 되면, GBaaS 는 요청대로 아이템을 인벤토리에서 지우는 작업과 유저 데이터를 지우는 작업을 수행하고 각각의 결과를 게임 서버에 반환한다. 게임 서버는 이 두 작업이 모두 성공적으로 수행되었다면 클라에게 변경된 유저 상태값을 전송해주게 된다. 앞에서 설명한 것처럼 이 경우에는 게임 서버가 GBaaS 의 API 를 호출하는 단순한 역할을 수행하고, API 를 호출할 때마다 GBaaS 와의 트래픽이 발생하고, GBaaS 상에 유저데이터가 저장됨을 알 수 있다. 만일 게임 서버가 GBaaS 에서 제공해주지 못하는 기능을 별도로 구현하고 있다면 이렇게 따로 게임 서버를 주는 것이 의미가 있겠지만 정말 모든 것을 GBaaS 에 의존하고 있다면 별도의 게임 서버를 제거할 수도 있을 것이다. 아래 그림은 게임 서버를 제거하고 게임 클라가 직접 GBaaS 에 접근하는 경우이다.

.
24-gbaas003

▲그림3: GBaaS 의 다른 예: GBaaS API 를 호출하는 게임 서버를 별도로 두지 않고 게임 클라가 직접 GBaaS 의 API 를 호출한다.

이렇게 바꾸더라도 아이템 사용에 따른 동작이 2개의 API 호출을 발생시킨다는 점에서는 변함이 없다. 그리고 이제는 GBaaS 에서 제공하는 것 외에 별도의 기능 구현은 아예 불가능하다. 정리하면, [GBaaS 의 장점] GBaaS 가 제공하는 기능 셋에 잘 들어맞는 게임이라면 정말 빨리 게임 서버를 만들 수 있다. 특히 별도의 서버 관리도 필요 없고, 기능이 GBaaS 를 통해서 제공되기 때문에 서버 프로그래머도 필요 없다. 만일 구현해야 되는 게임이 이 경우라면 질문의 여지 없이 GBaaS 는 최고의 선택이 될 수 있다. [GBaaS 의 단점] 1. 구현하려는 게임 콘텐츠가 제공되는 기능에 완전히 들어맞지 않는 경우, GBaaS 외에 별도로 게임 서버를 두고 구현해야 되는데, 이 작업이 그냥 게임 서버를 만드는 것보다 훨씬 어렵고 복잡할 수 있다. 2. 게임의 주요 데이터가 GBaaS 상에 저장됨으로 인해 유저데이터 관리 문제와 GBaaS 사업자 폐업으로 인한 서비스 중단 문제가 발생할 수 있다 3. 게임 서버가 매번 GBaaS 와 통신을 해야되기 때문에 게임 서버 비용 외에, 서버-GBaaS 간에 높은 네트워크 트래픽 비용이 발생하며, GBaaS 사용료도 별도로 발생한다. 4. GBaaS 와 게임이 서비스되는 지역 사이의 네트워크 딜레이에 따라 게임의 장르가 영향을 받을 수 있다. 특히 실시간 콘텐츠 구현은 GBaaS 에서 사실상 불가능하다.

아이펀팩토리 문대경 대표

모듈화, End-to-end 원칙, 그리고 Fate-sharing

게임 서버처럼 규모가 큰 시스템을 구현할 때, 우리는 일반적으로 관련된 기능을 묶어서 다루게 된다. 그리고 그걸 “모듈 (Module)” 이라고 부른다. 시스템 모듈화는 처음 시스템을 만들 때 모듈단위의 점진적 구현이 가능하게 한다는 점도 있지만, 추후 시스템 갱신 시 손 대야되는 것의 범위를 한정해준다는 점에서 더욱 중요하다. 다시 말해 시스템 기능이 변경 될 때 전체 시스템을 모두 바꾸는 것이 아니라 관련된 모듈만 교체할 수 있는 것이다.

이때문에 모듈은 단순히 관련된 기능의 집합으로서 “적당히” 모아 두는 것이 아니라, *잘 정의된 (well-defined) 최소한의 (minimal)* 인터페이스를 갖도록 구현되어야 한다. 그래야만 모듈이 변경될 때 미치는 영향을 최소화할 수 있고, 모듈 자체도 인터페이스를 유지하는 한 내부 구현에 대해 자유도를 가질 수 있다. 이런 이유로 모듈화는 시스템 구현에 있어서 중요한 원칙 중 하나이다.

그런데 모듈을 단순히 코드 단위로 묶이는 물리적 단위로 생각하는 것보다 프로세스나 기계 혹은 이들의 클러스터 단위로까지 묶일 수 있는 논리적 단위로 생각하는 것이 좋다. 그러니까 같은 서브디렉토리 아래 있는 소스 파일들이나 라이브러리 뿐만 아니라, 특정 유사 기능을 묶어서 프로세스나 기계로 나눠주는 것도 모듈화다. 예를 들면 우리가 게임 서버에서 구글 플레이 인증을 검증하기 위해서 구글 서버에 REST API 로 접근하게 되는데, 이때 API 를 받는 것이 하나의 프로세스일지 한대의 기계일지 기계들의 집합일지 알 수 없지만 (물론 구글이 처리하는 요청량을 볼 때 당연히 기계들의 집합일 것이다) “인증 검증” 이라는 특정 기능을 전담하는 구글 서버는 그것 자체로 하나의 서비스로도 이해될 수 있고, 전체 “게임 시스템”이라는 측면에서는 외부 모듈로도 이해될 수 있다. 그리고 REST API 가 앞에서 설명한대로 그 모듈의 인터페이스고 우리는 구글이 그 인터페이스를 바꾸지 않는 한 구글 엔지니어가 인증 처리를 더 효율적으로 하기 위해서 내부에서 코드를 바꾸는 것에 개의치 않는다.

그러면 모듈은 어떤 단위로 나누는게 좋을까? 그리고 기계단위로까지 모듈이 흩어질 수 있다면 모듈은 어디서 구현되어야 할까? 그리고 모듈이 상태값 (state) 를 가져야될 때 그건 어디에 저장되어야 될까?

모듈을 어떤 단위로 나눌까 하는 문제에 있어 컴퓨터 네트워킹 쪽에서 주로 사용하는 방법은 “계층 (layer)” 단위로 나누는 것이다. 우리가 사용하는 TCP/IP 역시 이 계층화 원칙에 따른 결과물이다. HTTP 로 게임 서버와 통신하는 것을 예로 들어보면, 1) 802.11a/b/g/n/ac 이라는 물리 규격 계층 위에 2) 안정적인 전송을 보장하지는 않지만 최대한 보내려는 노력은 해주는 (best-effort) IP 계층이 존재하고, 3) 그 위에 전송의 안정성을 보장하는 TCP 계층과 4) TCP 연결이 끊기더라도 사용자 정보를 유지할 수 있게 하는 HTTP 세션 계층, 5) 끝으로 그 위에 게임 서버-클라이언트 프로토콜이라는 게임 애플리케이션 계층이 존재하는 것이다.

그럼 네트워크에서 하나의 프로토콜 계층이 하나의 모듈 단위라면, 앞에서 말한 “잘 정의된 최소한의 인터페이스” 라는 건 어디에 존재하는 걸까? 정답은 프로토콜 헤더다. 상위 계층은 하위 계층의 기능을 이용하기 위해서 하위 계층의 헤더를 채워야하고 (아마 어떤 독자는 그런 헤더를 직접 채운 적이 없어서 의아해할 수도 있는데, 여러분이 사용하는 소켓 라이브러리가 이 헤더를 채워주고 있다), 하위 계층의 헤더에 존재하지 않는 건 쓸 수 없다. 일부 프로그래머들은 프로토콜 헤더를 통신을 위한 단순한 더미로 인식하고 있는 경우가 많다. 그러나 프로토콜에 있어서 핵심은 헤더라고 해도 과언이 아닐 정도로 특정 프로토콜이 할 수 있는 것과 할 수 없는 것은 헤더 포맷으로 결정 되며, 최대한 단순한 인터페이스라는 측면에서 프로토콜 헤더에 새로운 필드를 추가하는 것은 신중해야된다.

그럼 프로토콜 헤더에 뭔가를 추가하는 것이 신중해야된다고 했는데, 넣어도 되고 안되고는 어떤 기준으로 판단할 수 있을까? 이 기준의 좋은 예가 “End-to-end 원칙”이다. 양쪽 끝단 (end) 이 통신하면서 어떤 기능이 필요하다고 했을 때, 아랫쪽 계층에 그 기능이 들어가도 되는지에 대한 가이드라인인데, 이 원칙은 관점에 따라 여러 방식으로 표현되고 이해될 수 있다.

먼저 해당 계층이 자기의 힘만으로 그 기능을 완벽하게 구현할 수 없다면 그 기능을 넣어서는 안된다. 즉, 불완전한 기능을 넣어서는 안된다는 뜻으로 이해할 수 있다. 불완전한 기능은 쓰기에 부적합해서 어차피 양 끝단 (상위 계층)에서 같은 기능을 직접 만들어야 되는 상황이 발생하기 때문이다.

다른 관점으로는, 만일 해당 기능이 양 끝단 (상위 계층) 에서 충분히 만들 수 있다면 아래 계층에 그 기능을 넣지 말아야 한다. 그러니까 아래 계층은 윗 계층이 할 수 있는 일을 대신하는게 아니라 할 수 없는 일에 집중해야된다는 뜻이다. 다만 예외적으로 높은 성능 향상이 있는 경우는 아래 계층에 포함 시킬 수 있는데, 이때도 해당 기능을 쓰지 않는 다른 상황이 부가적인 오버헤드를 겪어서는 안되야만 한다.

이 원칙은 네트워크 통신의 계층화 뿐만 아니라 다른 경우에도 적용해 볼 수 있는데, 예를 들어 여러 게임에 사용될 공통 라이브러리를 만든다고 했을 때, 어떤 기능이 그게 사용될 게임의 특성을 알아야만 하는 것이라면 일반적으로는 라이브러리에 포함되서는 안되고, 개발 시간 단축이나 라이브러리 내부 구조를 모르고 만들 때 보다 큰 성능 향상의 메리트가 있다면 고려해볼 수 있다. 다만 해당 라이브러리를 쓰는 다른 게임들이 그 기능 때문에 라이브러리를 쓰는게 복잡해지거나 성능 하락을 겪어서는 안된다.

End-to-end 원칙은 네트워크 계층 설계에 핵심적인 원리지만, 사실 현실 세계에서는 이 원칙을 의도적으로 무시하는 경우들이 존재한다. 예를 들어 우리가 사용자들이 가끔씩 광고를 보는 조건으로 무료 WiFi 를 제공한다면 광고를 끼워 넣기 위해서는 양 끝단의 통신 (end-to-end) 에 무선 공유기가 끼어들 수 있어야할 것이다. 또한 우리가 게임을 서비스할 때 로드밸런서를 사용한다면, 게임 클라-서버의 통신이라는 end-to-end 중간에 로드밸런서라는 장비가 끼어든 것이기도 하다. 비슷하게 보안장비나 torrent 트래픽을 별도로 처리하기 위한 QoS 장비들 역시 end-to-end 를 의도적으로 깨는 경우다. 그러나 이런 것들은 end-to-end 원리가 무의미하다는 뜻이 아니라, 커뮤니케이션이라는 본연의 목적 외에 관리 목적 때문에 의도적으로 위반하고 있는 것으로 이해하는 것이 맞을 것 같다.

끝으로 분산 환경에서 상태값(state)은 어디에 저장하는 것이 좋을까? 이에 대한 가이드라인이 Fate-sharing 원칙인데, Fate-sharing 원칙은 상태값을 이용하는 객체가 상태값을 관리하게 해서 둘이 운명을 공유하게 (fate-sharing) 해야된다는 뜻이다. 이는 해당 객체가 죽는 경우 상태값을 지워줘야 되는데, 만일 상태값이 다른 곳에 저장 된 경우, 해당 객체가 죽었는지 살았는지를 계속 확인하지 않는 한 죽은 객체의 상태값이 남을 수 있기 때문이다. 우리가 모바일 게임을 개발해서 TCP/IP 연결을 맺더라도 이 연결에 대한 소켓 정보는 게임 서버와 스마트폰의 OS 레벨에서 저장이 될 뿐 중간에 거치게 되는 라우터에는 저장이 되지 않는 것도 이 원칙에 의한 것이다. 그리고 이처럼 중간에 있는 객체가 상태에 대한 정보를 가지고 있지 않으면, IP 가 중간에 포워딩 경로를 바꿀 수 있는 것처럼, 양 끝단 (end-to-end) 은 중간에 커뮤니케이션 경로를 바꾸는 등 자유도가 높아질 수도 있다.

만일 Fate-sharing 원칙을 적용하기 어려운 경우 대안으로 저장되는 상태값을 soft-state 로 유지하는 방법도 가능하다. Soft-state 는 상태값이되 일정 시간이 지나면 사라지는 상태값으로써, 상태값을 쓰는 객체가 주기적으로 상태값을 갱신하게끔 해서 만일 그 객체가 죽는 경우 일정 시간 뒤에는 상태값이 자동으로 소멸되는 것을 보장한다.

이로써 지난 세번의 컬럼을 통해 시스템 설계에 대한 전반적인 이야기를 했다. 첫 컬럼에서는 시스템 설계에 있어서 우선 순위 설정에 따라 시스템의 모습이 달라짐을, 두번째 컬럼에서는 이 관점에서 TCP/IP 가 어떤 우선 순위로 설계 되었는지를, 그리고 이번 컬럼에서는 시스템 설계에 적용되는 일반적 원칙들을 살펴보았다. 다음 컬럼에서는 본격적으로 게임 서버에 대해서 이야기 나누도록 하겠다.

아이펀팩토리 문대경 대표