TIL - Unreal 네트워크 함수 호출과 RPC 실행 흐름

조범근·2026년 9월 20일

TIL

목록 보기
85/87

0. 개요

클래스 선택 Preview 기능을 구현하면서 기존 SelectCombatClass()와 같은 형태로 Server RPC 호출 코드를 작성했다.

void ADominationPlayerController::SetPreviewCombatClass(
	const EDominationCombatClass RequestedClass)
{
	if (HasAuthority())
	{
		ServerSetPreviewCombatClass_Implementation(RequestedClass);
		return;
	}

	ServerSetPreviewCombatClass(RequestedClass);
}

처음에는 이 코드를 보며 대략 다음과 같이 이해하고 있었다.

클라이언트
→ 일반 함수 호출
→ Server RPC로 서버에 요청
→ 요청이 성공하면 _Implementation() 실행

서버
→ HasAuthority()이므로 RPC를 거치지 않고 직접 실행

하지만 코드를 다시 확인하면서 몇 가지 개념을 서로 섞어서 이해하고 있었다는 것을 알게 됐다.

특히 다음 부분이 헷갈려 있었다.

  • 서버와 클라이언트에 같은 Actor가 존재하면 함수도 양쪽에서 같이 실행되는가?
  • ServerFoo()와 ServerFoo_Implementation()은 각각 무엇인가?
  • _Implementation()은 RPC가 성공했을 때 호출되는 함수인가?
  • HasAuthority() 분기는 Server RPC를 사용하기 위해 반드시 필요한가?
  • 서버에서 Server RPC를 호출하면 다시 네트워크를 거치는가?
  • _Implementation()을 직접 호출하는 것과 표준 RPC 함수를 호출하는 것은 완전히 같은가?

이번 TIL에서는 Unreal의 네트워크 시스템 전체를 다루기보다, 이 코드에서 실제로 헷갈렸던 부분을 중심으로 함수 호출 → Server RPC → Authority → RPC 실행 경로의 관계를 정리한다.


1. 네트워크 환경에서 함수는 어디에서 실행되는가

1-1. 같은 Actor가 존재한다는 것과 같은 함수를 실행한다는 것은 다르다

멀티플레이에서는 서버와 클라이언트에 같은 네트워크 Actor를 나타내는 인스턴스가 각각 존재할 수 있다.

예를 들어 하나의 플레이어 Pawn이 서버에도 있고 클라이언트에도 보일 수 있다.

하지만 서버와 클라이언트가 하나의 C++ 객체를 공유하는 것은 아니다.

각 머신은 자신의 World 안에 별도의 Actor 인스턴스를 가지고 있다.

따라서 클라이언트에서 일반 함수를 호출하면 기본적으로 그 클라이언트의 Actor에서만 실행된다.

Client World

Pawn::Foo()
    ↓
Client의 Pawn에서 Foo() 실행

이 호출이 존재한다고 해서 자동으로 다음처럼 되지는 않는다.

Client Pawn::Foo()
        ↓
Server Pawn::Foo()

이 차이가 네트워크 코드를 이해할 때 중요한 출발점이다.

서버에 존재하는 Actor에서 어떤 코드를 실행하고 싶다면, 일반 함수 호출과 별도로 네트워크를 넘어가기 위한 수단이 필요하다.

그 역할 중 하나가 RPC다.


2. Server RPC는 어떻게 실행되는가

2-1. 일반 함수와 RPC의 차이

일반 C++ 함수는 현재 실행 중인 프로세스의 객체를 호출한다.

반면 RPC(Remote Procedure Call)는 다른 네트워크 머신에서 함수를 실행할 수 있도록 Unreal이 제공하는 호출 방식이다.

예를 들어 다음 함수가 있다.

UFUNCTION(Server, Reliable)
void ServerSetPreviewCombatClass(
	EDominationCombatClass RequestedClass);

Server가 붙어 있으므로 이 RPC의 실행 대상은 서버다.

개념적으로 보면 다음과 같다.

일반 함수
현재 머신의 함수 실행

Server RPC
서버에서 RPC 본문 실행

2-2. ServerFoo()와 _Implementation()

Unreal의 Server RPC는 보통 다음 두 이름을 보게 된다.

ServerSetPreviewCombatClass()

그리고

ServerSetPreviewCombatClass_Implementation()

둘의 책임은 다르다.

ServerSetPreviewCombatClass()
    → Unreal의 RPC 진입점

ServerSetPreviewCombatClass_Implementation()
    → 서버에서 실행할 실제 RPC 본문

UFUNCTION(Server, ...) 선언을 처리하면서 UnrealHeaderTool이 RPC 호출에 필요한 코드를 생성하기 때문에, 우리가 ServerFoo()를 호출하면 단순히 _Implementation()을 직접 호출하는 것과는 다른 경로를 거친다.

개념적으로는 다음과 같다.

ServerFoo()
    ↓
Unreal RPC 처리 경로
    ↓
ServerFoo_Implementation()

2-3. _Implementation()은 성공 콜백이 아니다

여기에서 처음 오해했던 부분이 있었다.

