본문 바로가기

RPC와 서버-클라이언트 통신

Server·Client·NetMulticast RPC의 실행 위치와 Reliable 여부를 구분해 액션과 결과를 전달합니다.

플레이어가 마우스를 클릭해 총을 쏘는 순간을 생각해 봅시다. 클릭은 클라이언트에서 일어나지만, 탄약을 줄이고 적에게 피해를 주는 결정은 서버가 해야 합니다. 서버가 결정한 결과 가운데 "탄약이 부족합니다" 같은 메시지는 쏜 사람에게만, 폭발 효과는 근처의 모든 플레이어에게 보여야 합니다. 변수 복제는 상태를 맞추는 데는 알맞지만, 이렇게 "지금 이 일을 해 달라"는 요청과 "이 사람에게만 알려 달라"는 알림은 함수 호출을 보내는 RPC가 맡습니다.

복제(Replication) 시스템 이해에서 상태 복제를 보았으니, 이 글에서는 이벤트를 전달하는 RPC를 자세히 다룹니다. Server RPC는 클라이언트의 요청을 서버의 권한으로 검증하고, Client RPC와 NetMulticast RPC는 서버가 결정한 결과를 대상 클라이언트로 전합니다. 각 RPC의 동작과 구현 방법, 그리고 호출 위치에 따라 실행 대상이 어떻게 달라지는지 살펴봅니다.


RPC란 무엇인가

RPC(Remote Procedure Call)는 네트워크의 다른 컴퓨터에 있는 함수를 로컬 함수처럼 부를 수 있게 해 주는 기술입니다. 언리얼 네트워킹에서는 이를 이용해 클라이언트가 서버의 함수를 부르거나, 서버가 클라이언트의 함수를 부르게 합니다. RPC는 UFUNCTION() 매크로에 특정 지정자를 붙여 선언합니다.

변수 복제가 상태를 맞춘다면 RPC는 호출을 전달합니다. RPC도 네트워크 지연을 겪으며, 나중에 접속한 클라이언트에게 지나간 호출을 다시 재생해 주지 않는다는 점이 상태 복제와 다릅니다.


Server RPC: 클라이언트의 요청

Server RPC는 클라이언트가 서버에 함수 실행을 요청할 때 씁니다. 총 발사, 스킬 사용, 아이템 줍기처럼 플레이어 입력에 대한 게임 로직은 대부분 서버의 권한으로 확정되어야 하므로, 클라이언트는 자신의 입력을 Server RPC로 서버에 전달합니다.

Server RPC는 UFUNCTION()에 Server 지정자를 붙이고, 전송 방식으로 Reliable이나 Unreliable을 고른 뒤, 서버에서 입력과 게임 규칙을 어떻게 검증할지 설계해 만듭니다. WithValidation은 선택 사항이며, 검증에 실패했을 때 연결을 끊는 것이 알맞은 경우에만 씁니다.

MyPlayerCharacter.h (ACharacter 상속)
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "MyPlayerCharacter.generated.h"

UCLASS()
class MYPROJECT_API AMyPlayerCharacter : public ACharacter
{
    GENERATED_BODY()

public:
    AMyPlayerCharacter();

protected:
    virtual void BeginPlay() override;

    // 플레이어가 총을 발사했을 때 서버에게 알리는 RPC
    UFUNCTION(Server, Reliable, WithValidation)
    void Server_ShootWeapon(FVector MuzzleLocation, FRotator MuzzleRotation);

    // Server_ShootWeapon_Validate 함수 선언 (반드시 bool 반환)
    bool Server_ShootWeapon_Validate(FVector MuzzleLocation, FRotator MuzzleRotation);

    // Server_ShootWeapon_Implementation 함수 선언 (실제 로직 구현)
    void Server_ShootWeapon_Implementation(FVector MuzzleLocation, FRotator MuzzleRotation);

public:
    // 클라이언트에서 이 함수를 호출하여 Server_ShootWeapon RPC를 트리거합니다.
    UFUNCTION(BlueprintCallable, Category = "Combat")
    void Client_RequestShoot(FVector MuzzleLocation, FRotator MuzzleRotation);
};
MyPlayerCharacter.cpp
#include "MyPlayerCharacter.h"
#include "Net/UnrealNetwork.h" // RPC 구현을 위해 필요

AMyPlayerCharacter::AMyPlayerCharacter()
{
    bReplicates = true; // 캐릭터는 항상 복제되어야 합니다.
    SetReplicateMovement(true); // 캐릭터 이동도 복제
}

void AMyPlayerCharacter::BeginPlay()
{
    Super::BeginPlay();

    if (HasAuthority()) // 서버
    {
        UE_LOG(LogTemp, Warning, TEXT("Server: MyPlayerCharacter spawned."));
    }
    else // 클라이언트
    {
        UE_LOG(LogTemp, Warning, TEXT("Client: MyPlayerCharacter spawned."));
    }
}

// ===========================================
// Server_ShootWeapon RPC 구현
// ===========================================

