TIL_096 : UObject, 언리얼 GC, CreateDefaultSubobject/NewObject

김펭귄·2026년 1월 14일

Today What I Learned (TIL)

목록 보기
96/109

1. GC의 대상

  • 언리얼에서는 객체가 UObject를 상속받아야 GC의 관리 대상이 된다

  • 따라서 NewObject<> / SpawnActor<> 같은 엔진 API로 객체를 생성해야 하고, 그 포인터를 UPROPERTY로 들고 있어야 추적 및 수명 관리가 가능하다

  • Root부터 OuterChain을 따라가며 Mark & Sweep 알고리즘으로 관리

  • new로 만들면 안 된다는 것

  • 또한 생성한 객체를 delete로 직접 제거하면 엔진 내부가 망가져서 하면 안 됨

class UMyClass : public UObject
{
    UPROPERTY()  // UPROPERTY 설정
    UObject* SafeRef;
};

void UMyClass::Example()
{
    SafeRef = NewObject<UInventory>();	// 엔진 API로 객체 생성
    
    UObject* obj = NewObject<UObject>();
    obj = nullptr;  // delete가 아니라 참조만 끊으면 GC가 알아서 제거
}

2. UObject의 기능

  1. 매크로를 이용한 블루프린트, 에디터 통합
UCLASS(BlueprintType) 
class MYGAME_API UMyCharacter : public UObject
{
    GENERATED_BODY()
    
    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    // ... //
    UFUNCTION(BlueprintCallable)
    // ... //
};
  1. 자동 저장/불러오기
UPROPERTY(SaveGame)
FName PlayerName;  // 자동으로 세이브 파일에 저장
  1. 네트워크 리플리케이션
UPROPERTY(Replicated)  // 네트워크 동기화
int32 PlayerScore;

// 함수도 네트워크를 통해 호출 가능
UFUNCTION(Server, Reliable)
void ServerFireWeapon();
  1. 리플렉션
  1. 메모리 자동 관리
UCharacter* player = NewObject<UCharacter>();
player = nullptr;

3. UObject 생성 방법 (CreateDefaultSubobject vs NewObject

3.1. CreateDefaultSubobject

  • 객체에 필요한것들을 생성자에서 미리 준비해놓는 것

  • 게임 시작 전부터 존재하고, 에디터와 연동도 됨

  • 보통 컴포넌트 생성에 사용

class AMyActor : public AActor
{
public:
    AMyActor() 
    {
        MeshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("MeshComp"));
    }

private:
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components")
    UStaticMeshComponent* MeshComp;
};
  • 안정적이고 예측 가능

  • 자동으로 객체에 등록되고, 객체 제거시 자동으로 사라짐

3.2. NewObject

  • 런타임에 필요할 때만 동적으로 생성 가능

  • 컴포넌트의 경우 Register Component로 등록해주고, Attach to Component로 붙여줘야하는 등 작업이 필요

TSubclassOf<UMyClass> BlueprintClass; // 블루프린트 클래스 변수

UObject* NewObj = NewObject<UMyClass>(
    this,                   // Outer (주인)
    BlueprintClass.Get(), 	// 클래스 타입
    TEXT("ObjectName")      // 이름 (선택)
);
  • Outer : Outer의 하위체계로 객체를 생성. 즉, GC의 관리체계에서 연결이 이어지도록 해서 Mark과정에서 표시되도록 함

  • <T> : 컴파일 타임에 객체 생성이 안전한지를 확인하게 해줌

  • 매개인자에서의 클래스 : 런타임에 해당 클래스로 생성.
    동적필요 없으면, 정적으로 UMyClass::StaticClass() 처럼 생성도 가능

Outer 종류

  • NewObject<UObject>(GetTransientPackage()) : 파일에 저장은 안 할 임시 객체일 때. 영구보존

  • NewObject<UObject>(GetWorld()) : 레벨소유. 레벨 바뀌면 사라짐

  • NewObject<UObject>(GetGameInstance()) : 게임인스턴스 소유. 영구보존

  • NewObject<UActorComponent>(MyActor) : 액터 소유. 액터의 생명주기와 함께 감

주의할 점

// this -> parent -> child
UObject* Parent = NewObject<UObject>(this);
UObject* Child = NewObject<UObject>(Parent);

