Reflection, Garbage Collection

2023. 6. 5. 14:00UnrealEngine/개념

Refection

프로그램이 런타임에 자기 자신을 조사하는 기능
기본 C++는 리플렉션을 지원하지 않음
언리얼엔진은 자체적으로 C++ 클래스, 구조체, 함수, 멤버 변수, 열거형 정보를 수집하는 별도의 리플렉션 시스템이 구축되어있음.
프로퍼티 시스템이라고 함.

어떠한 클래스, 구조체, 함수, 변수, 열거형 정보를 리플렉션에 추가하려면 특수한 주석(매크로)를 달아줘야함.
이러한 주석이 달린 정보들은 UHT(Unreal Header Tool)가 프로젝트를 컴파일 할 때 해당 정보를 수집함.


https://www.unrealengine.com/ko/blog/unreal-property-system-reflection

 

언리얼 프로퍼티 시스템 (리플렉션)

리플렉션(Reflection)은 프로그램이 실행시간에 자기 자신을 조사하는 기능입니다. 이는 엄청나게 유용한 데다 언리얼 엔진 테크놀로지의 근간을 이루는 것으로, 에디터의 디테일 패널, 시리얼라

www.unrealengine.com


Garbage Collection

프로그래밍에서 동적으로 할당했던 메모리 영역은 프로그래머가 직접 할당을 해제해야한다.
가비지 컬렉션은 이러한 동적으로 할당했던 메모리 중 필요없게된 부분을 탐색/판단하여 자동으로 할당을 해제하는 기능이다.
가비지 컬렉션이 성공적으로 수행되기 위해선 프로그램이 런타임 중 자기 자신을 탐색할 수 있어야 하는데 언리얼엔진은 프로퍼티 시스템을 통한 리플렉션으로 해당 기능을 가능하게 한다.


언리언엔진은 오브젝트들을 그래프 형태로 관리하고 해당 그래프들은 "Root Set"라고 지정된 오브젝트들이 존재한다.

각 그래프의 Root Set에서 시작하여 트리탐색을 하여 그래프에 존재하는 모든 오브젝트를 참조하도록 한다.
탐색이 끝났을 때 참조되지 않은 오브젝트들은 필요없는 오브젝트라고 판단하여 메모리 할당을 해제한다.

 

https://docs.unrealengine.com/5.2/ko/unreal-object-handling-in-unreal-engine/

 

언리얼 오브젝트 처리

UObject 시스템의 기능에 대한 개요입니다.

docs.unrealengine.com

 

언리얼 오브젝트(UObject)는 다음과 같은 방식으로 참조 그래프에 추가될 수 있다.

1. 강한 참조(UPROPERTY)로 참조되는 경우(대상 UObject를 참조하는 객체로 참조되고 있어야 한다.)

2. UObject::AddReferencedObjects 호출을 통해 등록되는 경우(호출하는 주체 객체도 참조되고 있어야한다.)
3. UObject::AddToRoot로 루트 집합에 추가되는 경우(일반적으로 많이 쓰이지는 않는다.)

 

UObject가 3개의 경우 중 하나라도 충족시키지 못한다면 참조 그래프 탐색(GC 사이클)을 통해 도달할 수 없는 객체로 마킹될 것이고 가비지 콜렉팅에 의해 제거될 것이다.

UPROPERTY로 등록된 raw pointer의 경우 shared pointer와 유사하게 작동한다.

접근 가능한 객체를 강제로 제거하려면 해당 객체에 대해 MarkPendingKill을 호출하여 다음 GC 주기에서 강제로 제거되도록 할 수 있다.

GC는 게임 스레드에서 실행되므로 함수 호출 중 객체가 제거되지는 않는다.

 

가비지 컬렉터로 수집된 객체를 소멸할때는 다음과 같은 작업을 수행한다.

1. UPROPERTY로 등록된 raw pointer를 nullptr로 세팅한다.
2. UPROPERTY로 등록된 컨테이너(TArray, TSet, TMap), 원소들은 nullptr로 세팅하되 컨테이너가 지워지지는 않는다.

 

여기서 'UPROPERTY로 등록된' raw pointer에 대해서만 이러한 작업이 수행된다는 점에 유의하여야 한다.

(UActorComponent나 AActor의경우 언리얼 엔진 컨테이너 클래스에 저장된 레퍼런스라 이러한 작업이 적용된다.)

 

//..
UPROPERTY()
UObject* obj1;
UObject* obj2;

void UTestActor::Init()
{
	obj1 = NewObject<UObject>();
	obj2 = NewObject<UObject>();
}

void UTestActor::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
	Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
	if (obj1 != nullptr)
	{
		UE_LOG(LogTemp, Warning, TEXT("obj1 is alive!"));
	}
	if (obj2 != nullptr)
	{
		UE_LOG(LogTemp, Warning, TEXT("obj2 is alive!"));
	}
}