// _Validate 함수: 클라이언트에서 받은 인자의 유효성을 서버에서 검증합니다.
// false를 반환하면 _Implementation을 실행하지 않고 호출한 클라이언트의 연결을 끊습니다.
bool AMyPlayerCharacter::Server_ShootWeapon_Validate(FVector MuzzleLocation, FRotator MuzzleRotation)
{
    // 정상적인 재사용 대기시간 거절은 _Implementation에서 처리합니다.
    // 이 예제에는 발사 빈도·탄약 검증이 아직 없습니다.
    if (MuzzleLocation.ContainsNaN() || MuzzleRotation.ContainsNaN())
    {
        return false; // NaN/Inf 인자를 거부합니다.
    }

    // 예시: 발사 위치가 캐릭터에서 너무 멀리 떨어져 있는지 검사
    if (FVector::DistSquared(GetActorLocation(), MuzzleLocation) > FMath::Square(500.0f)) // 500cm 이내
    {
        UE_LOG(LogTemp, Warning, TEXT("Server_ShootWeapon_Validate: MuzzleLocation too far from actor!"));
        return false;
    }

    return true; // 인자가 유효하면 true 반환
}

// _Implementation 함수: 실제 총 발사 로직 (서버에서만 실행)
void AMyPlayerCharacter::Server_ShootWeapon_Implementation(FVector MuzzleLocation, FRotator MuzzleRotation)
{
    // 이 코드는 서버에서만 실행됩니다. (HasAuthority() == true)
    UE_LOG(LogTemp, Warning, TEXT("Server: Received ShootWeapon RPC. Processing shot from %s"), *MuzzleLocation.ToString());

    // 1. 탄약 감소 (서버에서만)
    // CurrentAmmo--;

    // 2. 실제 투사체 스폰 (서버에서만)
    // AProjectile* NewProjectile = GetWorld()->SpawnActor<AProjectile>(ProjectileClass, MuzzleLocation, MuzzleRotation);
    // if (NewProjectile)
    // {
    //     // NewProjectile이 bReplicates=true로 설정되어 있으면 자동으로 클라이언트에게 복제됩니다.
    // }

    // 3. 데미지 계산 및 적용 (서버에서만)
    // Target->ApplyDamage(DamageAmount);

    // 4. 모든 클라이언트에게 시각/청각 효과 재생 명령 (NetMulticast RPC 호출)
    // Multicast_PlayMuzzleEffectAndSound(MuzzleLocation);
}

// 클라이언트에서 호출하여 서버 RPC를 트리거하는 함수
void AMyPlayerCharacter::Client_RequestShoot(FVector MuzzleLocation, FRotator MuzzleRotation)
{
    // 원격 클라이언트와 리슨 서버의 로컬 플레이어 입력을 모두 허용합니다.
    if (IsLocallyControlled())
    {
        UE_LOG(LogTemp, Warning, TEXT("Client: Requesting Server_ShootWeapon."));
        Server_ShootWeapon(MuzzleLocation, MuzzleRotation); // 서버 RPC 호출
    }
    // 클라이언트에서 즉각적인 피드백 (예측)
    // PlayLocalMuzzleFlash();
    // PlayLocalSound();
}

클라이언트는 Client_RequestShoot을 부르고, 이 함수는 로컬에서 조종하는 캐릭터일 때만 Server_ShootWeapon으로 서버에 요청합니다. IsLocallyControlled()로 확인하므로 원격 클라이언트뿐 아니라 리슨 서버의 로컬 플레이어 입력도 받아들입니다. 이름에 Client_가 붙었지만 RPC가 아니라 일반 함수라는 점에 주의합니다. 서버에 도착한 요청은 먼저 _Validate에서 검사됩니다. 이 예제는 위치나 회전에 NaN이나 무한대 값이 있으면 거절하고, 발사 위치가 캐릭터에서 500cm보다 멀면 거절합니다. 검사를 통과하면 _Implementation이 실행되는데, 이 예제에서 실제로 하는 일은 로그를 남기는 것뿐이고 탄약 감소, 투사체 스폰, 피해 적용, 효과 재생은 모두 주석입니다.

거리 검사의 기준값을 계산해 봅시다. 서버에서 캐릭터의 위치가 (0, 0, 0)cm이고, 유한한 MuzzleLocation이 (300, 400, 0)cm라면 거리의 제곱은 300²+400² = 250,000cm²이고, 이는 500² = 250,000과 같습니다. 검사는 >로 비교하므로 정확히 500cm인 이 값은 거리 조건으로 거절되지 않습니다. 다만 _Validate를 통과했다고 탄약, 재사용 대기 시간, 조준의 타당성까지 승인된 것은 아닙니다. 이 예제는 그런 검사를 아직 하지 않습니다.

_Validate와 _Implementation

WithValidation을 붙인 Server RPC는 반드시 bool FunctionName_Validate(Args) 형식의 _Validate 함수를 구현해야 합니다. 이 함수는 RPC가 서버에 도착했을 때 가장 먼저 실행되어, 클라이언트가 보낸 인자가 유효한지, 조작된 값이 아닌지를 검사합니다. false를 돌려주면 _Implementation은 실행되지 않고 호출한 클라이언트의 연결이 끊깁니다. 그래서 _Validate는 정상적인 클라이언트라면 보낼 수 없는 값을 걸러 내는 첫 방어선으로 씁니다.