// SomeOtherObject -> parent
Parent->Rename(nullptr, OtherObject); 
  • Rename을 통해 Parent의 이름을 바꾸고, OtherObject로 Outer를 설정

  • 그럼 Parent에 달려있던 Child는 고아 객체가 되어버려 에러 발생할 수 있다

  • 따라서, Outer는 설정한 이후에 수정 안 하는 것이 좋고 하게 된다면 Child도 다 해주어야 함

4. UObject의 생명주기(함수)

UCLASS()
class UMyObject : public UObject
{
    GENERATED_BODY()

public:
    virtual void PostInitProperties() override;

    virtual void PostLoad() override;

    virtual void BeginDestroy() override;

    virtual void FinishDestroy() override;
};
  • PostInitProperties : 객체가 생성된 직후, 생성자가 끝난 다음에 호출.
    초기 상태를 수정하거나 유효성 검사할 때 주로 사용

  • PostLoad : 세이브파일(Asset이나 레벨)로부터 이 객체가 메모리에 로드되고 나서 호출.
    로드된 정보 검사할 때 사용

  • BeginDestroy : GC가 삭제하려고 결정한 직후에 호출(레벨에서 사라질때).
    타이머, 델리게이트, 바인딩, 네트워크 등 정리 해제 작업 함

  • FinishDestroy : 메모리에서 제거되기 직전에 호출. 로그 출력할 때 사용

  • Super::함수 빼먹지 말기

5. GC 관리받게 하기 총정리

  1. UPROPERTYUObject 사용하기

  2. Outer Chain 사용

  3. 정말 최후의 방법으로 Root로 설정

// 비동기 로딩 중에 삭제되면 안 되는 객체
class UAssetLoader : public UObject
{
public:
    void LoadAssetAsync(const FSoftObjectPath& AssetPath)
    {
        AddToRoot();    // 로딩 시작 전에 자신을 보호

        // 비동기 로딩 시작
        UAssetManager& AssetManager = UAssetManager::Get();
        StreamableHandle = AssetManager.LoadAssetAsync(AssetPath,
            FStreamableDelegate::CreateUObject(this, &UAssetLoader::OnLoadComplete)
        );
    }

private:
    void OnLoadComplete()
    {
        RemoveFromRoot();  // 로딩 완료 → 보호 해제
    }
};

6. Hard/Weak 레퍼런스와 GC

6.1. 하드 레퍼런스

UCLASS()
class UInventory : public UObject
{
    GENERATED_BODY()

public:
    UPROPERTY()
    TArray<UObject*> Items;  // Hard 레퍼런스
};
  • 하드레퍼런스로 하면, 아이템은 인벤토리에 의해 관리되고, GC도 관리해줌

  • GC가 Items 배열의 아이템들을 다 관리해줌

  • 인벤토리가 사라지면 아이템도 같이 GC에 의해 사라짐

  • 부모-자식 관계처럼 소유 관계가 명확할 때 하드 레퍼런스를 사용

6.2. Weak 레퍼런스

UCLASS()
class UItemWidget : public UObject
{
    GENERATED_BODY()

public:
    UPROPERTY()
    TWeakObjectPtr<AActor> ObservedActor;  // Weak 레퍼런스
};
  • 하드 레퍼런스와 다르게 Weak 레퍼런스는 객체에 대해 읽기만 하지 존재를 책임지진 않음

  • 따라서 GC에 의해 제거될 수 있다

  • 따라서 항상 사용하기 전에 IsValid로 유효성 검사를 해주어야함

  • 책임의 정도가 약할 때, 값을 읽고만 싶을 때(관찰) 소프트 레퍼런스를 사용

6.3. 사용법 비교

// Hard Reference 체크
UPROPERTY()
AActor* MyActor;

void CheckHardRef()
{
    if (IsValid(MyActor))  // 전역 함수 IsValid() 사용
    {
        MyActor->DoSomething();  // 바로 사용
    }
}

// Weak Reference 체크
UPROPERTY()
TWeakObjectPtr<AActor> MyActorWeak;

void CheckWeakRef()
{
    if (MyActorWeak.IsValid())  // 포인터의 멤버 함수 IsValid()
    {
        AActor* Actor = MyActorWeak.Get();  // Get()으로 포인터 획득
        Actor->DoSomething();               // 이제 안전하게 사용
    }
}
  • 하드 레퍼런스의 경우, 전역함수인 IsValid를 통해 사용

  • Weak 레퍼런스의 경우, Weak포인터의 멤버함수인 IsValid를 사용하고 Get멤버함수까지 사용해야 함

profile
반갑습니다

0개의 댓글