[DAY68] Network Lifecycle : Log & Debugging

베리투스·2025년 11월 21일

TIL: Today I Learned

목록 보기
58/93
post-thumbnail

멀티플레이 개발을 하다 보면 서버와 클라이언트의 로그가 뒤섞여서 대체 누가 실행한 코드인지, 어떤 순서로 호출되는지 알기 어려울 때가 많다.
오늘은 서버/클라이언트를 구분하는 커스텀 로그 매크로를 만들고, 실제 로그를 한 줄 한 줄 뜯어보며 '서버 시작 -> 로그인 -> 빙의 -> 클라이언트 동기화'로 이어지는 거대한 생명주기(Lifecycle)를 완전 정복했다.


📌 오늘의 목표

  • 서버([Server])와 클라이언트([Client])를 구분하는 디버그 매크로 작성
  • 로그인(Login) \rightarrow 빙의(Possess) \rightarrow 복제(Replication)의 정확한 호출 순서 파악
  • OnRep_ReplicatedHasBegunPlay가 클라이언트의 BeginPlay를 유발하는 원리 이해

📚이론 및 원리

1. 로그가 섞인다? 커스텀 매크로 만들기

언리얼 에디터에서 "Run Under One Process"를 켜면 출력 로그 창에 서버와 클라이언트 로그가 뒤죽박죽 섞여 나온다.
이때 GetNetMode() 함수를 이용해 현재 코드가 실행되는 곳이 서버인지 클라이언트인지 판별하는 매크로를 만들면 디버깅이 훨씬 쾌적해진다.

// [DedicatedX.h]
// NetMode를 확인해서 로그 앞에 [Server], [Client01] 등의 머리말을 붙여준다.
#define NETMODE_TCHAR ((GetNetMode() == ENetMode::NM_Client) ? *FString::Printf(TEXT("Client%02d"), UE::GetPlayInEditorID()) : ((GetNetMode() == ENetMode::NM_Standalone) ? TEXT("StandAlone") : TEXT("Server")))

// 실제 코드에서 사용할 매크로 (함수 이름까지 찍어줌)
#define DX_LOG_NET(LogCategory, Verbosity, Format, ...) UE_LOG(LogCategory, Verbosity, TEXT("[%s] %s %s"), NETMODE_TCHAR, *FString(ANSI_TO_TCHAR(__FUNCTION__)), *FString::Printf(Format, ##__VA_ARGS__))

🕵️‍♂️ [실습] 로그로 보는 완벽한 라이프사이클

매크로를 적용하고 게임을 실행하니, 그동안 안 보였던 엔진의 '진짜 순서'가 보였다. 스크린샷에 찍힌 로그를 5단계로 나누어 분석해 보았다.

1단계: 서버의 기상 (Server Start)

게임 모드가 생성되고 StartPlay를 외치자, GameState가 받아서 게임 시작을 알린다.

[Server] ADXGameModeBase::ADXGameModeBase
[Server] ADXGameStateBase::ADXGameStateBase
[Server] ADXGameModeBase::StartPlay Begin
[Server] ADXGameStateBase::HandleBeginPlay Begin
[Server] ADXGameStateBase::HandleBeginPlay End
[Server] ADXGameModeBase::StartPlay End

핵심: StartPlay가 끝나야 비로소 게임이 '플레이 가능한 상태'가 된다. 만약 Super::StartPlay()를 빼먹으면 아무 일도 일어나지 않는다.

2단계: 접속 요청과 수락 (Login Process)

클라이언트가 접속을 시도하면 PreLogin(입장권 검사)과 Login(입장 처리)이 실행된다. 이때 서버는 클라이언트를 대변할 PlayerController를 먼저 생성한다.

[Server] ADXGameModeBase::PreLogin Begin/End
[Server] ADXGameModeBase::Login Begin
[Server] ADXPlayerController::ADXPlayerController  <-- 서버에 PC 생성!
[Server] ADXPlayerController::PostInitializeComponents Begin/End
[Server] ADXPlayerController::BeginPlay Begin/End
[Server] ADXGameModeBase::Login End

3단계: 영혼 주입 (Possession & Role Change) 🔥중요

로그인이 끝나면(PostLogin), 서버는 캐릭터(Pawn)를 만들고 컨트롤러에게 "이거 네 거야"라며 빙의(Possess)시킨다.
이때 로그를 자세히 보면 역할(Role)의 변화가 일어나는 것을 알 수 있다.