// 다른 컴포넌트에서 GEngine->ForceGarbageCollection(true); 호출

다음의 예제에서 우리는 "obj1 is alive!"라는 로그와 "obj2 is alive!"라는 로그가 지속적으로 출력되다가

다른 컴포넌트에서 GC를 실행한 이후 UPROPERTY로 지정되지 않은 obj2의 객체가 소멸되면서 "obj1 is alive!"라는 로그만 출력되길 기대할 것이다.

 

하지만 GC 실행 이후에도 동일하게 "obj2 is alive!"라는 로그도 지속적으로 출력된다.

obj2 객체를 참조하는데 사용한 포인터가 raw pointer이기 때문에 GC로 인해 실제 객체의 메모리는 제거되었음에도 obj2는 nullptr로 세팅되지 않고 텅 빈 공간을 가리키고 있게 되는 것이다.

이러한 경우는 메모리 관련 크래시를 유발하므로 절대적으로 피해야한다.

 

그렇다고 모든 오브젝트의 포인터를 UPROPERTY로 지정하라는 의미는 아니다.

오브젝트를 참조 그래프에 추가하진 않으면서 null 캐치를 할 수 있게 해주는 포인터가 존재하는데 바로 TWeakObjectPtr이다.


TWeakObjectPtr

 

앞서 말한대로 언리얼 엔진은 자체적인 GC시스템이 존재하기 때문에 메모리 걱정 없이 포인터를 이용할 수 있다. 자체적인 GC의 구현을 위해 '참조 그래프' 형식으로 오브젝트들을 관리하고 이러한 그래프에 오브젝트를 추가하기 위해 UPROPERTY 매크로를 사용한다.

하지만 UPROPERTY는 내부적으로 강한 참조 방법을 사용하기 때문에 해당 객체의 수명에 관여(shared_ptr와 비슷)하게 되고 과하게 사용하면 참조 그래프가 비대해져 GC의 비용이 증가하는 문제가 존재한다.

 

객체의 수명 의존성과 GC의 최적화를 위해서 모든 오브젝트에 대해 UPROPERTY의 사용은 자제해야 하지만 UPROPERTY를 사용하지 않은 포인터들은 GC에 의해 nullptr로 변하지 않고 제거된 메모리 공간을 그대로 가리키는 단점이 존재한다.

이러한 점을 해결하기 위한 방법이 TWeakObjectPtr를 사용하는 것이다.

 

TWeakObjectPtr는 UPROPERTY와 다르게 오브젝트를 참조 그래프에 추가하지 않아서 GC를 방지하는 기능은 없다. 하지만 접근 전 질의를 통한 유효성 검사가 가능하고 가리키는 오브젝트가 소멸했을 경우 nullptr로 설정하는 기능도 추가된 포인터다.

 

//BaseCharacter.h
class MYDREAM_API ABaseCharacter : public ACharacter
{
	GENERATED_BODY()
protected:
	UPROPERTY(VisibleAnywhere)
	UAttributeComponent* AttributeComponent;
}

//MainOverlay.h
class MYDREAM_API UMainOverlay : public UUserWidget
{
	GENERATED_BODY()
private:
	TWeakObjectPtr<UAttributeComponent> AttributeComponent;
}

//MainOverlay.cpp
void UMainOverlay::UpdateHealthPercent()
{
	if (AttributeComponent.IsValid() && HealthBar) // 접근 전 유효 확인
		HealthBar->SetPercent(AttributeComponent->GetHealthRatio());
}

현재 진행중인 프로젝트의 일부 코드를 발췌한 것이다.

 

MainOverlay에서 AttributeComponent를 사용해야 하는 상황이고 해당 컴포넌트는 자신과 다른 오브젝트의 컴포넌트이므로 접근할때 유효성을 판단하는 것이 중요하다.

UPROPERTY로 지정하게 되면 자신이 갖고있지 않은 컴포넌트의 수명에 대해서 관여하게 되므로 좋은 접근 방법이 아니고 raw pointer를 사용하자니 유효성을 보장할 수가 없다.

(BaseCharacter가 Destroy되어도 MainOverlay에서 AttributeComponent를 UPROPERTY로 참조하고 있으므로 해당 컴포넌트는 할당 해제되지 않을 것이다.)

 

이때 TWeakObjectPtr를 사용하여 동일한 유효성 확인 절차를 기대하면서도 컴포넌트의 수명에 관여하지 않을 수 있고 메모리 누수 방지와 더불어 GC의 코스트를 줄여주는 효과까지 기대할 수 있다.

'UnrealEngine > 개념' 카테고리의 다른 글

Unreal Interface  (0) 2023.03.17
IK 릭 애니메이션 리타기팅  (0) 2023.03.08
Delegate  (0) 2023.03.07
Unreal Foot IK  (0) 2023.03.07
IK(Inverse Kinematic, 역운동학)  (0) 2023.03.07