_Implementation 함수(void FunctionName_Implementation(Args))는 _Validate가 true를 돌려주었을 때 실제로 RPC의 로직이 실행되는 함수입니다. 생성된 RPC 진입점인 Server_ShootWeapon을 거쳐 전달된 호출은 서버에서 실행됩니다. 반면 _Implementation을 직접 부르면 일반 C++ 호출이 되어, 네트워크 전달과 검증 경로를 모두 건너뜁니다.

Reliable과 Unreliable

Reliable은 유효한 연결에서 확인 응답과 재전송으로 RPC의 전달을 추적합니다. 패킷이 손실되면 다시 보내므로 안정적이지만, 대역폭을 더 쓰고 지연이 생길 수 있습니다. 연결이 끊긴 뒤의 전달이나 재생, 그 호출이 게임 규칙상 성공하는지까지 보장하지는 않습니다. 피해, 아이템 획득, 퀘스트 갱신처럼 중요한 게임 로직에 씁니다.

Unreliable은 호출이 전달될 수도 있고 안 될 수도 있습니다. 패킷이 손실되어도 다시 보내지 않아 빠르지만 데이터가 사라질 수 있으므로, 캐릭터 애니메이션 동기화나 짧은 시각 효과처럼 자주 일어나고 하나쯤 놓쳐도 되는 실시간 갱신에 씁니다.


Client RPC: 소유 클라이언트에게 알리기

Client RPC는 서버가 특정 클라이언트에게 함수를 실행하게 할 때 씁니다. 서버에서 일어난 일을 그 클라이언트에게만 알리거나, 클라이언트마다 다른 시각, 청각 피드백을 줄 때 쓸모가 있습니다. UFUNCTION()에 Client 지정자를 붙이고 Reliable이나 Unreliable을 고르며, Client RPC에는 _Validate가 없으므로 _Implementation만 구현합니다.

MyPlayerCharacter.h
#pragma once

// ...

UCLASS()
class MYPROJECT_API AMyPlayerCharacter : public ACharacter
{
    GENERATED_BODY()

public:
    // ...

protected:
    // 서버가 액터의 소유 클라이언트에게 메시지를 보내는 RPC
    UFUNCTION(Client, Reliable)
    void Client_DisplayMessageOnHUD(const FString& Message);

    // Client_DisplayMessageOnHUD_Implementation 함수 선언
    void Client_DisplayMessageOnHUD_Implementation(const FString& Message);

public:
    // 서버에서 이 함수를 호출하여 Client_DisplayMessageOnHUD RPC를 트리거합니다.
    UFUNCTION(BlueprintCallable, Category = "Communication")
    void Server_SendPersonalMessageToOwner(const FString& MessageToSend);
};
MyPlayerCharacter.cpp
#include "MyPlayerCharacter.h"

// ...

// ===========================================
// Client_DisplayMessageOnHUD RPC 구현
// ===========================================

// _Implementation 함수: 호출 위치와 소유 연결에 따른 대상에서 실행
void AMyPlayerCharacter::Client_DisplayMessageOnHUD_Implementation(const FString& Message)
{
    // 리슨 서버의 로컬 소유 캐릭터도 포함합니다.
    if (IsLocallyControlled())
    {
        UE_LOG(LogTemp, Warning, TEXT("Client: Displaying HUD message: %s"), *Message);
        // 여기에 UMG 위젯을 통해 HUD에 메시지를 표시하는 로직을 구현합니다.
    }
}

// 서버에서 호출하여 클라이언트 RPC를 트리거하는 함수
void AMyPlayerCharacter::Server_SendPersonalMessageToOwner(const FString& MessageToSend)
{
    // 이 함수는 서버에서만 호출되어야 합니다.
    if (HasAuthority())
    {
        // 특정 플레이어 컨트롤러를 찾아 그 컨트롤러가 소유한 캐릭터의 Client RPC를 호출합니다.
        // 또는 이 액터의 오너가 PlayerController인지 확인하고 RPC를 호출합니다.
        // FString OwnerName = GetOwner() ? GetOwner()->GetName() : TEXT("No Owner");
        // UE_LOG(LogTemp, Warning, TEXT("Server: Sending personal message to owner (%s)."), *OwnerName);

        Client_DisplayMessageOnHUD(MessageToSend); // Client RPC 호출
    }
}

Server_SendPersonalMessageToOwner는 권한을 가진 쪽에서만 Client_DisplayMessageOnHUD를 부르고, _Implementation은 로컬에서 조종하는 캐릭터일 때만 메시지를 처리합니다. 이 검사로 리슨 서버의 로컬 소유 캐릭터도 포함하면서 HUD를 처리할 대상을 좁힙니다.

