깃허브 주소 : CPP20_GameServer
이번에는 앞으로 만들 네트워크 라이브러리에서 사용할 유용한 클래스들을 설계하려고 한다.
그중에서도 스레드 관리를 담당할 ThreadManager와 그에 관련된 모듈 및 구현 파일들을 우선적으로 구성할 예정이다.
우선적으로 CoreGlobal, CoreMacro, CoreTLS, ThreadManager 를 추가할 예정이다.
이 파일들은 기존의 방식처럼 헤더 파일과 구현 파일로 나누는 것이 아니라, 모듈 인터페이스 파일과 모듈 구현 파일의 구조로 작성할 계획이다.
이러한 전환 과정에서 예상보다 많은 문제들이 발생했고, 기존 구조에서 수정해야 할 부분도 많았다.
이제부터 그 과정에서 마주친 문제점들과, 변경이 필요했던 사항들에 대해 차근차근 살펴보도록 하겠다.
먼저 기존에 존재하던 파일들의 구조부터 변경해야 했다.
우리가 앞으로 추가할 모듈들도 CorePch에 export할 예정이지만, 문제는 이미 각각의 파일들이 CorePch를 import하고 있다는 점이다.
이러한 구조에서 다른 프로젝트(예: GameServer)에서 CorePch를 import하게 되면, 각 파일들이 CorePch를 다시 re-import하고 있는 상황이 발생하면서 순환 참조 문제가 일어나게 된다.
이를 해결하기 위해 필자는 CorePch의 서브 모듈을 만들어 구조를 개선하였다.
다음으로 Types 모듈에서도 CorePch.stdx를 import하고, 자주 사용할 기능들을 using으로 선언하였다.
따라서 앞으로 만들 모듈 인터페이스 파일들은 기본적으로 CorePch.stdx와 Types 모듈을 import하게 될 것이다.
CoreTLS 파일은 각 스레드에 고유한 ID를 부여하는 thread_local 변수를 포함하고 있다.
이 변수는 각 스레드를 식별하고 관리하는 데 중요한 역할을 할 것이다.
이 변수를 전역적으로 접근할 수 있도록 선언하면서도, 다른 변수들과의 이름 충돌을 방지하기 위해 extern 키워드와 함께 사용하고 있다.
이제 ThreadManager 파일을 만들어, 각 스레드에게 작업을 부여할 수 있도록 준비하였다.
앞서 만든 CoreTLS를 통해 현재 실행 중인 스레드의 ID를 파악하고, 이를 기반으로 스레드에 적절한 작업을 분배할 수 있도록 구성하였다.
이는 작업을 부여할 대상 스레드의 정체를 명확히 알고 있어야 하기 때문에 반드시 필요한 과정이다.
이제 앞으로 만들 파일들의 실행 시작점을 담당할 CoreGlobal 파일을 설계하였다.
CoreGlobal은 ThreadManager나 기타 Manager 계열의 클래스들, 그리고 추후 추가될 DB 관련 클래스들의 초기화 지점으로 사용될 예정이다.
초기에 구조를 모듈 방식으로 개편한 이유도 바로 이러한 확장성과 유연성을 고려했기 때문이다.
사실 우리가 현재 사용하고 있는 모듈 기반 개발 방식과 매크로는 근본적으로 호환되지 않는다.
특히, 매크로를 모듈 인터페이스 파일에서 선언하고 export하는 행위는 C++20에서 엄격히 금지되는 방식이다.
C++20에서 모듈은 전처리 이후의 토큰을 단위로 저장하는 반면에, 매크로는 선언이 아닌 텍스트 치환 규칙에 의해서 export 대상이 아니다.
그리고 아직은 매크로가 export할 수 있는 문법 또한 존재하지 않아서 관련 매크로를 사용하고 싶어도 미정의 동작 에러가 난다.
이러한 제약으로 인해, 필자는 CoreMacro를 헤더 파일 형태로 유지하고, 모듈을 import하기 전에 반드시 포함되도록 설계를 변경하였다.
CoreMacro.h 파일
해당 헤더에는 CRASH 매크로를 정의하여, 런타임 오류가 발생했을 때 어떤 이유로 문제가 발생했는지를 빠르게 파악할 수 있도록 했다.
기존에는 미리 컴파일된 헤더(PCH)에 CoreMacro를 포함시키고, 이를 CorePch에 추가하면 다른 프로젝트에서도 자동으로 매크로를 사용할 수 있었다.
하지만 현재는 모듈 기반 개발 환경으로 전환했기 때문에, 매크로를 사용하려면 별도의 환경 설정이 추가로 필요하다.
GameServer/DummyClient 속성 -> C/C++ 일반 -> 추가 포함 디렉터리
$(SolutionDir)ServerCore\Main 추가
이렇게 경로를 추가해두면, 각 프로젝트에서 CoreMacro의 매크로 함수들을 문제없이 사용할 수 있다.
이제 지금까지 구성한 모듈들을 실제로 GameServer 프로젝트에서 사용해볼 차례이다.
GameServer.cpp 파일
우선, 매크로가 포함된 디렉터리를 상대 경로로 지정하여 include 디렉터리에 추가해주어야 한다. 그 다음, CoreGlobal 객체를 생성하여 모든 Manager들을 초기화하고, 그 중 일부인 ThreadManager를 실행하면 된다.여기서 한 가지 더 소개할 문법은 C++20에 새롭게 추가된 std::osyncstream이다.
스레드가 동시에 콘솔에 메시지를 출력할 때, 서로의 출력이 뒤섞이거나 개행이 누락되는 문제가 발생할 수 있다.
이러한 문제를 방지하기 위해 osyncstream을 사용하면, 스레드 간 출력이 서로 간섭 없이 안전하게 출력되도록 보장할 수 있다.
실행 결과
위와 같이 각 스레드가 깔끔하게 자신의 ID를 출력하는 결과를 확인할 수 있다. 이제 CoreGlobal을 통해 초기화된 Manager들과, ThreadManager를 통해 실행된 각 스레드들이 유기적으로 동작하는 구조가 완성된 것이다.Inflearn [Rookiss][C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버