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가지를 순서대로 확인하면 된다.
- 어떤 클래스의 멤버 함수로 RPC가 정의되었는가
- 해당 클래스의 객체가 어떤 Owning Connection(OwningClient/Server/None)으로 생성되어 있는가
- 그중에서 어떤 컴퓨터에서 호출되고, 어떤 컴퓨터에서 실행되기를 의도하는가
이 세 가지를 순서대로 따라가면, Server/Client/NetMulticast 표 중 어떤 표를 봐야 하는지와 실행 여부(혹은 Dropped 여부)를 바로 판단할 수 있다.
'학습 > Unreal' 카테고리의 다른 글
| Property Replication (기초) (0) | 2026.06.22 |
|---|---|
| 서버를 거치는 통신 구조 (0) | 2026.06.22 |
| NetRole (Authority, Proxy, 로컬/리모트 롤) (0) | 2026.06.19 |
| NetMode, NetConnection, NetDriver, Ownership (0) | 2026.06.18 |
| Dedicated Server, 서버-클라이언트 구조의 핵심 특징 (0) | 2026.06.17 |
