왜 프로젝트를 3분할하는가
| 프로젝트 | 책임 | 의존 방향 |
|---|
| ServerCore (정적 라이브러리) | 네트워크, 스레드, 동기화, 유틸 공통 기반 | (가능하면) 다른 앱에 의존하지 않음 |
| Server (실행 앱) | 게임 서버 구동, 로직 조합, 운영 진입점 | Server -> ServerCore |
| DummyClient (테스트 앱) | 부하/접속/프로토콜 검증 | DummyClient -> ServerCore |
분할의 이점
- 공통 코드를 라이브러리로 분리하면 재사용성과 테스트 편의가 높아집니다.
- 서버/더미클라를 독립 실행해 프로토콜/성능 테스트를 빠르게 반복할 수 있습니다.
- 의존 방향이 단순해져 빌드/배포/디버깅이 쉬워집니다.
설계 원칙
ServerCore에는 게임 도메인 의존(맵/몬스터/퀘스트)을 최소화합니다.
- 상위 계층(
Server)이 하위 계층(ServerCore)을 사용하고, 역의존은 피합니다.
ServerCore 빌드/링크 체크리스트
기본 링크 흐름
ServerCore를 빌드해 .lib 생성
Server, DummyClient에서 include/lib path 설정
- Additional Dependencies에
ServerCore.lib 연결
실수 방지 체크
| 항목 | 체크 포인트 |
|---|
| 플랫폼 | x64/x86 일치 |
| 구성 | Debug/Release 라이브러리 혼용 금지 |
| 런타임 라이브러리 | /MD, /MDd 등 CRT 설정 일관성 유지 |
| 출력 경로 | 팀 공통 경로 규칙으로 고정 |
흔한 링크 문제
- LNK2019/LNK2001: 심볼 선언/정의 불일치, 라이브러리 누락
- CRT 충돌: Debug 앱 + Release 라이브러리 혼용
- include는 되는데 링크 실패: 헤더만 보고 구현 라이브러리를 누락한 경우
코어 헤더와 PCH 운영
CorePCH.h 역할
- 자주 쓰는 공통 헤더(타입, 매크로, TLS 선언, STL)를 모아 컴파일 시간을 줄입니다.
PCH.cpp에서 #include "CorePCH.h"를 단일 진입점으로 유지합니다.
헤더 계층 규칙
CoreGlobal.h: 전역 객체 선언(extern)
CoreTLS.h: TLS 변수/도우미 선언
ThreadManager.h: 스레드 관리 API 정의
주의사항
- PCH에 너무 많은 프로젝트 특화 헤더를 넣으면 의존이 비대해집니다.
- "모든 파일에서 무조건 PCH"보다, 계층 경계를 지키는 include 설계가 더 중요합니다.
Thread Local Storage (TLS) 실전
기본 문법
thread_local int32 LThreadId = 0;
thread_local const char* LThreadName = "Unknown";
TLS가 주는 장점
- 스레드마다 독립 저장소를 가지므로 락 없이 접근 가능합니다.
- 스레드 ID, 통계 버퍼, 임시 scratch 메모리 같은 "스레드 전용 상태"에 적합합니다.
TLS 사용 시 함정
- 스레드 풀에서는 스레드가 재사용되므로 TLS 값이 다음 작업에 남을 수 있습니다.
- TLS 객체가 크면 스레드 수만큼 메모리를 점유해 총 사용량이 커집니다.
- TLS 주소를 다른 스레드로 넘기는 패턴은 의미가 어긋나기 쉽습니다.
ThreadManager 설계와 수명 주기
| 메서드 | 역할 |
|---|
Launch(callback) | 스레드 생성 + TLS 초기화 + 콜백 실행 |
InitTLS() | 스레드 ID/이름/진단 컨텍스트 초기화 |
DestroyTLS() | 종료 전 TLS 정리 |
Join() | 모든 워커 종료 대기 및 핸들 정리 |
권장 실행 흐름
- 매니저 생성
- 워커 Launch
- 서비스 실행
- 종료 신호 전파
Join()으로 정상 종료 대기
- 매니저 해제
구현 시 주의점
Join()은 보통 여러 번 호출해도 안전(idempotent)하게 설계하는 편이 운영에 유리합니다.
- 워커 콜백 예외를 처리하지 않으면
std::terminate()로 서버가 종료될 수 있습니다.
- 스레드 ID 할당은 atomic 카운터를 사용해 중복을 방지합니다.
GThreadManager 전역 접근 전략
기본 패턴
extern std::unique_ptr<ThreadManager> GThreadManager;
std::unique_ptr<ThreadManager> GThreadManager;
초기화/종료 순서
main()에서 명시적으로 생성/초기화하고,
- 종료 시
Join() 이후 reset()으로 해제하는 순서를 지키는 것이 안전합니다.
전역 사용의 트레이드오프
- 장점: 접근이 간단하고 도입 비용이 낮음
- 단점: 테스트 대역(mock) 주입이 어려워질 수 있음
- 규모가 커지면 DI(의존성 주입) 기반으로 점진 전환을 고려합니다.
강의 시 유의사항
강조 포인트
- 분할 구조의 핵심은 "파일 나누기"가 아니라 의존 방향 관리입니다.
- TLS는 편리하지만, 스레드 풀 재사용 시 초기화 규칙이 없으면 버그가 누적됩니다.
- ThreadManager의 진짜 역할은 "스레드 생성"보다 수명 관리(시작/종료/정리) 입니다.
자주 하는 오해
| 오해 | 바로잡기 |
|---|
| TLS면 무조건 안전하다 | 데이터 경합은 줄지만 상태 초기화/수명 문제는 별도 관리 필요 |
| 전역 매니저가 있으면 구조가 항상 단순하다 | 초기엔 단순하지만 테스트성과 확장성 비용이 생길 수 있음 |
| Debug/Release lib 혼용해도 대충 돌아간다 | CRT/ABI 충돌로 런타임 문제를 만들 수 있음 |
체크 질문 (스스로 답해보기)
- 왜
ServerCore에 도메인 로직 의존을 최소화해야 하는가?
- 스레드 풀 환경에서 TLS를 사용할 때 어떤 초기화 규칙이 필요한가?
GThreadManager를 안전하게 초기화/해제하는 순서를 설명할 수 있는가?