프로젝트 구조와 TLS

Jaemyeong Lee·2025년 1월 5일

게임 서버1

목록 보기
119/220

왜 프로젝트를 3분할하는가

프로젝트책임의존 방향
ServerCore (정적 라이브러리)네트워크, 스레드, 동기화, 유틸 공통 기반(가능하면) 다른 앱에 의존하지 않음
Server (실행 앱)게임 서버 구동, 로직 조합, 운영 진입점Server -> ServerCore
DummyClient (테스트 앱)부하/접속/프로토콜 검증DummyClient -> ServerCore

분할의 이점

  • 공통 코드를 라이브러리로 분리하면 재사용성과 테스트 편의가 높아집니다.
  • 서버/더미클라를 독립 실행해 프로토콜/성능 테스트를 빠르게 반복할 수 있습니다.
  • 의존 방향이 단순해져 빌드/배포/디버깅이 쉬워집니다.

설계 원칙

  • ServerCore에는 게임 도메인 의존(맵/몬스터/퀘스트)을 최소화합니다.
  • 상위 계층(Server)이 하위 계층(ServerCore)을 사용하고, 역의존은 피합니다.

ServerCore 빌드/링크 체크리스트

기본 링크 흐름

  1. ServerCore를 빌드해 .lib 생성
  2. Server, DummyClient에서 include/lib path 설정
  3. 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()모든 워커 종료 대기 및 핸들 정리

권장 실행 흐름

  1. 매니저 생성
  2. 워커 Launch
  3. 서비스 실행
  4. 종료 신호 전파
  5. Join()으로 정상 종료 대기
  6. 매니저 해제

구현 시 주의점

  • Join()은 보통 여러 번 호출해도 안전(idempotent)하게 설계하는 편이 운영에 유리합니다.
  • 워커 콜백 예외를 처리하지 않으면 std::terminate()로 서버가 종료될 수 있습니다.
  • 스레드 ID 할당은 atomic 카운터를 사용해 중복을 방지합니다.

GThreadManager 전역 접근 전략

기본 패턴

// CoreGlobal.h
extern std::unique_ptr<ThreadManager> GThreadManager;

// CoreGlobal.cpp
std::unique_ptr<ThreadManager> GThreadManager;

초기화/종료 순서

  • main()에서 명시적으로 생성/초기화하고,
  • 종료 시 Join() 이후 reset()으로 해제하는 순서를 지키는 것이 안전합니다.

전역 사용의 트레이드오프

  • 장점: 접근이 간단하고 도입 비용이 낮음
  • 단점: 테스트 대역(mock) 주입이 어려워질 수 있음
  • 규모가 커지면 DI(의존성 주입) 기반으로 점진 전환을 고려합니다.

강의 시 유의사항

강조 포인트

  • 분할 구조의 핵심은 "파일 나누기"가 아니라 의존 방향 관리입니다.
  • TLS는 편리하지만, 스레드 풀 재사용 시 초기화 규칙이 없으면 버그가 누적됩니다.
  • ThreadManager의 진짜 역할은 "스레드 생성"보다 수명 관리(시작/종료/정리) 입니다.

자주 하는 오해

오해바로잡기
TLS면 무조건 안전하다데이터 경합은 줄지만 상태 초기화/수명 문제는 별도 관리 필요
전역 매니저가 있으면 구조가 항상 단순하다초기엔 단순하지만 테스트성과 확장성 비용이 생길 수 있음
Debug/Release lib 혼용해도 대충 돌아간다CRT/ABI 충돌로 런타임 문제를 만들 수 있음

체크 질문 (스스로 답해보기)

  • ServerCore에 도메인 로직 의존을 최소화해야 하는가?
  • 스레드 풀 환경에서 TLS를 사용할 때 어떤 초기화 규칙이 필요한가?
  • GThreadManager를 안전하게 초기화/해제하는 순서를 설명할 수 있는가?

profile
李家네_공부방

0개의 댓글