Client RPC의 실행 대상은 호출 위치에 따라 달라집니다. 클라이언트에서 직접 부르면 그 클라이언트에서만 실행됩니다. 서버에서 부르면, 원격 소유 연결이 있을 때는 그 클라이언트로 전달되고, 서버가 소유하거나 소유 연결이 없으면 서버에서 실행됩니다.


NetMulticast RPC: 관련 있는 모두에게

NetMulticast RPC는 서버에서 부르면 서버에서도 실행되고, 그 액터가 지금 관련 있는 클라이언트에도 전달됩니다. 관련 있는 플레이어들에게 폭발 효과처럼 한 번 보여 줄 표현을 전할 때 씁니다. 다만 모든 플레이어가 같은 순간에 본다거나, 나중에 접속한 플레이어가 지나간 효과를 받는다는 보장은 없습니다. UFUNCTION()에 NetMulticast 지정자를 붙이고 전송 방식을 고르며, _Implementation만 구현합니다.

MyPlayerCharacter.h
#pragma once

// ...

UCLASS()
class MYPROJECT_API AMyPlayerCharacter : public ACharacter
{
    GENERATED_BODY()

public:
    // ...

protected:
    // 서버가 모든 클라이언트에게 파티클 효과 재생을 명령하는 RPC
    UFUNCTION(NetMulticast, Unreliable) // 시각 효과는 Unreliable로 충분한 경우가 많습니다.
    void Multicast_PlayExplosionEffect(FVector ExplosionLocation);

    // Multicast_PlayExplosionEffect_Implementation 함수 선언
    void Multicast_PlayExplosionEffect_Implementation(FVector ExplosionLocation);

public:
    // 서버에서 이 함수를 호출하여 Multicast_PlayExplosionEffect RPC를 트리거합니다.
    UFUNCTION(BlueprintCallable, Category = "Effects")
    void Server_TriggerGlobalExplosion(FVector Location);
};
MyPlayerCharacter.cpp
#include "MyPlayerCharacter.h"

// ...

// ===========================================
// Multicast_PlayExplosionEffect RPC 구현
// ===========================================

// _Implementation 함수: 서버 호출 시 서버와 현재 관련 있는 클라이언트에서 실행
void AMyPlayerCharacter::Multicast_PlayExplosionEffect_Implementation(FVector ExplosionLocation)
{
    // 서버에서 호출한 경우 서버와 현재 관련 있는 클라이언트에서 실행됩니다.
    UE_LOG(LogTemp, Warning, TEXT("All Clients (and Server): Playing explosion effect at %s"), *ExplosionLocation.ToString());
    // 여기에 UGameplayStatics::SpawnEmitterAtLocation, UGameplayStatics::PlaySoundAtLocation 등
    // 시각적/청각적 효과를 구현합니다.
}

// 서버에서 호출하여 멀티캐스트 RPC를 트리거하는 함수
void AMyPlayerCharacter::Server_TriggerGlobalExplosion(FVector Location)
{
    // 이 함수는 서버에서만 호출되어야 합니다.
    if (HasAuthority())
    {
        UE_LOG(LogTemp, Warning, TEXT("Server: Triggering global explosion at %s."), *Location.ToString());
        Multicast_PlayExplosionEffect(Location); // NetMulticast RPC 호출
    }
}

Server_TriggerGlobalExplosion은 권한을 가진 쪽에서만 Multicast_PlayExplosionEffect를 부르고, 효과의 _Implementation에서 파티클이나 사운드를 재생합니다. 클라이언트에서 NetMulticast RPC를 부르면 그 클라이언트에서만 실행되고, 서버에서 부르면 서버와 그 액터가 지금 관련 있는 클라이언트가 대상입니다. 이 예제처럼 Unreliable이라면 그 대상에서도 호출이 손실될 수 있습니다.


호출 위치와 실행 대상

RPC를 쓸 때는 종류와 실제 실행 위치를 함께 확인해야 합니다. 아래 그림은 생성된 RPC 진입점을 불렀을 때의 규칙을 정리합니다.

RPC 종류와 호출 위치로 실행 대상을 판단합니다

클라이언트에서 생성된 RPC 진입점을 호출하면 복제/소유 연결 검사 뒤 서버 검증 경로로 전달되거나 드롭되는 호출 관문이다. WithValidation false와 정상 게임상 거절은 별도 경로로 남긴다.

UNREAL C++ · CH8-3

RPC 종류와 호출 위치로 실행 대상을 판단합니다

복제 액터·소유 연결의 조건 아래 생성된 RPC 진입점을 호출할 때의 규칙입니다. 함수 이름의 Server_·Client_ 접두사만으로 원격 호출이 생기지는 않습니다.

