왜 c#으로 서버를 작성하려 하나?

30 Jun 2014

c# server

본 글은 동아리 친구의 질문인 '왜 게임 서버를 c++이 아닌 c#으로 작성하려 하냐?'에 대한 답변이다.

간단히 c++과 c#의 차이를 통해 답변하면 이렇다.

  • c++은 속도가 빠르다.
  • c#은 기본 라이브러리가 풍부하다.
  • c#은 표현력이 좋다. linq나 reflection의 도움을 받을 수도 있다.
  • c#은 native에서 벌어지는 access violation 등으로부터 다소 안전하다.

즉, c#으로 프로그래밍할 경우 c++로 할 때에 비해서 보다 편하게, 보다 안전하게 프로그래밍을 할 수 있다고 생각한다. 그렇기 때문에 c#으로 작성한다고 답변한 것이다. (물론 도메인에 의한 판단이 우선이다. 속도가 중요한 서버인데 c#으로 짜라는 고집을 부리지는 않는다.)

게임 서버를 구현한다고 해보자. 게임 서버는 게임 + 서버이므로, 클라이언트를 처리하는 서버적 기능과 게임이적 요소를 포함하면 되겠다. 대충 다음과 같이 분류할 수 있을 것 같다.

  • network: 클라이언트의 요청을 처리해야 한다.
  • persistence: 클라이언트의 정보를 저장해야 한다.
  • logging: 클라이언트의 행적을 기록하고 운영 대응을 해야 한다.
  • logic: 게임 내용을 위한 요소들(npc, 시야, 전투, 커뮤니티, 기타 컨텐츠 등)

요소별로 생각해보자.

  • network 코드는 기반 network 코드와 message handler 코드로 나눌 수 있다.
    • 기반 network 코드는 message를 주고받는 부분이나, byte stream을 암호화하는 부분 등으로 나눌 수 있다.
    • message handler는 message를 설계하고 handler 코드를 구현/등록하는 부분으로 나눌 수 있다.
  • persistence는 게임 object에 대한 crud에 대한 코드가 있다.
  • logging은 게임 object 혹은 content에 대해 기록을 남기는 코드일 것이다.
  • logic 코드는 데이터를 읽거나 상황을 판단해서 content 별로 적절히 처리하는 코드일 것이다.

network나 message 쪽 코드는 워낙 generator도 많고 좋은 추상화된 라이브러리도 많아서 c++이나 c#이나 크게 차이가 없을 수 있겠다. c++도 protobuf에 asio 붙이면 코드가 그렇게 끔찍하지는 않다고 생각한다. 하지만 c#에서는 딱히 라이브러리 안 붙여도 core 코드를 적은 줄에 쉽게 작성할 수 있다. (약간의 성능을 포기하고 async/await을 쓰면 더 짧아진다.)

persistence 쪽 코드나 logging 코드는 (경험상) bolierplate 코드가 많았기 때문에 무의미한 반복 코딩을 하게되는 경우가 많았다. 하지만 이 쪽도 c++아니 c#이나 ORM이나 code generation 등으로 어느 정도 귀찮음을 줄일 수 있으므로 큰 차이가 없다고 생각할 수도 있겠다. (개인적으로는 reflection이 있기 때문에 c# 쪽이 더 편리한 점이 많다고 생각한다.)

하지만 위 부분들은 모두 전체 서버 코드에 큰 비율을 차지하지 않는다. 가장 많이 작성해야 하는 부분은 logic 코드 부분이다. 이 부분에서는 대부분 데이터를 탐색하거나, 연산을 하거나, 객체를 가져와서 변경하는 작업을 주로한다. 이러한 코드를 작성함에 있어 과도할 정도로 표현력이 풍부한 c#이 아무래도 c++보다 코딩하기 낫다고 생각하는 것이다.

요약하면 그냥 c#으로 두 줄 작성하면 되는 것을 c++로 작성하려면 열 줄, 스무 줄로 늘어나니 귀찮다는 것. 그래서 가능하다면 c++보다는 c#으로 작성할 것이다.

성능?

약간 다른 이야기지만 c++과 c#의 성능 비교 이야기를 해보자. 현재 내가 알고 있는 범위에서 c#이 느린 부분은 다음과 같다.

  • native에 비해 기본 연산이 느리다. 물론 그렇겠지만 JIT가 돌아가는 마당이니 큰 차이는 없다.
  • async/await가 느리다. DefaultTaskScheduler가 좀 대충 만들어져서 느린데 고쳐서 쓰거나 그냥 AsyncIO를 쓰면 어느 정도 회피할 수 있기는 하다.
  • gc가 돌면 세상이 멈춘다.

c++에서 하던 식으로 모든 객체를 메모리에 올려두는 식으로 프로그래밍하다 보면 당연히 gen2에 쌓이는 객체가 많아진다. 때문에 gen2를 탐색하는 gc가 수행될 때 서버의 전체적인 throughput이 크게 떨어지는 문제가 발생할 수도 있다. 물론 concurrent gc를 사용하거나, server gc를 잘 튜닝해서 사용하면 문제를 어느 정도 회피할 수 있다고는 하지만 간단해 보이지는 않는다.

gen2로 가는 객체의 수를 줄이는 것이 관건인데 이게 또 간단하지 않다.

  • 동접이 5,000명이라면 적어도 유저 객체 5,000개를 메모리에 올려놔야 한다는 것인데, 각 유저 객체마다 DictionaryList를 갖기 시작하면 그 내에서도 파편화된 수 많은 객체들이 존재할 수 있기 때문이다.
  • 또한 게임 서버에서 사용되는 데이터들에 대해서도 서버 구동 시 미리 읽어두는 경우가 많은데 이 객체들이 모두 gen2로 넘어가게 된다.

이들을 최적화하는 방법을 찾아야 좀 제대로된 서버를 만들 수 있을 것이다. 이 때문에 c++/cli 영역에서 메모리를 할당한 후 그것을 사용하는 사람도 있었고, 아니면 단순히 python으로 갈아타는 사람도 있었다.

위 문제를 열심히 고민하고 있는데 적절한 해결책을 찾지 못했다. 좀 더 고민해봐야 겠다.

comments powered by Disqus