처음에는 다음과 비슷하게 생각했다.

Server RPC 요청
    ↓
요청 성공
    ↓
_Implementation()

하지만 _Implementation()은 게임 요청 성공 이후 호출되는 콜백이 아니다.

Server RPC의 서버 측 실제 함수 본문이다.

따라서 더 정확한 흐름은 다음과 같다.

Client
ServerSetPreviewCombatClass()
        ↓
Server RPC
        ↓
Server
ServerSetPreviewCombatClass_Implementation()
        ↓
실제 게임 로직 실행

즉 _Implementation()이 실행됐다는 사실만으로 클래스 변경이 승인됐다고 볼 수는 없다.


3. RPC 실행과 게임 요청의 성공은 다른 단계다

클라이언트가 Server RPC를 정상적으로 호출했고 서버에서 _Implementation()까지 실행됐다고 해도, 게임 로직에서는 그 요청을 거부할 수 있다.

예를 들어 클래스 Preview 요청의 흐름은 다음처럼 이어진다.

Client
    ↓
Server RPC
    ↓
ServerSetPreviewCombatClass_Implementation()
    ↓
GameMode
    ↓
MatchStartManager
    ↓
PlayerState
    ↓
게임 규칙 검증

서버에서는 이후에 다시 여러 조건을 검사한다.

예를 들면 다음과 같다.

현재 클래스 선택 단계인가?

유효한 참가자인가?

이미 클래스를 확정한 플레이어인가?

해당 클래스가 선택 가능한가?

따라서 다음 세 가지는 같은 의미가 아니다.

RPC를 호출했다.

RPC 본문이 서버에서 실행됐다.

게임 규칙상 요청이 승인됐다.

이들을 분리해서 봐야 한다.

3-1. Reliable의 의미도 요청 승인이 아니다

다음 선언에서

UFUNCTION(Server, Reliable)

Reliable이라는 이름 때문에 요청 성공까지 보장한다고 생각하기 쉽다.

하지만 이것 역시 게임 규칙에 대한 의미가 아니다.

Reliable은 RPC의 네트워크 전달 특성에 대한 설정이다.

따라서

Reliable
≠ 요청 내용이 유효함
≠ 서버가 요청을 승인함

이다.

게임 규칙의 검증은 서버의 게임 로직에서 별도로 이루어져야 한다.


4. HasAuthority()와 Server RPC의 관계

다시 처음 코드로 돌아가 보면 다음과 같은 분기가 존재한다.

void ADominationPlayerController::SetPreviewCombatClass(
	const EDominationCombatClass RequestedClass)
{
	if (HasAuthority())
	{
		ServerSetPreviewCombatClass_Implementation(RequestedClass);
		return;
	}

	ServerSetPreviewCombatClass(RequestedClass);
}

겉으로 보면

서버 → HasAuthority() 분기
클라이언트 → Server RPC

구조이기 때문에 HasAuthority()가 Server RPC를 사용하기 위한 필수 분기처럼 보일 수 있다.

하지만 둘은 서로 다른 역할을 한다.

4-1. HasAuthority()는 무엇을 확인하는가

HasAuthority()는 현재 Actor 인스턴스가 그 Actor의 Authority 역할을 가지고 있는지 확인한다.

일반적인 네트워크 Actor에서는 서버 쪽 Actor가 Authority를 가진다.

따라서

if (HasAuthority())

는 대략

현재 이 Actor 인스턴스가 authoritative instance인가?

를 확인하는 코드다.

반면 Server RPC는

이 RPC의 실행을 서버에서 수행하도록 하는 호출 경로

다.

즉 둘은 애초에 묻는 것이 다르다.

HasAuthority()
→ 지금 실행 중인 Actor의 역할은 무엇인가?

Server RPC
→ 이 RPC는 어느 머신에서 실행되어야 하는가?

4-2. 서버에서도 Server RPC를 호출할 수 있다

여기서 또 하나 오해했던 부분이 있었다.

Server RPC라는 이름 때문에 클라이언트가 서버에게 요청할 때만 호출해야 한다고 생각하기 쉽다.

하지만 서버에서 다음 함수를 호출하는 것도 가능하다.

ServerSetPreviewCombatClass(RequestedClass);

이미 서버에서 Server RPC를 호출한 경우 Unreal은 호출 위치를 판정하고 서버 내부의 Local 실행으로 처리할 수 있다.

즉 개념적으로는 다음과 같다.

Server
    ↓
ServerSetPreviewCombatClass()
    ↓
RPC callspace 판단
    ↓
이미 서버에서 호출됨
    ↓
Local
    ↓
ServerSetPreviewCombatClass_Implementation()

서버가 자기 자신에게 RPC 패킷을 다시 전송하는 구조가 아니다.

따라서 현재 코드에서는 다음처럼 작성해도 기능 결과는 동일하다.

void ADominationPlayerController::SetPreviewCombatClass(
	const EDominationCombatClass RequestedClass)
{
	ServerSetPreviewCombatClass(RequestedClass);
}

클라이언트에서 호출하면 Server RPC 경계를 넘어가고,

