RPC는 언제 쓰는가

RPC는 "호출하는 컴퓨터와 실제로 실행되는 컴퓨터가 다를 수 있는 함수 호출"이다.

언리얼 멀티플레이에서는 주로 게임 결과에 큰 영향을 주지 않는 일시적 효과(사운드, 파티클 등 코스메틱)에 사용한다.

데미지, 충돌, 스폰처럼 게임 진행에 직접적인 영향을 주는 로직은 RPC가 아니라 프로퍼티 레플리케이션을 사용하는 것이 원칙이다.

이 글의 목표는 RPC를 다루는 3개의 핵심 표(Server RPC, Client RPC, NetMulticast RPC)를 완전히 이해하는 것이다.


1. Call vs Invoke : RPC가 간접 호출인 이유

함수를 다루는 두 가지 방식이 있다.

구분 의미 결정 시점 예시
Call 직접적(Direct) 호출 컴파일 타임 일반 전역 함수, 멤버 함수
Invoke 간접적(Indirect) 호출 런타임 함수 포인터, 동적 바인딩, RPC

 

일반 함수는 코드를 작성하는 시점에 "누가 어디서 실행하는지"가 고정된다(Call).

반면 RPC는 컴파일 타임에는 호출 위치만 정해져 있을 뿐, 실제 실행 위치는 런타임에 네트워크 시스템이 결정한다.

그래서 RPC는 Invoke로 분류된다.

주의할 점: 클라이언트가 호출한 Client RPC가 결과적으로 같은 클라이언트에서 실행되는 경우에도 이는 Call이 아니라 Invoke다. "결과 위치가 같다"는 것과 "호출-실행 결정 메커니즘이 직접적이다"는 것은 다른 문제다. RPC는 항상 런타임 디스패치 시스템을 거치기 때문에 메커니즘 자체가 간접적이다.

 

2. Owning Connection : 액터는 누구의 것인가

RPC 표의 "실행 여부"를 결정하는 가장 중요한 축은 액터의 소유 관계다.

멀티플레이에서 액터는 서버에서 스폰되고 클라이언트로 복제된다는 전제하에, 소유 관계를 다음 5가지로 분류한다.

분류 조건  예시
OwningClient SetOwner(PlayerController)가 호출된 액터 플레이어 캐릭터(Pawn)
Server SetOwner() 호출 없이 서버가 명시적으로 관리 GameMode, GameState
None SetOwner() 호출 없이 레벨에 그냥 배치된 액터 나무, 바위, 문
Invoking Client 그 액터의 Owner == 지금 RPC를 호출 중인 컨트롤러 (GetController() == LocalController) 내 캐릭터를 내가 호출
Different Client 그 액터의 Owner == 다른 컨트롤러 (GetController() != LocalController) 남의 캐릭터를 내가 호출

 

핵심은 OwningClient/Server/None은 액터의 고정된 소유 상태이고, Invoking Client/Different Client는 "지금 누가 RPC를 호출하느냐" 기준으로 상대적으로 결정된다는 점이다.

같은 액터라도 호출자가 바뀌면 Invoking Client였다가 Different Client가 될 수 있다.

MyPawn->SetOwner(MyPlayerController);

if (Actor->GetOwner() == LocalController)
{
    // Invoking Client 케이스
}

 

3. NetMulticast / Server / Client 키워드

UFUNCTION() 매크로에 작성하는 이 키워드들은 RPC가 어디서 실행되어야 하는지를 선언한다.

키워드  호출 가능 위치 실행 위치
NetMulticast 서버에서만 의미 있음 서버 + 관련 있는(relevant) 모든 클라
Server 클라(자기 소유 액터)에서 서버
Client 서버에서 그 액터를 소유한 특정 클라

 

4. 3대 핵심 표 분석

 

4-1. Server RPC

호출(Calling) 소유(Owning) 실행(Executing)
Server Client / Server / None Server
Client Invoking Client (RPC 실행한 애) Server
Client Different Client Dropped
Client Server Dropped ❌
Client None Dropped ❌

 

Server RPC는 "내가 소유한 액터에서, 내(클라)가 호출했을 때만" 서버에서 실행된다.

이건 보안 문제. 만약 "Different Client" 케이스를 허용한다면, 악의적인 클라이언트가 자기 캐릭터가 아닌 남의 캐릭터를 사칭해서 서버에 부정확한 요청을 보낼 수 있다. 그래서 언리얼은 "네가 소유한 액터에 대해서만 서버에 요청할 수 있다"는 규칙으로 원천 차단한다. 반대로 서버가 직접 호출할 때는 소유권 검사 없이 항상 실행되는데, 이는 서버가 유일하게 신뢰되는 컴퓨터이기 때문이다. 클라이언트는 패킷 조작이나 메모리 수정이 가능한 위험 지대로 취급되지만, 서버는 위변조될 수 없다고 가정한다.

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

MyCharacter->ServerFire();        // OK — 내 소유 액터
OtherCharacter->ServerFire();     // 컴파일은 되지만 런타임에 Dropped

 

4-2. Client RPC

호출(Calling) 소유(Owning) 실행(Executing)
Server Owning Client Owning Client
Server Server / None Server
Client Invoking / Different / Server / None Invoking Client (항상)

 

