RPC의 기본 개념과 동작 원리를 학습하였다. 언리얼 C++에서 다양한 RPC를 사용하는 방법이 있는데 서버와 클라이언트 각각에서 실행되는지 여부를 잘 체크해야 한다. PROPERTY Replication과 RPC의 사용방법에는 차이가 있다. 두 방법의 차이점을 학습하여 상황에 따라 적절하게 활용하자.

 

 

인프런 이득우님의 '언리얼 프로그래밍 Part3 - 네트웍 멀티플레이 프레임웍의 이해' 강의를 참고하였습니다. 
😎 [이득우의 언리얼 프로그래밍 Part3 - 네트웍 멀티플레이 프레임웍의 이해] 강의 들으러 가기!

 

 

목차

     

     


     

     

    RPC (Remote Procedure Call)

     

    RPC의 기본 개념과 동작 원리를 이해하기

    언리얼 C++에서 다양한 RPC를 사용하는 방법을 학습하기

    PROPERTY Replication과 RPC와의 차이점 이해

     

     

    Replicated와 Replicated Notify는 복제이고 RPC는 통신이다!  

      Replicated Replicated Notify RPC
      단순 복제  복제 후 특정 시점에 지정한 함수 호출 즉시 알림. 통신.
      스케쥴링. 모은 다음에 복제.
    그 틱에 바로 가지 않는다
    Replicated와 마친가지로 즉시 복제는 안 된다.  수집하지 않음.
    데이터를 복제시키지 않음.
    그냥 통신한다.
    내가 원하는 시점에 통신으로 이벤트를 날린다.
      어느 시점에 바뀌는지 알지 못하지만 안전하게 바꾸고 싶을때 사용한다.  복제된 시점에 지정된 이벤트 함수(OnRep_OOO)를 호출하는 방식으로 사용한다. 그 시점에 이벤트를 파라미터와 
    복제본에서 원본으로 RPC 이벤트를 날릴 수 있다. 반면에 Replicate로는 복제본에서 원본으로 날릴 수 없다.

     

     

    ◈ Replicated와 RPC를 동시에 사용할 때 주의사항 (중요) 

    Replicated와 같은 Tick에 RPC를 날리면 안 된다!!!

     

    ex. 총을스폰 시킨 후 총을 반짝이는 기능을 구현하고 싶다. 총을 스폰시킨 시점(Replicated 된 시점)에 반짝이게 하는 RPC를 날리면 안 된다. 총이 스폰되기 전에 반짝이는 함수를 콜하면 총이 없어 크래쉬가 난다. 네트워크 환경이 좋아 만약 통과하면 최악이다. 문제를 찾기가 힘들다. 

     

    ->> 해당 경우는 RPC가 아닌 Replicated Notify를 사용해서 총이 스폰된 시점(또는 스폰된 이후)에 반짝이는 기능의 함수를 호출하게 한다.


     

     

    RPC(Remote Procedure Call) 란?

     

    • 원격 프로시져(함수) 호출의 약자
    • 원격 컴퓨터에 있는 함수를 호출할 수 있도록 만든 통신 프로토콜
    • Network Multiplay에서 Server와 Client 간에 빠르게 행동을 명령하고 정보를 주고받는데 사용
    • Client-Server모델을 사용하는 언리얼 엔진에서 Client에서 Server로 통신하는 유일한 수단을 제공한다.

     

    ' Connection을 소유하는 Actor( =Ownership을 가지는 Actor) '는 Client에서 Server로 명령을 내릴 수 있다.

     

     

     

     

    NetMulticast를 클라이언트에서 호출하면 바보!

     

     

    https://dev.epicgames.com/documentation/ko-kr/unreal-engine/rpcs-in-unreal-engine?application_version=5.3

     

    언리얼 엔진의 RPC

    네트워크를 통해 함수 리플리케이션을 지정합니다.

    dev.epicgames.com


     

     

    RPC 사용 방법:   Server  vs. Client

     

    서버(Server)에서 RPC를 호출한 경우 

       NetMulticast Server Client
    Client 소유 Server + 모든 Client Server Only 소유한 Client
    Server 소유 Server + 모든 Client 소유한 Server Server Only
    소유 없음 Server + 모든 Client Server Only Server Only

     

     

    클라이언트(Client)에서 RPC를 호출한 경우 

       NetMulticast Server Client
    호출한 Client 소유 호출한 Client Only Server 호출한 Client Only
    다른 Client 소유 호출한 Client Only 버려짐 호출한 Client Only
    Server 소유 호출한 Client Only 버려짐 호출한 Client Only
    소유 없음 호출한 Client Only 버려짐  

     

    초록색:  클라이언트 쪽에서 실행

    보라색:  서버 쪽에서 실행

    주황색:  Ownership과 무관하게 연관성으로 동작


     

    Client RPC 개요

     

    • Server에서 Client로 호출하는 RPC
    • 이를 활용해 특정 Client에게만 명령을 보낼 수 있음
    • Server에서 명령을 보낼 Client의 Connection을 소유한 Actor를 사용해야 함. ( AActor::GetNetConnection )

     

    Client쪽에서는 함수에 _Implemention가 붙게 된다.

     

     

    RPC는 반드시 전달되어야 하는 명령이 아니면 Unreliable 키워드를 사용하는게 좋다.


     

     

    Server RPC 개요

     

    • Client에서 Server로 호출하는 RPC
    • 언리얼 엔진 구조에서 유일하게 Client가 Server의 함수를 호출할 수 있는 기능
    • Server쪽에서 Client의 명령을 검증할 수 있는 함수를 구현할 수 있음. (만일 악의적인 유저가 데이터를 변조해 서버에 명령을 내리는데 서버가 이것을 그냥 조건없이 모두 수용하게 되면 현재 진행 중인 전반적인 게임 시스템이 무너질 수 있다.)
      • Server RPC에 WithValidation이라는 키워드를 선언했다면  _Validate 라는 함수를 추가로 구현해준다.
    • Client RPC와 동일하게 Server와의 Connection을 소유한 Actor를 사용해야 함

     


     

     

    NetMulticast RPC 개요

     

    • Server를 포함해 모든 플레이어에게 명령을 보내는 RPC
    • Property Replication과 유사하게 연관성 기반으로 동작함 ( Connection을 소유하지 않아도 동작 )
    • Property Replication과 유사하지만 다른 용도로 사용

     

     


     

     

    RPC 선언에 관련된 키워드 정리

     

       
     Unreliable -  RPC 호출을 보장하지 않는 옵션.
    -  움직임에 필요한 데이터를 빈번하게 전송함으로써 부드러운 움직임을 구현할 수 있도록 도와준다.
    -  빠름
     Reliable -  RPC 호출을 보장해주는 추가 옵션.
    -  데이터 전송을 보장하기 위해서 내부적으로 보낸 패킷이 잘 도착했는지 서로 확인하는 번거로운 작업을 거친다.
    -  네트워크 상태에 따라 꽤나 부하를 유발할 수 있기 때문에 게임 플레이에 영향을 미치는 꼭 필요한 때만 이 옵션을 사용하는 것이 좋다.
    -  정말 필요한 때만 호출.
     WithValidation -  Server에서 검증 로직을 추가로 구현할 때 추가하는 옵션
    -  문법적으로는 모든 RPC에 사용이 가능하지만 실질적으로는 서버 RPC에서만 사용한다. RPC를 호출한 결과가 서버에서 구동되기 때문이다.
    -  클라이언트 RPC에서 사용하면 바보!

     

     

     

    키워드 1 키워드 2 키워드 3
    Client Unrelible WithValidation
    (Server만 사용)
    Server Reliable  
    NetMulticast    

     

     

     

     

    프로젝트 예시


     

     

    예시#1 - NetMulticast RPC  분수대 색이 변하지 않음

     

    ABFountain.h

    더보기
    #pragma once
    #include "CoreMinimal.h"
    #include "GameFramework/Actor.h"
    #include "ABFountain.generated.h"
    
    UCLASS()
    class ARENABATTLE_API AABFountain : public AActor
    {
    	GENERATED_BODY()
    	
    public:	
    	AABFountain();
    
    protected:
    	virtual void BeginPlay() override;
    
    public:	
    	virtual void Tick(float DeltaTime) override;
    
    	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = Mesh)
    	TObjectPtr<class UStaticMeshComponent> Body;
    
    	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = Mesh)
    	TObjectPtr<class UStaticMeshComponent> Water;
    
    public:
    	virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
    	//virtual void OnActorChannelOpen(class FInBunch& InBunch, class UNetConnection* Connection) override;
    	//virtual bool IsNetRelevantFor(const AActor* RealViewer, const AActor* ViewTarget, const FVector& SrcLocation) const override;
    	//virtual void PreReplication(IRepChangedPropertyTracker& ChangedPropertyTracker) override;
    
    	UPROPERTY(ReplicatedUsing = OnRep_ServerRotationYaw)
    	float ServerRotationYaw;
    
    	UPROPERTY(ReplicatedUsing = OnRep_ServerLightColor)
    	FLinearColor ServerLightColor;
    
    	UFUNCTION()
    	void OnRep_ServerRotationYaw();
    
    	UFUNCTION()
    	void OnRep_ServerLightColor();
    
    	UFUNCTION(NetMulticast, Unreliable) // 이거 작업함!
    	void MulticastRPCChangeLightColor(const FLinearColor& NewLightColor);
        
        UFUNCTION(Server, Unreliable)
        void ServerRPCChangeLightColor_Implementation()
    
    	float RotationRate = 30.0f;
    	float ClientTimeSinceUpdate = 0.0f;
    	float ClientTimeBetweenLastUpdate = 0.0f;
    };

    함수 추가

    • UFUNCTION(NetMulticast, Unreliable)
      void MulticastRPCChangeLightColor(const FLinearColor& NewLightColor);

     

     

    ABFountain.cpp

    더보기
    #include "Prop/ABFountain.h"
    #include "Components/StaticMeshComponent.h"
    #include "Components/PointLightComponent.h"
    #include "Net/UnrealNetwork.h"
    #include "ArenaBattle.h"
    #include "EngineUtils.h"
    
    AABFountain::AABFountain()
    {
     	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
    	PrimaryActorTick.bCanEverTick = true;
    
    	Body = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Body"));
    	Water = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Water"));
    
    	RootComponent = Body;
    	Water->SetupAttachment(Body);
    	Water->SetRelativeLocation(FVector(0.0f, 0.0f, 132.0f));
    
    	static ConstructorHelpers::FObjectFinder<UStaticMesh> BodyMeshRef(TEXT("/Script/Engine.StaticMesh'/Game/ArenaBattle/Environment/Props/SM_Plains_Castle_Fountain_01.SM_Plains_Castle_Fountain_01'"));
    	if (BodyMeshRef.Object)
    	{
    		Body->SetStaticMesh(BodyMeshRef.Object);
    	}
    
    	static ConstructorHelpers::FObjectFinder<UStaticMesh> WaterMeshRef(TEXT("/Script/Engine.StaticMesh'/Game/ArenaBattle/Environment/Props/SM_Plains_Fountain_02.SM_Plains_Fountain_02'"));
    	if (WaterMeshRef.Object)
    	{
    		Water->SetStaticMesh(WaterMeshRef.Object);
    	}
    
    	bReplicates = true;
    	NetUpdateFrequency = 1.0f;
    	NetCullDistanceSquared = 4000000.0f;
    	//NetDormancy = DORM_Initial;
    }
    
    // Called when the game starts or when spawned
    void AABFountain::BeginPlay()
    {
    	Super::BeginPlay();
    	
    	if (HasAuthority())
    	{
    		FTimerHandle Handle;
    		GetWorld()->GetTimerManager().SetTimer(Handle, FTimerDelegate::CreateLambda([&]
    			{
    				//BigData.Init(BigDataElement, 1000);
    				//BigDataElement += 1.0;
    				//ServerLightColor = FLinearColor(FMath::RandRange(0.0f, 1.0f), FMath::RandRange(0.0f, 1.0f), FMath::RandRange(0.0f, 1.0f), 1.0f);
    				//OnRep_ServerLightColor();
    
    				//const FLinearColor NewLightColor = FLinearColor(FMath::RandRange(0.0f, 1.0f), FMath::RandRange(0.0f, 1.0f), FMath::RandRange(0.0f, 1.0f), 1.0f);
    				//MulticastRPCChangeLightColor(NewLightColor);
    				//ClientRPCChangeLightColor(NewLightColor);
    			}
    		), 1.0f, true, 0.0f);
    
    		
    	}
    	else
    	{
    		SetOwner(GetWorld()->GetFirstPlayerController()); // Clinet는 Proxy(=서버의 내용물을 복제한 것)에 불과하기 때문에 Clinet에 오너를 설정하는 것은 의미가 없다. 
    		FTimerHandle Handle;
    		GetWorld()->GetTimerManager().SetTimer(Handle, FTimerDelegate::CreateLambda([&]
    			{
    				ServerRPCChangeLightColor();
    			}
    		), 1.0f, true, 0.0f);
    	}
    }
    
    void AABFountain::Tick(float DeltaTime)
    {
    	Super::Tick(DeltaTime);
    
    	if (HasAuthority())
    	{
    		AddActorLocalRotation(FRotator(0.0f, RotationRate * DeltaTime, 0.0f));
    		ServerRotationYaw = RootComponent->GetComponentRotation().Yaw;
    	}
    	else
    	{
    		ClientTimeSinceUpdate += DeltaTime;
    		if (ClientTimeBetweenLastUpdate < KINDA_SMALL_NUMBER)
    		{
    			return;
    		}
    
    		const float EstimateRotationYaw = ServerRotationYaw + RotationRate * ClientTimeBetweenLastUpdate;
    		const float LerpRatio = ClientTimeSinceUpdate / ClientTimeBetweenLastUpdate;
    
    		FRotator ClientRotator = RootComponent->GetComponentRotation();
    		const float ClientNewYaw = FMath::Lerp(ServerRotationYaw, EstimateRotationYaw, LerpRatio);
    		ClientRotator.Yaw = ClientNewYaw;
    		RootComponent->SetWorldRotation(ClientRotator);
    	}
    }
    
    void AABFountain::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
    {
    	Super::GetLifetimeReplicatedProps(OutLifetimeProps);
    
    	DOREPLIFETIME(AABFountain, ServerRotationYaw);
    	DOREPLIFETIME(AABFountain, ServerLightColor);
    }
    
    //void AABFountain::OnActorChannelOpen(FInBunch& InBunch, UNetConnection* Connection)
    //{
    //	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("Begin"));
    //
    //	Super::OnActorChannelOpen(InBunch, Connection);
    //
    //	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("End"));
    //}
    //
    //bool AABFountain::IsNetRelevantFor(const AActor* RealViewer, const AActor* ViewTarget, const FVector& SrcLocation) const
    //{
    //	bool NetRelevantResult = Super::IsNetRelevantFor(RealViewer, ViewTarget, SrcLocation);
    //	if (!NetRelevantResult)
    //	{
    //		AB_LOG(LogABNetwork, Log, TEXT("Not Relevant:[%s] %s"), *RealViewer->GetName(), *SrcLocation.ToCompactString());
    //	}
    //
    //	return NetRelevantResult;
    //}
    //
    //void AABFountain::PreReplication(IRepChangedPropertyTracker& ChangedPropertyTracker)
    //{
    //	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("Begin"));
    //	Super::PreReplication(ChangedPropertyTracker);
    //}
    
    void AABFountain::OnRep_ServerRotationYaw()
    {
    	//AB_LOG(LogABNetwork, Log, TEXT("Yaw : %f"), ServerRotationYaw);
    
    	FRotator NewRotator = RootComponent->GetComponentRotation();
    	NewRotator.Yaw = ServerRotationYaw;
    	RootComponent->SetWorldRotation(NewRotator);
    
    	ClientTimeBetweenLastUpdate = ClientTimeSinceUpdate;
    	ClientTimeSinceUpdate = 0.0f;
    }
    
    void AABFountain::OnRep_ServerLightColor()
    {
    	if (!HasAuthority())
    	{
    		AB_LOG(LogABNetwork, Log, TEXT("LightColor : %s"), *ServerLightColor.ToString());
    	}
    
    	UPointLightComponent* PointLight = Cast<UPointLightComponent>(GetComponentByClass(UPointLightComponent::StaticClass()));
    	if (PointLight)
    	{
    		PointLight->SetLightColor(ServerLightColor);
    	}
    }
    
    void AABFountain::ServerRPCChangeLightColor_Implementation()
    {
    	const FLinearColor NewLightColor = FLinearColor(FMath::RandRange(0.0f, 1.0f), FMath::RandRange(0.0f, 1.0f), FMath::RandRange(0.0f, 1.0f), 1.0f);
    	MulticastRPCChangeLightColor(NewLightColor);
    }
    
    void AABFountain::MulticastRPCChangeLightColor_Implementation(const FLinearColor& NewLightColor) // 이거 작업함!
    {
    	AB_LOG(LogABNetwork, Log, TEXT("LightColor : %s"), *NewLightColor.ToString());
    
    	UPointLightComponent* PointLight = Cast<UPointLightComponent>(GetComponentByClass(UPointLightComponent::StaticClass()));
    	if (PointLight)
    	{
    		PointLight->SetLightColor(NewLightColor);
    	}
    }

    Clinet는 Proxy(=서버의 내용물을 복제한 것)에 불과하기 때문에 Clinet에 오너를 설정하는 것은 의미가 없다. 

     

     

    실행화면

    만약 클라이언트가 분수대에 멀리 떨어지게되면 연관성을 잃게 되어 분수대의 색이 변하지 않게 된다. ( Multicast RPC는 연관성에 관련하여 동작한다. )

    연관성을 잃게 되면 클라이언트에는 해당 RPC가 호출되지 않는다.

     


     

     

    예시#2 - Server RPC

     

    ABFountain.h

    더보기
    #pragma once
    #include "CoreMinimal.h"
    #include "GameFramework/Actor.h"
    #include "ABFountain.generated.h"
    
    UCLASS()
    class ARENABATTLE_API AABFountain : public AActor
    {
    	GENERATED_BODY()
    	
    public:	
    	AABFountain();
    
    protected:
    	virtual void BeginPlay() override;
    
    public:	
    	virtual void Tick(float DeltaTime) override;
    
    	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = Mesh)
    	TObjectPtr<class UStaticMeshComponent> Body;
    
    	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = Mesh)
    	TObjectPtr<class UStaticMeshComponent> Water;
    
    public:
    	virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
    	//virtual void OnActorChannelOpen(class FInBunch& InBunch, class UNetConnection* Connection) override;
    	//virtual bool IsNetRelevantFor(const AActor* RealViewer, const AActor* ViewTarget, const FVector& SrcLocation) const override;
    	//virtual void PreReplication(IRepChangedPropertyTracker& ChangedPropertyTracker) override;
    
    	UPROPERTY(ReplicatedUsing = OnRep_ServerRotationYaw)
    	float ServerRotationYaw;
    
    	UPROPERTY(ReplicatedUsing = OnRep_ServerLightColor)
    	FLinearColor ServerLightColor;
    
    	UFUNCTION()
    	void OnRep_ServerRotationYaw();
    
    	UFUNCTION()
    	void OnRep_ServerLightColor();
    
    	UFUNCTION(NetMulticast, Unreliable)
    	void MulticastRPCChangeLightColor(const FLinearColor& NewLightColor);
        
        UFUNCTION(Server, Unreliable) // 이거 작업함!
        void ServerRPCChangeLightColor();
    
    	float RotationRate = 30.0f;
    	float ClientTimeSinceUpdate = 0.0f;
    	float ClientTimeBetweenLastUpdate = 0.0f;
    };

    함수 추가

    • UFUNCTION(Server, Unreliable)
      void ServerRPCChangeLightColor();

     

     

    ABFountain.cpp

    더보기
    #include "Prop/ABFountain.h"
    #include "Components/StaticMeshComponent.h"
    #include "Components/PointLightComponent.h"
    #include "Net/UnrealNetwork.h"
    #include "ArenaBattle.h"
    #include "EngineUtils.h"
    
    AABFountain::AABFountain()
    {
     	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
    	PrimaryActorTick.bCanEverTick = true;
    
    	Body = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Body"));
    	Water = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Water"));
    
    	RootComponent = Body;
    	Water->SetupAttachment(Body);
    	Water->SetRelativeLocation(FVector(0.0f, 0.0f, 132.0f));
    
    	static ConstructorHelpers::FObjectFinder<UStaticMesh> BodyMeshRef(TEXT("/Script/Engine.StaticMesh'/Game/ArenaBattle/Environment/Props/SM_Plains_Castle_Fountain_01.SM_Plains_Castle_Fountain_01'"));
    	if (BodyMeshRef.Object)
    	{
    		Body->SetStaticMesh(BodyMeshRef.Object);
    	}
    
    	static ConstructorHelpers::FObjectFinder<UStaticMesh> WaterMeshRef(TEXT("/Script/Engine.StaticMesh'/Game/ArenaBattle/Environment/Props/SM_Plains_Fountain_02.SM_Plains_Fountain_02'"));
    	if (WaterMeshRef.Object)
    	{
    		Water->SetStaticMesh(WaterMeshRef.Object);
    	}
    
    	bReplicates = true;
    	NetUpdateFrequency = 1.0f;
    	NetCullDistanceSquared = 4000000.0f;
    	//NetDormancy = DORM_Initial;
    }
    
    // Called when the game starts or when spawned
    void AABFountain::BeginPlay()
    {
    	Super::BeginPlay();
    	
    	if (HasAuthority())
    	{
    		FTimerHandle Handle2;
    		GetWorld()->GetTimerManager().SetTimer(Handle2, FTimerDelegate::CreateLambda([&]
    		{
            // 방법#1
    		//FlushNetDormancy();
    		//for (FConstPlayerControllerIterator Iterator = GetWorld()->GetPlayerControllerIterator(); Iterator; ++Iterator)
    		//{
    		//	APlayerController* PlayerController = Iterator->Get();
    		//	if (PlayerController && !PlayerController->IsLocalPlayerController())
    		//	{
    		//		SetOwner(PlayerController);
    		//		break;
    		//	}
    		//}
            
            // 방법#2
            for (APlayerController* PlayerController : TActorRange<APlayerController>(GetWorld()))
            {
                if (PlayerController && !PlayerController->IsLocalPlayerController())
                {
                    SetOwner(PlayerController);
                    break;
                }
            }
    		}
    	}
    ), 10.0f, false, -1.0f);
    	}
    	else
    	{
    		SetOwner(GetWorld()->GetFirstPlayerController()); // Clinet는 Proxy(=서버의 내용물을 복제한 것)에 불과하기 때문에 Clinet에 오너를 설정하는 것은 의미가 없다. 
    		FTimerHandle Handle;
    		GetWorld()->GetTimerManager().SetTimer(Handle, FTimerDelegate::CreateLambda([&]
    			{
    				ServerRPCChangeLightColor();
    			}
    		), 1.0f, true, 0.0f);
    	}
    }
    
    void AABFountain::Tick(float DeltaTime)
    {
    	Super::Tick(DeltaTime);
    
    	if (HasAuthority())
    	{
    		AddActorLocalRotation(FRotator(0.0f, RotationRate * DeltaTime, 0.0f));
    		ServerRotationYaw = RootComponent->GetComponentRotation().Yaw;
    	}
    	else
    	{
    		ClientTimeSinceUpdate += DeltaTime;
    		if (ClientTimeBetweenLastUpdate < KINDA_SMALL_NUMBER)
    		{
    			return;
    		}
    
    		const float EstimateRotationYaw = ServerRotationYaw + RotationRate * ClientTimeBetweenLastUpdate;
    		const float LerpRatio = ClientTimeSinceUpdate / ClientTimeBetweenLastUpdate;
    
    		FRotator ClientRotator = RootComponent->GetComponentRotation();
    		const float ClientNewYaw = FMath::Lerp(ServerRotationYaw, EstimateRotationYaw, LerpRatio);
    		ClientRotator.Yaw = ClientNewYaw;
    		RootComponent->SetWorldRotation(ClientRotator);
    	}
    }
    
    void AABFountain::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
    {
    	Super::GetLifetimeReplicatedProps(OutLifetimeProps);
    
    	DOREPLIFETIME(AABFountain, ServerRotationYaw);
    	DOREPLIFETIME(AABFountain, ServerLightColor);
    }
    
    //void AABFountain::OnActorChannelOpen(FInBunch& InBunch, UNetConnection* Connection)
    //{
    //	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("Begin"));
    //
    //	Super::OnActorChannelOpen(InBunch, Connection);
    //
    //	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("End"));
    //}
    //
    //bool AABFountain::IsNetRelevantFor(const AActor* RealViewer, const AActor* ViewTarget, const FVector& SrcLocation) const
    //{
    //	bool NetRelevantResult = Super::IsNetRelevantFor(RealViewer, ViewTarget, SrcLocation);
    //	if (!NetRelevantResult)
    //	{
    //		AB_LOG(LogABNetwork, Log, TEXT("Not Relevant:[%s] %s"), *RealViewer->GetName(), *SrcLocation.ToCompactString());
    //	}
    //
    //	return NetRelevantResult;
    //}
    //
    //void AABFountain::PreReplication(IRepChangedPropertyTracker& ChangedPropertyTracker)
    //{
    //	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("Begin"));
    //	Super::PreReplication(ChangedPropertyTracker);
    //}
    
    void AABFountain::OnRep_ServerRotationYaw()
    {
    	//AB_LOG(LogABNetwork, Log, TEXT("Yaw : %f"), ServerRotationYaw);
    
    	FRotator NewRotator = RootComponent->GetComponentRotation();
    	NewRotator.Yaw = ServerRotationYaw;
    	RootComponent->SetWorldRotation(NewRotator);
    
    	ClientTimeBetweenLastUpdate = ClientTimeSinceUpdate;
    	ClientTimeSinceUpdate = 0.0f;
    }
    
    void AABFountain::OnRep_ServerLightColor()
    {
    	if (!HasAuthority())
    	{
    		AB_LOG(LogABNetwork, Log, TEXT("LightColor : %s"), *ServerLightColor.ToString());
    	}
    
    	UPointLightComponent* PointLight = Cast<UPointLightComponent>(GetComponentByClass(UPointLightComponent::StaticClass()));
    	if (PointLight)
    	{
    		PointLight->SetLightColor(ServerLightColor);
    	}
    }
    
    void AABFountain::ServerRPCChangeLightColor_Implementation()
    {
    	const FLinearColor NewLightColor = FLinearColor(FMath::RandRange(0.0f, 1.0f), FMath::RandRange(0.0f, 1.0f), FMath::RandRange(0.0f, 1.0f), 1.0f);
    	MulticastRPCChangeLightColor(NewLightColor); // 서버가 모든 클라이언트에게 바뀐 분수대 빛을 적용하라고 명령
    }
    
    void AABFountain::MulticastRPCChangeLightColor_Implementation(const FLinearColor& NewLightColor)
    {
    	AB_LOG(LogABNetwork, Log, TEXT("LightColor : %s"), *NewLightColor.ToString());
    
    	UPointLightComponent* PointLight = Cast<UPointLightComponent>(GetComponentByClass(UPointLightComponent::StaticClass()));
    	if (PointLight)
    	{
    		PointLight->SetLightColor(NewLightColor);
    	}
    }

     

     

     

    예시#3 

     

    ABFountain.h

    더보기
    #pragma once
    #include "CoreMinimal.h"
    #include "GameFramework/Actor.h"
    #include "ABFountain.generated.h"
    
    UCLASS()
    class ARENABATTLE_API AABFountain : public AActor
    {
    	GENERATED_BODY()
    	
    public:	
    	AABFountain();
    
    protected:
    	virtual void BeginPlay() override;
    
    public:	
    	virtual void Tick(float DeltaTime) override;
    
    	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = Mesh)
    	TObjectPtr<class UStaticMeshComponent> Body;
    
    	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = Mesh)
    	TObjectPtr<class UStaticMeshComponent> Water;
    
    public:
    	virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
    	//virtual void OnActorChannelOpen(class FInBunch& InBunch, class UNetConnection* Connection) override;
    	//virtual bool IsNetRelevantFor(const AActor* RealViewer, const AActor* ViewTarget, const FVector& SrcLocation) const override;
    	//virtual void PreReplication(IRepChangedPropertyTracker& ChangedPropertyTracker) override;
    
    	UPROPERTY(ReplicatedUsing = OnRep_ServerRotationYaw)
    	float ServerRotationYaw;
    
    	UPROPERTY(ReplicatedUsing = OnRep_ServerLightColor)
    	FLinearColor ServerLightColor;
    
    	UFUNCTION()
    	void OnRep_ServerRotationYaw();
    
    	UFUNCTION()
    	void OnRep_ServerLightColor();
    
    	UFUNCTION(NetMulticast, Unreliable)
    	void MulticastRPCChangeLightColor(const FLinearColor& NewLightColor);
        
        UFUNCTION(Server, Unreliable)
        void ServerRPCChangeLightColor_Implementation()
    
    	float RotationRate = 30.0f;
    	float ClientTimeSinceUpdate = 0.0f;
    	float ClientTimeBetweenLastUpdate = 0.0f;
    };

     

     

     

    ABFountain.cpp

    더보기
    #include "Prop/ABFountain.h"
    #include "Components/StaticMeshComponent.h"
    #include "Components/PointLightComponent.h"
    #include "Net/UnrealNetwork.h"
    #include "ArenaBattle.h"
    #include "EngineUtils.h"
    
    AABFountain::AABFountain()
    {
     	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
    	PrimaryActorTick.bCanEverTick = true;
    
    	Body = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Body"));
    	Water = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Water"));
    
    	RootComponent = Body;
    	Water->SetupAttachment(Body);
    	Water->SetRelativeLocation(FVector(0.0f, 0.0f, 132.0f));
    
    	static ConstructorHelpers::FObjectFinder<UStaticMesh> BodyMeshRef(TEXT("/Script/Engine.StaticMesh'/Game/ArenaBattle/Environment/Props/SM_Plains_Castle_Fountain_01.SM_Plains_Castle_Fountain_01'"));
    	if (BodyMeshRef.Object)
    	{
    		Body->SetStaticMesh(BodyMeshRef.Object);
    	}
    
    	static ConstructorHelpers::FObjectFinder<UStaticMesh> WaterMeshRef(TEXT("/Script/Engine.StaticMesh'/Game/ArenaBattle/Environment/Props/SM_Plains_Fountain_02.SM_Plains_Fountain_02'"));
    	if (WaterMeshRef.Object)
    	{
    		Water->SetStaticMesh(WaterMeshRef.Object);
    	}
    
    	bReplicates = true;
    	NetUpdateFrequency = 1.0f;
    	NetCullDistanceSquared = 4000000.0f;
    	//NetDormancy = DORM_Initial;
    }
    
    // Called when the game starts or when spawned
    void AABFountain::BeginPlay()
    {
    	Super::BeginPlay();
    	
    	if (HasAuthority())
    	{
        	FTimerHandle Handle;
            GetWorld()->GetTimerManager().SetTimer(Handle, FTimerDelegate::CreateLambda([&]
                {
                    const FLinearColor NewLightColor = FLinearColor(FMath::RandRange(0.0f, 1.0f), FMath::RandRange(0.0f, 1.0f), FMath::RandRange(0.0f, 1.0f), 1.0f);
                    //MulticastRPCChangeLightColor(NewLightColor);
                    ClientRPCChangeLightColor(NewLightColor);
                }
            ), 1.0f, true, 0.0f);
    
    		FTimerHandle Handle2;
    		GetWorld()->GetTimerManager().SetTimer(Handle2, FTimerDelegate::CreateLambda([&]
    		{
            // 방법#1
    		//FlushNetDormancy();
    		//for (FConstPlayerControllerIterator Iterator = GetWorld()->GetPlayerControllerIterator(); Iterator; ++Iterator)
    		//{
    		//	APlayerController* PlayerController = Iterator->Get();
    		//	if (PlayerController && !PlayerController->IsLocalPlayerController())
    		//	{
    		//		SetOwner(PlayerController);
    		//		break;
    		//	}
    		//}
            
            // 방법#2
            for (APlayerController* PlayerController : TActorRange<APlayerController>(GetWorld()))
            {
                if (PlayerController && !PlayerController->IsLocalPlayerController())
                {
                    SetOwner(PlayerController);
                    break;
                }
            }
    		}
    	}
    	), 10.0f, false, -1.0f);
    	}
    	else
    	{
    		SetOwner(GetWorld()->GetFirstPlayerController()); // Clinet는 Proxy(=서버의 내용물을 복제한 것)에 불과하기 때문에 Clinet에 오너를 설정하는 것은 의미가 없다. 
    		FTimerHandle Handle;
    		GetWorld()->GetTimerManager().SetTimer(Handle, FTimerDelegate::CreateLambda([&]
    			{
    				ServerRPCChangeLightColor();
    			}
    		), 1.0f, true, 0.0f);
    	}
    }
    
    void AABFountain::Tick(float DeltaTime)
    {
    	Super::Tick(DeltaTime);
    
    	if (HasAuthority())
    	{
    		AddActorLocalRotation(FRotator(0.0f, RotationRate * DeltaTime, 0.0f));
    		ServerRotationYaw = RootComponent->GetComponentRotation().Yaw;
    	}
    	else
    	{
    		ClientTimeSinceUpdate += DeltaTime;
    		if (ClientTimeBetweenLastUpdate < KINDA_SMALL_NUMBER)
    		{
    			return;
    		}
    
    		const float EstimateRotationYaw = ServerRotationYaw + RotationRate * ClientTimeBetweenLastUpdate;
    		const float LerpRatio = ClientTimeSinceUpdate / ClientTimeBetweenLastUpdate;
    
    		FRotator ClientRotator = RootComponent->GetComponentRotation();
    		const float ClientNewYaw = FMath::Lerp(ServerRotationYaw, EstimateRotationYaw, LerpRatio);
    		ClientRotator.Yaw = ClientNewYaw;
    		RootComponent->SetWorldRotation(ClientRotator);
    	}
    }
    
    void AABFountain::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
    {
    	Super::GetLifetimeReplicatedProps(OutLifetimeProps);
    
    	DOREPLIFETIME(AABFountain, ServerRotationYaw);
    	DOREPLIFETIME(AABFountain, ServerLightColor);
    }
    
    //void AABFountain::OnActorChannelOpen(FInBunch& InBunch, UNetConnection* Connection)
    //{
    //	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("Begin"));
    //
    //	Super::OnActorChannelOpen(InBunch, Connection);
    //
    //	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("End"));
    //}
    //
    //bool AABFountain::IsNetRelevantFor(const AActor* RealViewer, const AActor* ViewTarget, const FVector& SrcLocation) const
    //{
    //	bool NetRelevantResult = Super::IsNetRelevantFor(RealViewer, ViewTarget, SrcLocation);
    //	if (!NetRelevantResult)
    //	{
    //		AB_LOG(LogABNetwork, Log, TEXT("Not Relevant:[%s] %s"), *RealViewer->GetName(), *SrcLocation.ToCompactString());
    //	}
    //
    //	return NetRelevantResult;
    //}
    //
    //void AABFountain::PreReplication(IRepChangedPropertyTracker& ChangedPropertyTracker)
    //{
    //	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("Begin"));
    //	Super::PreReplication(ChangedPropertyTracker);
    //}
    
    void AABFountain::OnRep_ServerRotationYaw()
    {
    	//AB_LOG(LogABNetwork, Log, TEXT("Yaw : %f"), ServerRotationYaw);
    
    	FRotator NewRotator = RootComponent->GetComponentRotation();
    	NewRotator.Yaw = ServerRotationYaw;
    	RootComponent->SetWorldRotation(NewRotator);
    
    	ClientTimeBetweenLastUpdate = ClientTimeSinceUpdate;
    	ClientTimeSinceUpdate = 0.0f;
    }
    
    void AABFountain::OnRep_ServerLightColor()
    {
    	if (!HasAuthority())
    	{
    		AB_LOG(LogABNetwork, Log, TEXT("LightColor : %s"), *ServerLightColor.ToString());
    	}
    
    	UPointLightComponent* PointLight = Cast<UPointLightComponent>(GetComponentByClass(UPointLightComponent::StaticClass()));
    	if (PointLight)
    	{
    		PointLight->SetLightColor(ServerLightColor);
    	}
    }
    
    void AABFountain::ServerRPCChangeLightColor_Implementation()
    {
    	const FLinearColor NewLightColor = FLinearColor(FMath::RandRange(0.0f, 1.0f), FMath::RandRange(0.0f, 1.0f), FMath::RandRange(0.0f, 1.0f), 1.0f);
    	MulticastRPCChangeLightColor(NewLightColor);
    }
    
    void AABFountain::MulticastRPCChangeLightColor_Implementation(const FLinearColor& NewLightColor)
    {
    	AB_LOG(LogABNetwork, Log, TEXT("LightColor : %s"), *NewLightColor.ToString());
    
    	UPointLightComponent* PointLight = Cast<UPointLightComponent>(GetComponentByClass(UPointLightComponent::StaticClass()));
    	if (PointLight)
    	{
    		PointLight->SetLightColor(NewLightColor);
    	}
    }

     

     

    ※ 참고:  TActorRange< 클래스타입 >(GetWorld())

    • 월드 내에 존재하는 해당 클래스타입의 객체를 다 가져온다.

           for (APlayerController* PlayerController : TActorRange<APlayerController>(GetWorld()))
            {
                if (PlayerController && !PlayerController->IsLocalPlayerController())
                {
                    SetOwner(PlayerController);
                    break;
                }
            }


     

     

     

    GetNetConnction 코드 :  Actor, Pawn, PlayerController

     

    Actor.cpp

    Owner가 있을때는 Owner의 GetNetConnection을 리턴하고 없을때는 nullptr 리턴.

     

    Pawn.cpp

    Pawn의 GetNetConnection이 있을때는 Pawn의 GetNetConnection을 리턴하고 없을때는 부모클래스인 Actor의 GetNetConnection을 리턴한다.

     

    PlayerController.cpp

    Player가 있으면(=PlayerController가 있고 오너십이 있으면) NetConnection을 반환한다.

     

     

     

    Owner를 Pawn으로 설정하거나 PlayerController로 설정하면 해당 Actor는 Ownership을 가지게 된다


     

     

    실행화면

     

     

     


     

     

    RPC 사용 시 주의사항


     

     

    Reliable  vs.  Unreliable 

     

    • Reliable 기능을 과도하게 사용하면 해당 기능의 대기열이 넘칠 수 있다. 이로 인해 강제로 연결이 끊어지는 상황이 발생할 수 있다.
    • 프레임 단위(ex. Tick)로 Replicated된 함수를 호출하는 경우, Unreliable로 설정해야 한다.
    • 플레이어 입력에 바인딩된 Reliable함수가 있는 경우, 플레이어가 해당 함수를 호출할 수 있는 빈도를 제한해야 한다.
    • Reliable는 자주 호출되지 않는 함수에 적합하다.
      • ex. 충돌 이벤트, 무기 발사 시작/종료, 액터 스

     

     

    https://docs.unrealengine.com/5.1/en-US/networking-overview-for-unreal-engine/

     

    Networking Overview

    Setting up networked games for multiplayer.

    docs.unrealengine.com


     

     

    Actor를 네트워크로 복제하고 싶은 경우 메뉴얼

     

    리플리케이티드 액터 체크리스트

    리플리케이티드 액터를 생성하려면, 다음 단계를 따르세요:

    • 액터의 리플리케이트됨(Replicated) 세팅을 True로 설정한다. (캐릭터의 경우 기본값으로 True가 설정되어 있다.)
    • 리플리케이티드 액터가 이동해야 하면, 무브먼트 리플리케이트(Replicates Movement)를 True로 설정한다.
    • 리플리케이티드 액터를 스폰하거나 소멸할 때, 서버에서 해야 한다.
    • 리플리케이트할 컴퓨터 간에 공유되어야 하는 모든 변수를 설정합니다. 이러한 변수는 보통 게임플레이 필수 변수다.
    • 언리얼 엔진의 사전 제작 무브먼트 컴포넌트(Movement Components)는 이미 리플리케이션용으로 구축되어 있으므로, 가급적 사용한다.
    • 서버-오소리티 모델을 사용한다면, 플레이어가 수행할 수 있는 모든 새로운 액션은 서버 함수가 트리거하도록 해야 한다.

     

    To create a replicated Actor, follow these steps:

    • Set the Actor's Replicated setting to True.
    • If the replicated Actor needs to move, set Replicates Movement to True.
    • When you spawn or destroy a replicated Actor, ensure that you do it on the server.
    • Set any variables that must be shared between machines to replicate. This usually applies to gameplay-essential variables.
    • Use Unreal Engine's pre-made Movement Components whenever possible, as they are already built for replication.
    • If you are using a server-authoritative model, make sure any new actions that the player can perform are triggered by Server functions.

     

     

    Networking Tips

     

    네트워킹 팁

    • RPC나 리플리케이티드 블루프린트 함수는 최대한 적게 사용한다. 대신 RepNotify를 사용할 수 있다면, 그렇게 해야 한다.
    • 멀티캐스트 함수는 세션에 접속된 각 클라이언트에 대해 추가 네트워크 트래픽을 생성하므로 특히 사용을 자제한다.
    • 리플리케이트되지 않은 함수가 서버에서만 실행된다는 것을 보장할 수 있다면, 서버 전용 로직을 서버 RPC에 꼭 포함하지 않아도 된다.
    • Reliable RPC를 플레이어 입력에 바인딩할 때는 신중하게 결정한다. 플레이어는 매우 빠르게 반복해서 버튼을 누르므로 신뢰성 RPC의 큐가 오버플로우된다. 플레이어가 이를 활성화할 수 있는 빈도를 어떻게든 제한해야 한다.
    • 게임이 Tick에서처럼 RPC나 리플리케이티드 함수를 매우 자주 호출한다면, 이를 Unreliable으로 만들어야 한다.
    • 일부 함수는 게임플레이 로직에 대한 응답으로 호출한 다음 RepNotify에 대한 응답으로 호출하여 클라이언트와 서버가 병렬 실행하게 함으로써 재활용할 수 있다.(= 서버와 클라이언트가 같이 실행되게 만들 수 있다.)
    • 액터의 네트워크 역할을 확인하여 ROLE_Authority 인지 아닌지 알 수 있다. 이는 서버와 클라이언트 양쪽에서 활성화되는 함수 실행을 필터링하는 데 유용한 방법이다.
    • Pawn이 C++의 IsLocallyControlled 함수나 블루프린트의 Is Locally Controlled 함수를 사용하여 로컬로 제어되는지 확인할 수 있다. 이는 소유 클라이언트와의 관련 여부에 따라 실행을 필터링하는 데 유용하다.
    • 생성 중에는 Pawn에 할당된 컨트롤러가 없을 수 있으므로 생성자 스크립트에서는 IsLocallyControlled 를 사용하지 않도록 한다.
      • 생성자 스크립트나 생성자 같은 경우에는 엔진이 초기화되는 시점에서 호출되기 때문에 Pawn과 Controller 개념 자체도 없는 상황이다. 그 상황에서 IsLocallycontrolled() 함수를 사용하면 당연히 제대로 동작하지 않는다.

     

    Networking Tips

    • Use as few RPCs or replicated Blueprint functions as possible. If you can use a RepNotify instead, you should.
    • Use Multicast functions especially sparingly, as they create extra network traffic for each connected client in a session.
    • Server-only logic does not necessarily have to be contained in a server RPC if you can guarantee that a non-replicated function will only execute on the server.
    • Be cautious when binding Reliable RPCs to player input. Players can repeatedly press buttons very rapidly, and that will overflow the queue for Reliable RPCs. You should use some way of limiting how often players can activate these.
    • If your game calls an RPC or replicated function very often, such as on Tick, you should make it Unreliable.
    • Some functions can be recycled by calling them in response to gameplay logic, then calling them in response to a RepNotify to ensure that clients and servers have parallel execution.
    • You can check an Actor's network role to see if it is ROLE_Authority or not. This is a useful method for filtering execution in functions that activate on both server and client.
    • You can check if a Pawn is locally controlled by using the IsLocallyControlled function in C++ or the Is Locally Controlled function in Blueprint. This is useful for filtering execution based on whether it is relevant to the owning client.
    • Avoid using IsLocallyControlled in constructor scripts, as it is possible for a Pawn not to have a Controller assigned during construction.

     

     

    RPC 사용시 주의할 점 정리

     

    각  RPC 종류마다 올바르게 사용할 것

    • Client, Netmulticast는 서버에서만 호출
    • Server는 Client에서 호출하지만, 플레이어로 참여하는 리슨서버의 경우 호출 가능
    • Client, Server는 Ownership을 가지고 있는 액터에서 호출
      •  Controller:  IsLocalController 함수
      •  Pawn:         IsLocallyControlled 함수
    • Tick 및 빈번하게 호출되는 함수에 Reliable RPC를 사용하지 말 것
    • NetMulticast RPC의 잦은 사용은 네트웍 부하를 가중시키니 신중을 기할 것
    • 게임 플레이 및 액터 상태에 영향을 미치는 중요한 데이터를 다루는 경우 RPC보다 Property Replication을 사용할 것

     

     

    RPC 사용 방법:   Server  vs. Client

     

    • HasAuthority 함수를 사용해 호출 지점에 대한 파악
    • IsLocalController 또는 IsLocallyControlled 함수를 사용해 Ownership을 파악
    • NetMulticast의 경우, Ownership과 무관하게 연관성으로 동작하기 때문에 모든 경우에 동작한다.

     

    초록색:  클라이언트 쪽에서 실행

    보라색:  서버 쪽에서 실행

    주황색:  Ownership과 무관하게 연관성으로 동작

     

    Server에서 RPC를 호출한 경우 

       NetMulticast Server Client
    Client 소유 Server + 모든 Client Server Only 소유한 Client
    Server 소유 Server + 모든 Client 소유한 Server Server Only
    소유 없음 Server + 모든 Client Server Only Server Only

     

     

    Client에서 RPC를 호출한 경우 

       NetMulticast Server Client
    호출한 Client 소유 호출한 Client Only Server 호출한 Client Only
    다른 Client 소유 호출한 Client Only 버려짐 호출한 Client Only
    Server 소유 호출한 Client Only 버려짐 호출한 Client Only
    소유 없음 호출한 Client Only 버려짐  

     


     

     

    Property Replication  vs.  NetMultiast RPC

     

    유사점

    • 서버와 모든 클라이언트의 지정한 함수를 호출할 수 있음
    • 지정한 데이터 전송을 보장할 수 있음
    • 액터의 오너시과 무관하게 연관성으로 동작함 

     

    차이점

    • Proeprty Replication으로 설정한 데이터는 클라이언트에 반드시 동기화됨.
      ( RPC 전송의 Reliablity와 다른 개념 )
    • NetMulticast RPC를 호출한 타이밍에 클라이언트가 없으면 해당 데이터를 받을 길이 없음. 휘발되서 날아감.
    • 반면에 Property Replication의 경우, 해당 값을 보존하고 있고 새롭게 클라이언트가 접속할 때 보존한 값을 갱신할 수 있게 그 데이터를 보내준다.

     

    정리

    • Proeprty Replication은 게임에 영향을 미치는 데이터에 사용함. ( Gameplay Property )
    • NetMulticast RPC는 게임과 무관한 휘발성 데이터에 사용함. ( Cosmetic )