클라이언트의 Server RPC는 소유 연결을 먼저 확인한다복제액터의생성된ServerRPC진입점을클라이언트가호출하는범위다. 일치소유연결이면서버검증경로,다른소유자/없음이면drop이다. _Validatefalse의disconnect와정상game거절은서버검증뒤별도이며Client/Multicast규칙은원표로읽는다.클라이언트 호출생성된 진입점RPC 전달 조건복제 액터 기준소유 연결 일치?호출자 연결 확인예아니요서버 검증 경로Validate → 구현 조건원격 호출 드롭게임 규칙 실행 안 함
클라이언트의 Server RPC는 소유 연결을 먼저 확인한다복제액터의생성된ServerRPC진입점을클라이언트가호출하는범위다. 일치소유연결이면서버검증경로,다른소유자/없음이면drop이다. _Validatefalse의disconnect와정상game거절은서버검증뒤별도이며Client/Multicast규칙은원표로읽는다.클라이언트 호출생성된 진입점RPC 전달 조건복제 액터 기준소유 연결 일치?호출자 연결 확인예아니요서버 검증 경로Validate → 구현 조건원격 호출 드롭게임 규칙 실행 안 함

이 그림은 소유권 전달 관문까지입니다. 전달됐다고 검증·탄약·피해가 완료된 것은 아니며, WithValidation false는 연결을 끊습니다.

RPC 호출 위치와 실제 실행 대상
RPC서버에서 호출클라이언트에서 호출
Server서버에서 실행합니다.호출자가 소유 연결인 경우 서버로 전달합니다. 다른 소유 연결이거나 소유하지 않으면 드롭됩니다.
Client원격 소유 연결이면 해당 클라이언트. 서버 소유 또는 연결이 없으면 서버에서 실행합니다.호출한 클라이언트에서 로컬 실행합니다.
NetMulticast서버와 해당 액터가 현재 관련 있는 클라이언트에 전달합니다.호출한 클라이언트에서만 로컬 실행합니다.
Server
서버에서 호출: 서버에서 실행합니다.
클라이언트에서 호출: 호출자가 소유 연결인 경우 서버로 전달합니다. 다른 소유 연결이거나 소유하지 않으면 드롭됩니다.
Client
서버에서 호출: 원격 소유 연결이면 해당 클라이언트. 서버 소유 또는 연결이 없으면 서버에서 실행합니다.
클라이언트에서 호출: 호출한 클라이언트에서 로컬 실행합니다.
NetMulticast
서버에서 호출: 서버와 해당 액터가 현재 관련 있는 클라이언트에 전달합니다.
클라이언트에서 호출: 호출한 클라이언트에서만 로컬 실행합니다.

Reliable은 연결 안의 전달 정책이며 소유권이나 게임 규칙 검증을 바꾸지 않습니다. _Validate=false는 연결 종료이고 정상적 게임상 거절과 구분합니다. _Implementation 직접 호출은 RPC 전달·검증 경로를 우회합니다.

이 예의 조건

  • 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
  • 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
원 도표의 제목·노드·관계·조건·주석
범위원문
관계·조건 설명복제 액터의 생성된 Server RPC 진입점을 클라이언트가 호출하는 범위다. 소유 연결이 일치하면 서버 검증 경로로 전달되며, 소유자가 다르거나 없으면 드롭된다. _Validate=false에 따른 연결 종료와 정상적인 게임상 거절은 서버 검증 뒤의 별도 경로이며, Client/Multicast 규칙은 원 표에서 확인한다.
원 표의 셀호출자의 연결이 소유 연결과 일치하는 경우 서버로 전달합니다. 다른 소유 연결이거나 소유하지 않으면 드롭됩니다.
본문·주석Reliable은 연결 안의 전달 정책이며 소유권이나 게임 규칙 검증을 바꾸지 않습니다. _Validate=false는 연결 종료이고 정상적인 게임상 거절과 구분합니다. _Implementation 직접 호출은 RPC 전달·검증 경로를 우회합니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
클라이언트 호출

생성된 RPC 진입점으로 요청한다.

  • 호출 위치: 클라이언트
  • 목표: 서버 RPC
  • 일반 함수처럼 어떤 객체에서나 서버 권한으로 진행하는 호출이 아니다.
연결 관문

복제·소유 연결 조건을 검사한다.

  • 통과: 서버 전달
  • 부적합: drop
  • 클라이언트의 요청 발생 자체가 서버 처리를 보장하지 않는다.
서버 검증

서버에서 요청의 게임상 조건을 검사한다.

  • WithValidation false: 별도 경로
  • 정상 거절: 게임 정책
  • validation 실패와 일반 게임상 거절을 같은 결과로 만들지 않는다.

RPC 종류와 호출 위치로 실행 대상을 판단합니다

클라이언트에서 생성된 RPC 진입점을 호출하면 복제/소유 연결 검사 뒤 서버 검증 경로로 전달되거나 드롭되는 호출 관문이다. WithValidation false와 정상 게임상 거절은 별도 경로로 남긴다.

UNREAL C++ · CH8-3

RPC 종류와 호출 위치로 실행 대상을 판단합니다

복제 액터·소유 연결의 조건 아래 생성된 RPC 진입점을 호출할 때의 규칙입니다. 함수 이름의 Server_·Client_ 접두사만으로 원격 호출이 생기지는 않습니다.