서버에서 호출하면 로컬 RPC 실행으로 처리된다.


4-3. 그렇다면 현재 HasAuthority() 분기는 무엇인가

현재 코드에서는 Authority일 때 표준 RPC 함수를 호출하지 않는다.

if (HasAuthority())
{
	ServerSetPreviewCombatClass_Implementation(RequestedClass);
	return;
}

즉 서버에서는

ServerSetPreviewCombatClass()
    ↓
RPC 처리
    ↓
_Implementation()

을 거치지 않고

_Implementation()

을 직접 실행한다.

따라서 이 HasAuthority() 분기는 Server RPC를 성립시키는 필수 장치가 아니다.

현재 코드에서는 이미 Authority라면 RPC dispatch 경로를 건너뛰고 실제 구현으로 직접 들어가는 별도의 로컬 경로라고 보는 것이 더 정확하다.

또한 이 프로젝트에서 확인되는 직접적인 이유는 기존 SelectCombatClass()가 같은 패턴으로 작성되어 있었고, Preview 구현도 그 구조를 따라갔다는 것이다.

이를 일반적으로

Server RPC는 성능 때문에 항상
HasAuthority() → _Implementation()
패턴으로 작성해야 한다.

라고 해석할 근거는 없다.


5. _Implementation() 직접 호출은 무엇을 우회하는가

현재 코드만 보면

ServerSetPreviewCombatClass(RequestedClass);

와

ServerSetPreviewCombatClass_Implementation(RequestedClass);

모두 결국 같은 게임 로직을 실행한다.

그래서 둘이 거의 같은 호출처럼 느껴질 수 있다.

하지만 실제 호출 경로는 다르다.

5-1. 표준 RPC 함수 호출

ServerFoo()
    ↓
Unreal의 RPC 진입 경로
    ↓
Generated RPC 코드
    ↓
ServerFoo_Implementation()

반면 _Implementation()을 직접 호출하면 다음과 같다.

ServerFoo_Implementation()
    ↓
C++ 구현 직접 실행

즉 _Implementation()을 직접 호출한다는 것은 네트워크 전송만 피하는 것이 아니라 Unreal이 만들어 둔 RPC 진입 경로 자체를 우회하는 것이다.


5-2. WithValidation이 존재한다면 차이가 드러난다

현재 Preview RPC에는 WithValidation이 없다.

UFUNCTION(Server, Reliable)
void ServerSetPreviewCombatClass(
	EDominationCombatClass RequestedClass);

따라서 현재 코드에서는 Validation thunk를 생각할 필요가 없다.

하지만 다음과 같은 RPC가 있다고 가정한다.

UFUNCTION(Server, Reliable, WithValidation)
void ServerFoo();

이 경우 표준 RPC 경로에서는 Validation이 먼저 실행된다.

ServerFoo()
    ↓
_Validate()
    ↓
성공
    ↓
_Implementation()

검증에 실패하면 Implementation은 실행되지 않는다.

그런데 C++에서 직접

ServerFoo_Implementation();

을 호출한다면 RPC의 generated 진입 경로를 거치지 않는다.

따라서 _Validate()도 실행되지 않는다.

ServerFoo_Implementation()
    ↓
_Validate() 우회
    ↓
구현 직접 실행

이 때문에 다음 두 호출은 단순히 이름만 다른 동일한 함수라고 생각하면 안 된다.

ServerFoo();
ServerFoo_Implementation();

현재 Preview RPC에는 WithValidation이 없기 때문에 이 차이가 현재 기능에 영향을 주지는 않는다.

또한 WithValidation의 존재 여부와 게임 규칙 검증도 별개의 문제다.

현재처럼 GameMode, Manager, PlayerState에서 수행하는 게임 규칙 검증은 여전히 필요하다.


마무리

이번 코드를 보면서 가장 크게 섞어서 생각했던 것은 함수 실행 위치, RPC, Authority, 요청 성공 여부였다.

각각을 분리하면 다음처럼 정리할 수 있다.

일반 함수 호출
→ 기본적으로 호출이 발생한 머신에서 실행된다.

Server RPC
→ 서버에서 RPC 본문을 실행하기 위한 Unreal의 진입 경로다.

_Implementation()
→ Server RPC의 서버 측 실제 구현이다.

HasAuthority()
→ 현재 Actor 인스턴스가 Authority 역할인지 확인한다.

게임 규칙 검증
→ RPC가 실행된 이후에도 별도로 이루어질 수 있다.

결국 이번에 가장 중요하게 정리한 것은 다음 한 문장이다.

RPC가 서버에서 실행되는 과정과, 서버가 그 요청을 게임 규칙상 받아들이는 과정은 서로 다른 단계다.

그리고 HasAuthority() 역시 Server RPC를 작동시키기 위한 필수 조건이 아니라, 현재 Actor의 Authority 여부를 확인하는 별도의 개념이다.

이 둘을 분리해서 보니 기존 코드에서 왜 HasAuthority()와 _Implementation() 직접 호출이 사용됐는지도 훨씬 명확하게 읽을 수 있었다.

0개의 댓글