서버 프로그래머가 되기 이전엔 멀티 스레드 따위 관심도 없었다.
물론 그 시기까지가 클럭 향상 -> 멀티 코어로 변화가 이루어지기 전이기도 했지만… 여하튼 나는 그런 것 보단 다른 것들에 관심이 훨씬 많았다.
서버 프로그래밍을 시작하면서 멀티 스레드를 다루기 시작했고 만 7년이 된 지금까지 여러 프로젝트를 경험해왔고, 여러 사고를 쳐왔다.
그냥 정줄 놓은 누가 봐도 코드를 잘못 짜서 생긴 사고 (…)도 많았지만, 그보다 더 큰 사고는 주로 함정에서 발생했는데, 그 중 최고봉은 역시나 멀티 스레드 버그였다고 할 수 있다.
아니 아주 정확히는 멀티스레드에 맞게 코딩하지 못한 내 버그다.
내가 지금껏 프로그래밍을 공부해오며 생각해온 방식은 구조적 프로그래밍에 대한 이해를 전제로 해왔다. 그러던 중 멀티스레드를 받아들이려니 도무지 그 복잡도를 따라갈 수가 없었다.
지금은 여러가지 상황에 대한 경험으로 데이터 경합/동시 접근에 대한 문제를 찾는 노하우가 생겼고, 스레드 파이프라인 설계 등으로 문제를 회피하는 노하우등이 생겼을 뿐 여전히 멀티 스레드 코드를 작성하는 과정은 그것에 대한 고려가 없을 때 보다 수십배는 더 어렵다.
집중해야만 잘할 수 있는 일은 실수가 더 많아지기 마련이고, 당연히 골치를 썩고 있음은 물론이다.
성능 최적화에 대한 문제도, 새로운 이해를 요구하고 있다.
애초에 내가 당연시 여기던 많은 것들이 변한 것은, 멀티스레드 때문이다.
전치검사/후치보장을 잘 지켜 문제없이 돌아가던 코드가, 멀티스레드라는 환경으로 인해 무너지는 일은 절대 유쾌한 경험이 아니다.
네트워크 엔진/렌더링 엔진등 성능 최적화를 목표로 하는 경우 특히나 멀티스레드 코딩을 자연스레 하게 되고, 그로 인해 수 많은 문제를 내제하는 코드를 만들게 된다.
이를 디버깅하려면 해당 엔진의 스레딩 구조 (나는 이 것을 위에서처럼 스레드 파이프라인이라 부른다. 렌더링 파이프라인처럼 패킷이 처리되는 일련의 과정을 파이프라인이라 칭하는 것이다) 를 이해해야 하는데, 이 과정이 익숙해지기란 절대 쉽지 않다.
잠금 (파일 접근 잠금, 메모리 잠금 등..), 경합과 양보, 병목 등의 문제를 해결해야 하는 데, 이는 기본 설계 자체가 잠금을 용인하거나, 병목을 해결하기 위한 준비가 되어있지 않은 경우라면 매우 리스크 큰 작업을 해야 함을 의미한다.
이런 과정은 테스트 환경을 세팅하고 작업을 시작한다 해도 꽤나 큰 시간을 소비해야 되는 작업이라, 설계부터 Through-put 을 예측하고 보정하며 개발하는 것이 유리하고, 경합이 일어나지 않도록 스레드 디자인을 하려면 높은 이해도와 고찰이 필요한 작업이라고 밖에 할 수 없다.
특히 병목 지점을 profiling하고, 제거하는 과정은 수많은 테스트 케이스와 자동화 테스트 기반이 필요하고, 설계가 이미 병목 지점을 의도하고 있다면 지나치게 큰 작업을 요구하게 되는 경우도 빈번하다.
사실상 Framework은 안정적으로 돌아갔으면 하는 것이 일반적인 기대치이고, Framework에서 Logic의 멀티스레드화를 자연스럽게 매칭 시킬 수 있도록 (물론 전제 조건이야 있겠지만) 지원이 되면 더할 나위 없을테고, 멀티 프로세싱을 통한 Through-put 향상만 이루어져도 대게는 만족스럽다.
지금이 멀티코어 시대고, 단일 프로세스 내에 퍼포먼스 극대화에 대한 시도를 나도 하고 있고, 몇몇 회사가 시도하고 있지만 (GDC2013의 길드워2에 대한 세션 참고 : http://blog.naver.com/PostView.nhn?blogId=spacesun&logNo=140185766419&redirect=Dlog&widgetTypeCall=true) 소규모 팀 및 회사에서 마저 이런 시도를 당연시 여기고 개발해온 근래까지의 C++ 기반 서버는 내가 봐도 납득이 잘 안간다. (특히 네트웍 라이브러리 자체 개발 같은거 말이지)
그렇지만, 로직에서라도 병목 지점을 profiling해야 되는 것은 어느 규모의 서버나 매한가지다.
이 작업 자체는 기본적인 이해도라 할지라도, 멀티스레드에 모든 프로그래머가 익숙해져야 했던 기존의 C++ 서버 개발 방식은 그다지 좋지 않다.
멀티스레드에 신경써야 되는 일은 소수의 시니어 프로그래머가 해야 하며, 대다수의 주니어 프로그래머 내지는 컨텐츠 프로그래머는 적절한 함수 콜만으로 원하는 동작을 구현해낼 수 있어야만 한다.
멀티스레드에 내가 익숙해지기까지의 시행착오는 쉽지 않았다. 멀티스레드에 신경써야 될 요인들은 그렇게 마냥 간단하지 않기 때문이다.
특히 병목지점 여부를 파악하는 것이나, 설계가 갖춰지지 않은 상태에서 자주 내리는 판단인 잠금 객체 사용 (critical section, semaphore, mutex 등)은 결과적으로 성능 저하를 이끈다.
그래서 나도 다음 서버를 만든다면 C++서버와 웹서버 조합으로 짤 예정이다. (ruby on rails 또는 node.js가 될거라 예상한다.)
C++ 만으로 모든 서버 로직을 개발하며 겪에 되는 여러가지 문제로 소비될 시간에 더 많은 테스트 커버리지와 또 다른 방식에서의 (스케일 아웃등의 매커니즘이 웹서버 기반에서 많은 노하우와 솔루션이 많기에) 최적화를 시도 할 수 있기 때문이다.
꽤나 많은 서버에 포함되는 기능이 웹서버로 빠질 수 있다. 그렇게 되면 서버 프로세스내에 through-put은 저절로 상승하게 된다. Anti packet flooding 같은 기능이 활성화 되어있어, 인증받지 못한 동작은 guard된다는 전제하에서지만 말이다.
현재 만들어지고 있는 꽤나 많은 게임은 connection을 상시 맺는걸 전제로 하지 않아도 된다. 그리고 RESTful API를 사용함으로써 생기는 DB 접근 비용 (캐시 서버를 쓴다 할지라도, 그 자체도 비용일 수 밖에 없다) 문제도, 해쉬를 통해 변화가 없을땐 갱신하지 않는 방법 등으로 줄일 수 있다.
허나 한편으로는 반응성이 중요한 멀티플레이 게임에서는 C++, C#, JAVA등으로 만들어진 TCP connection 기반 서버가 여전히 필요할 수 밖에 없다.
그렇기에 앞으로의 흐름에 맞추고, 효율성을 높이며, 게임의 한계를 줄이기 위해선 더 많은 연구가 고찰이 필요하지 않나 싶다.