클라이언트의 Server RPC는 소유 연결을 먼저 확인한다복제액터의생성된ServerRPC진입점을클라이언트가호출하는범위다. 일치소유연결이면서버검증경로,다른소유자/없음이면drop이다. _Validatefalse의disconnect와정상game거절은서버검증뒤별도이며Client/Multicast규칙은원표로읽는다.클라이언트 호출생성된 진입점RPC 전달 조건복제 액터 기준소유 연결 일치?호출자 연결 확인예아니요서버 검증 경로Validate → 구현 조건원격 호출 드롭게임 규칙 실행 안 함
클라이언트의 Server RPC는 소유 연결을 먼저 확인한다복제액터의생성된ServerRPC진입점을클라이언트가호출하는범위다. 일치소유연결이면서버검증경로,다른소유자/없음이면drop이다. _Validatefalse의disconnect와정상game거절은서버검증뒤별도이며Client/Multicast규칙은원표로읽는다.클라이언트 호출생성된 진입점RPC 전달 조건복제 액터 기준소유 연결 일치?호출자 연결 확인예아니요서버 검증 경로Validate → 구현 조건원격 호출 드롭게임 규칙 실행 안 함

이 그림은 소유권 전달 관문까지입니다. 전달됐다고 검증·탄약·피해가 완료된 것은 아니며, WithValidation false는 연결을 끊습니다.

RPC 호출 위치와 실제 실행 대상
RPC서버에서 호출클라이언트에서 호출
Server서버에서 실행합니다.호출자가 소유 연결인 경우 서버로 전달합니다. 다른 소유 연결이거나 소유하지 않으면 드롭됩니다.
Client원격 소유 연결이면 해당 클라이언트. 서버 소유 또는 연결이 없으면 서버에서 실행합니다.호출한 클라이언트에서 로컬 실행합니다.
NetMulticast서버와 해당 액터가 현재 관련 있는 클라이언트에 전달합니다.호출한 클라이언트에서만 로컬 실행합니다.
Server
서버에서 호출: 서버에서 실행합니다.
클라이언트에서 호출: 호출자가 소유 연결인 경우 서버로 전달합니다. 다른 소유 연결이거나 소유하지 않으면 드롭됩니다.
Client
서버에서 호출: 원격 소유 연결이면 해당 클라이언트. 서버 소유 또는 연결이 없으면 서버에서 실행합니다.
클라이언트에서 호출: 호출한 클라이언트에서 로컬 실행합니다.
NetMulticast
서버에서 호출: 서버와 해당 액터가 현재 관련 있는 클라이언트에 전달합니다.
클라이언트에서 호출: 호출한 클라이언트에서만 로컬 실행합니다.

Reliable은 연결 안의 전달 정책이며 소유권이나 게임 규칙 검증을 바꾸지 않습니다. _Validate=false는 연결 종료이고 정상적 게임상 거절과 구분합니다. _Implementation 직접 호출은 RPC 전달·검증 경로를 우회합니다.

이 예의 조건

  • 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
  • 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.

관계도의 보충 설명

원 도표의 제목·노드·관계·조건·주석
범위원문
관계·조건 설명복제 액터의 생성된 Server RPC 진입점을 클라이언트가 호출하는 범위다. 소유 연결이 일치하면 서버 검증 경로로 전달되며, 소유자가 다르거나 없으면 드롭된다. _Validate=false에 따른 연결 종료와 정상적인 게임상 거절은 서버 검증 뒤의 별도 경로이며, Client/Multicast 규칙은 원 표에서 확인한다.
원 표의 셀호출자의 연결이 소유 연결과 일치하는 경우 서버로 전달합니다. 다른 소유 연결이거나 소유하지 않으면 드롭됩니다.
본문·주석Reliable은 연결 안의 전달 정책이며 소유권이나 게임 규칙 검증을 바꾸지 않습니다. _Validate=false는 연결 종료이고 정상적인 게임상 거절과 구분합니다. _Implementation 직접 호출은 RPC 전달·검증 경로를 우회합니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
클라이언트 호출

생성된 RPC 진입점으로 요청한다.

  • 호출 위치: 클라이언트
  • 목표: 서버 RPC
  • 일반 함수처럼 어떤 객체에서나 서버 권한으로 진행하는 호출이 아니다.
연결 관문

복제·소유 연결 조건을 검사한다.

  • 통과: 서버 전달
  • 부적합: drop
  • 클라이언트의 요청 발생 자체가 서버 처리를 보장하지 않는다.
서버 검증

서버에서 요청의 게임상 조건을 검사한다.

  • WithValidation false: 별도 경로
  • 정상 거절: 게임 정책
  • validation 실패와 일반 게임상 거절을 같은 결과로 만들지 않는다.

위 그림의 위쪽은 클라이언트가 Server RPC를 부를 때 소유 연결부터 확인하는 흐름입니다. 클라이언트가 복제 액터의 생성된 진입점을 부르면, 복제 액터를 기준으로 전달 조건을 보고, 호출한 클라이언트의 연결이 그 액터의 소유 연결과 일치하는지 확인합니다. 일치하면 서버로 전달되어 _Validate와 _Implementation의 조건으로 넘어가고, 다른 소유자이거나 소유자가 없으면 원격 호출이 버려져 게임 규칙이 실행되지 않습니다. 그림 아래의 설명처럼 이 그림은 소유권 관문까지만 다루며, 전달되었다고 검증, 탄약, 피해가 완료된 것은 아닙니다.