Client RPC는 Dropped가 하나도 없다. 호출한 그 컴퓨터에서 무조건 실행된다.

클라가 다른 사람 소유의 액터를 대상으로 Client RPC를 호출해도, 실행은 항상 호출한 자기 자신에게 일어난다(다른 사람에게 가지 않는다)

 

대표 용도: 서버만 아는 정보를 특정 클라이언트 한 명에게만 전달할 때 사용한다.

UFUNCTION(Client, Reliable)
void ClientShowDamageNumber(float Damage);

// 서버 코드: 호출은 서버가 하지만, 실행은 그 액터를 소유한 클라에서 일어남
DamagedCharacter->ClientShowDamageNumber(50.f);

 

4-3. NetMulticast RPC

호출(Calling) 소유(Owning) 실행(Executing)
Server Client / Server / None 서버 + 관련 있는 모든 클라
Client Invoking / Different / Server / None Invoking Client (자기 자신만)

 

NetMulticast가 진짜로 모두에게 전달되려면 반드시 서버가 호출해야 한다.

클라가 호출하면 자기 자신에게만 실행되는 로컬 동작에 그친다.

 

"relevant for(관련 있는)"의 의미: 서버가 호출해도 전 세계 모든 클라에게 가는 게 아니라, 그 액터를 실제로 복제받고 있는(렐레번트한) 클라에게만 전달된다.

 

부하 주의: Server/Client RPC는 1:1이지만 NetMulticast는 1:N이다. 클라 수에 비례해 부하가 늘어나므로, 자주 호출하거나 Reliable과 함께 쓰는 것은 권장되지 않는다.

UFUNCTION(NetMulticast, Unreliable)
void MulticastPlayExplosionEffect();

if (HasAuthority())
{
    MulticastPlayExplosionEffect(); // 서버 + 관련 클라 전체에서 재생
}

 

4-4. 세 표 비교 요약

  클라 호출 시 제약 실행 범위
Server RPC 자기 소유 액터만 (아니면 Dropped) 서버 1곳
Client RPC 제약 없음 호출한 컴퓨터 1곳
NetMulticast RPC 제약 없음 서버 호출 시 다수 / 클라 호출 시 자기 자신만

 

공통 핵심: 클라가 호출하면 결과는 항상 "호출한 자기 자신" 혹은 "거부(Dropped)"이며, 절대 다른 클라에게 직접 전달되지 않는다. 다른 클라에게 정상적으로 전달되려면 무조건 서버를 거쳐야 한다.

 

5. WithValidation : 클라 요청에 대한 검증 장치

WithValidation을 붙이면 함수가 두 개로 분리된다.

  • _Implementation(): 실제 로직
  • _Validate(): "이 RPC를 실행해도 되는가?"를 검사 (bool 반환, false면 실행되지 않고 해당 클라가 연결 종료될 수도 있음)

왜 Server RPC에만 주로 쓰는가: 서버는 무조건 신뢰되지만 클라는 위변조 가능한 위험 지대이므로, 클라 → 서버 방향(Server RPC)의 요청만 추가 검증이 필요하다.

UFUNCTION(Server, Reliable, WithValidation)
void ServerFire(FVector AimDirection);

void AMyCharacter::ServerFire_Implementation(FVector AimDirection)
{
    // 실제 발사 로직
}

bool AMyCharacter::ServerFire_Validate(FVector AimDirection)
{
    return AimDirection.IsNormalized(); // false면 실행 자체가 안 됨
}

6. Unreliable vs Reliable

  보장 여부 용도 예시
Unreliable (기본값) 도착 보장 없음 게임에 큰 영향이 없는 로직 발자국 소리, 파티클 이펙트
Reliable 도착 보장됨 게임에 큰 영향을 주는 로직 충돌, 데미지, 스폰

 

NetMulticast + Reliable 조합 주의: NetMulticast는 이미 1:N이라 부하가 큰데, Reliable까지 더하면 N명 전체에 대해 도착을 보장(재전송 포함)해야 하므로 부하가 배로 커진다. 그래서 이 조합은 "자주 일어나지 않지만 놓치면 치명적인" 이벤트(예: 캐릭터 죽음 애니메이션)에만 제한적으로 사용해야 한다.

// 캐릭터 죽음 — 모두가 봐야 하고(NetMulticast), 절대 놓치면 안 됨(Reliable)
// 자주 발생하지 않는 이벤트이므로 부하 비용을 감수할 가치가 있음
UFUNCTION(NetMulticast, Reliable)
void MulticastOnCharacterDeath();

7. RPC 분석 시 체크포인트 (요약)

RPC 관련 코드를 마주쳤을 때 다음 3가지를 순서대로 확인하면 된다.

  1. 어떤 클래스의 멤버 함수로 RPC가 정의되었는가
  2. 해당 클래스의 객체가 어떤 Owning Connection(OwningClient/Server/None)으로 생성되어 있는가
  3. 그중에서 어떤 컴퓨터에서 호출되고, 어떤 컴퓨터에서 실행되기를 의도하는가

이 세 가지를 순서대로 따라가면, Server/Client/NetMulticast 표 중 어떤 표를 봐야 하는지와 실행 여부(혹은 Dropped 여부)를 바로 판단할 수 있다.

+ Recent posts