[Server] ADXGameModeBase::PostLogin Begin
[Server] ADXPlayerCharacter::ADXPlayerCharacter    <-- 캐릭터 생성
[Server] ADXPlayerController::OnPossess Begin

// 1. 빙의 시작: 아직 주인이 없다. (Role: SimulatedProxy)
[Server][ROLE_Authority/ROLE_SimulatedProxy] ADXPlayerCharacter::PossessedBy Begin 
[Server] ADXPlayerCharacter::PossessedBy There is no OwnerActor.

// 2. 주인 설정 완료: 이제 이 캐릭터는 특정 PC의 소유다.
[Server] ADXPlayerCharacter::PossessedBy OwnerActor Name: BP_PlayerController_C_0

// 3. 빙의 끝: 역할이 변경됨! (RemoteRole: Simulated -> Autonomous)
[Server][ROLE_Authority/ROLE_AutonomousProxy] ADXPlayerCharacter::PossessedBy End

[Server] ADXPlayerController::OnPossess End
[Server] ADXGameModeBase::PostLogin End

💡 왜 서버 로그에 AutonomousProxy가 뜰까?
로그 포맷이 [LocalRole / RemoteRole]이기 때문이다.
SetOwner가 실행되면서 서버는 "이 폰의 원격 역할(RemoteRole)은 이제부터 AutonomousProxy(주체)다!" 라고 선언한다. 이 정보가 잠시 후 클라이언트로 복제되면, 클라이언트는 비로소 키보드로 캐릭터를 움직일 수 있게 된다.

4단계: 클라이언트의 기상 (Client Initialization)

이제 클라이언트 쪽 로그([Client01])가 찍히기 시작한다. 서버와 연결(PostNetInit)되면서 통신 채널(Channel)이 열린다.

[Client01] ADXPlayerController::ADXPlayerController <-- 내 PC 생성
[Client01] ADXPlayerController::OnActorChannelOpen Begin/End
[Client01] ADXPlayerController::PostNetInit Begin
[Client01] ADXPlayerController::PostNetInit Server Connection: IpConnection_0
[Client01] ADXPlayerController::PostNetInit End

5단계: 동기화 및 게임 시작 (Replication & BeginPlay)

서버가 만든 캐릭터와 GameState가 클라이언트로 복제(Replication)되어 넘어온다.

[Client01] ADXPlayerCharacter::ADXPlayerCharacter   <-- 캐릭터 복제됨
[Client01] ADXPlayerCharacter::OnRep_Owner Begin    <-- 주인 정보 도착
[Client01] ADXPlayerCharacter::OnRep_Owner OwnerActor Name: BP_PlayerController_C_0
[Client01] ADXPlayerCharacter::OnRep_Owner End

[Client01] ADXGameStateBase::ADXGameStateBase       <-- GameState 복제됨
[Client01] ADXGameStateBase::OnRep_ReplicatedHasBegunPlay Begin <-- 게임 시작 신호!
[Client01] ADXPlayerController::BeginPlay Begin     <-- 클라 BeginPlay 실행
[Client01] ADXPlayerController::BeginPlay End

💡 깨달음: 클라이언트의 BeginPlay는 그냥 실행되는 게 아니다. 서버에서 넘어온 GameState"야, 서버 시작했대! 너네도 시작해!"라고 알려주는 OnRep_ReplicatedHasBegunPlay가 실행되어야 비로소 트리거 된다.


✅ 핵심 요약

순서단계주요 함수/이벤트비고
1서버 시작StartPlay \rightarrow HandleBeginPlay게임 규칙 로딩 완료
2로그인PreLogin \rightarrow Login서버에 플레이어 컨트롤러 생성
3빙의 (Server)Possess \rightarrow PossessedByRemoteRole 변경 (Simulated \rightarrow Autonomous)
4클라이언트 접속PostNetInit \rightarrow OnActorChannelOpen서버와의 연결 수립
5동기화 (Client)OnRep_Owner"이 캐릭터 내 거네?" (소유권 인식)
6클라 시작OnRep_ReplicatedHasBegunPlay이게 실행돼야 클라 BeginPlay가 호출됨
profile
Shin Ji Yong // Unreal Engine 5 공부중입니다~

0개의 댓글