그림의 표는 세 RPC를 서버와 클라이언트에서 불렀을 때의 실행 대상을 정리합니다. Server RPC는 서버에서 부르면 서버에서 실행되고, 클라이언트에서 부르면 호출자가 소유 연결일 때만 서버로 전달되며 그렇지 않으면 버려집니다. Client RPC는 서버에서 부르면 원격 소유 연결의 클라이언트에서 실행되고 서버 소유이거나 연결이 없으면 서버에서 실행되며, 클라이언트에서 부르면 그 클라이언트에서만 실행됩니다. NetMulticast RPC는 서버에서 부르면 서버와 관련 있는 클라이언트에 전달되고, 클라이언트에서 부르면 그 클라이언트에서만 실행됩니다.

그림 아래의 주의도 함께 기억합니다. Reliable은 연결 안의 전달 방식일 뿐 소유권이나 게임 규칙 검증을 바꾸지 않습니다. _Validate의 false는 연결 종료이므로 정상적인 게임상 거절과 구별해야 하고, _Implementation을 직접 부르면 RPC의 전달과 검증 경로를 건너뜁니다. 또 함수 이름의 Server_나 Client_ 접두사만으로는 원격 호출이 생기지 않습니다. 단계 그림도 클라이언트의 호출이 연결 관문을 거쳐 서버 검증으로 가는 과정을 차례로 보여 주며, 요청이 일어났다는 것만으로 서버 처리가 보장되지 않는다는 점을 짚습니다.


RPC를 쓸 때 고려할 점

RPC는 네트워크 대역폭을 씁니다. 필요 없는 RPC 호출은 피하고 필요한 데이터만 보냅니다. 중요하고 반드시 전달되어야 하는 호출은 Reliable로, 자주 일어나고 실시간성이 중요한 호출은 Unreliable로 정해 대역폭을 아낍니다. 예를 들어 총 발사는 Reliable로, 총알 궤적의 갱신은 변수 복제나 Unreliable RPC로 처리합니다.

Server RPC에는 반드시 서버 쪽 검증이 있어야 합니다. WithValidation으로 조작이 분명한 값을 걸러 내고, 게임 규칙에 따른 거절은 _Implementation의 앞부분에서 분기합니다. Client RPC는 서버에서 원격 소유 연결을 가진 액터로 부르면 그 클라이언트에게 전달되므로, 특정 플레이어의 일을 알릴 때 씁니다.

멀티플레이어 버그는 대부분 누가 무엇을 바꿀 권한을 가지는지가 흐릿할 때 생깁니다. 체력, 탄약, 점수 같은 상태는 서버만 고치고 클라이언트는 요청만 보냅니다. 효과, 사운드, UI 같은 표현은 OnRep_, Client RPC, NetMulticast로 나눠 처리합니다. Client RPC가 보이지 않으면 SetOwner로 소유가 제대로 정해졌는지부터 점검합니다. 문제를 재현할 때는 단일 클라이언트 PIE, 2인 PIE, 그리고 Net PktLag로 지연을 흉내 낸 환경의 순서로 넓혀 가면 원인을 나누기 쉽습니다.

C++에서 실행 중에 확인할 때는 지원되는 개발 빌드에서 log LogNet VeryVerbose로 LogNet 분류의 상세 로그를 켤 수 있습니다. GetLocalRole(), GetOwner(), 그리고 호출한 인스턴스를 로그에 함께 남기면 호출이 어디로 전달되었는지 구분하기 쉽고, Stat Net은 네트워크 통계를 보여 줍니다.

이 글의 발사 예제는 유한한 인자와 500cm 거리만 검사하고 요청을 로그로 남길 뿐입니다. 발사 빈도, 탄약, 조준 방향의 타당성, 투사체 생성과 피해 적용은 구현하지 않았으므로, 로그에 Processing shot이 나타났다고 그 처리가 완료되었다고 볼 수 없습니다.


더 깊이: RPC와 속성 복제가 실행되는 순서

받는 쪽에서 한 액터의 데이터를 처리할 때는 RPC가 먼저 실행되고, 속성 갱신이 그다음에 적용됩니다. 속성 갱신은 신뢰할 수 없는(unreliable) 데이터 묶음 하나로 보내집니다. 그래서 RPC 안에서 복제 변수에 값을 써도, 뒤이어 적용되는 속성 갱신이 그 값을 덮어써 사라질 수 있습니다. 예외는 Unreliable 멀티캐스트로, 호출한 자리에서 큐에 쌓였다가 항상 마지막에 직렬화되므로 속성 갱신이 적용된 뒤에 실행됩니다.

같은 액터에서 부른 Reliable RPC는 호출한 순서대로 실행되고, 액터와 그 하위 객체에서 부른 RPC도 받는 쪽에서 호출 순서가 지켜집니다. 하지만 서로 다른 액터에 걸친 RPC의 원래 호출 순서를 지켜 주는 장치는 없습니다. Unreliable과 Reliable RPC 사이의 순서도 지켜지는 것처럼 보일 수는 있지만 보장되지는 않습니다. 따라서 열쇠 액터의 RPC가 문 액터의 RPC보다 먼저 도착한다고 가정하는 로직이라면, 두 호출을 한 액터의 Reliable RPC로 모으거나 하나의 복제 상태로 표현해야 합니다.

OnRep_ 함수의 순서도 정해져 있지 않습니다. 서로 다른 복제 변수의 RepNotify가 클라이언트에서 불리는 순서는 결정적이지 않으며, 서버에서 값을 바꾼 순서나 헤더에 선언한 위치와도 관계가 없습니다. 그래서 체력과 최대 체력처럼 함께 해석해야 하는 값은 하나의 구조체에 담아 복제하거나, 받은 값의 RepNotify가 모두 불린 뒤에 호출되는 UObject::PostRepNotifies에서 한꺼번에 처리합니다. 이 규칙들은 Replicated Object Execution Order 문서에 정리되어 있습니다.


정리

  • RPC는 함수 호출을 네트워크로 전달하며, 상태 복제와 달리 나중에 접속한 클라이언트에 지나간 호출을 재생하지 않습니다.
  • Server RPC는 클라이언트의 요청을 서버로 보내며, 호출한 클라이언트가 그 액터의 소유 연결일 때만 전달됩니다.
  • _Validate의 false는 _Implementation을 막고 연결을 끊으므로 조작이 분명한 값에만 쓰고, 정상적인 거절은 _Implementation에서 처리합니다. _Implementation을 직접 부르면 전달과 검증을 건너뜁니다.
  • Client RPC는 서버에서 부르면 원격 소유 클라이언트에, NetMulticast RPC는 서버와 관련 있는 클라이언트에 전달되며, 둘 다 클라이언트에서 부르면 그 클라이언트에서만 실행됩니다.
  • Reliable은 연결 안의 전달 추적일 뿐 게임 규칙의 성공을 보장하지 않고, Unreliable은 손실을 허용합니다.
  • 상태는 서버만 고치고, 표현은 OnRep_와 RPC로 나누며, 문제는 1인 PIE, 2인 PIE, 지연 환경의 순서로 재현합니다.

연습 문제

1. 다른 사람의 캐릭터로 쏘기

B가 소유한 복제 캐릭터에서 클라이언트 A가 Server_ShootWeapon을 불렀습니다. 이 호출은 서버에서 실행될까요?

해설 보기

실행되지 않습니다. 클라이언트에서 부른 Server RPC는 호출한 클라이언트가 그 액터의 소유 연결일 때만 서버로 전달됩니다. A는 이 캐릭터의 소유 연결이 아니므로 원격 호출은 버려지고, 서버의 _Validate와 _Implementation도 실행되지 않습니다.

2. 기준을 1cm 넘으면

정당한 소유자 B가 서버 위치 (0, 0, 0)cm인 캐릭터에서 유한한 위치 (300, 401, 0)cm를 보냈습니다. 거리 검사와 연결, _Implementation은 어떻게 처리될까요? 정상적인 재사용 대기 시간 때문에 잠시 거절하는 정책도 같은 false 분기에 넣어야 할까요?

해설 보기

거리의 제곱은 300²+401² = 90,000+160,801 = 250,801cm²로 250,000보다 크므로, _Validate는 false를 돌려줍니다. 그러면 호출한 클라이언트 B의 연결이 끊기고 _Implementation은 실행되지 않습니다.

재사용 대기 시간 때문의 거절은 정상적인 플레이에서도 일어나는 일이므로 _Validate의 false로 처리하면 안 됩니다. _Implementation 안에서 대기 시간을 확인해 요청을 무시하거나 거절 알림을 보내는 게임 규칙 분기로 처리합니다.

3. 리슨 서버의 호스트에게 메시지 보내기

리슨 서버에서 호스트 플레이어가 조종하는 캐릭터에 대해 서버가 Client_DisplayMessageOnHUD를 불렀습니다. 이 RPC는 어디서 실행되고, 메시지가 처리될까요?

해설 보기

호스트의 캐릭터는 서버의 로컬 플레이어가 소유하므로 원격 소유 연결이 없습니다. 그래서 Client RPC는 서버에서 실행됩니다. 리슨 서버에서는 그 서버가 곧 호스트 플레이어의 화면이므로, _Implementation의 IsLocallyControlled() 검사가 참이 되어 메시지가 처리됩니다.

이 검사가 없었다면 서버에서 실행되는 모든 경우에 메시지를 처리하게 되므로, 로컬 조종 여부로 처리 대상을 좁히는 것이 중요합니다.

RPC의 실행 위치 규칙과 검증은 Epic의 Remote Procedure Calls 문서에서, 소유 연결은 Actor Owner and Owning Connection 문서에서 확인할 수 있습니다.

다음 글 멀티플레이어 게임 최적화에서는 관련성, 업데이트 빈도, 휴면 같은 설정으로 네트워크 대역폭을 줄이는 방법을 살펴봅니다.