PART 1 · 강의 1/4

UObject 생성과 소멸

NewObject, CreateDefaultSubobject, ConstructorHelpers, CDO의 동작 원리와 GC와의 관계를 이해합니다

SECTION 01

NewObject를 통한 UObject 생성

런타임에서 UObject를 생성하는 가장 기본적인 방법

NewObject<T>() 함수 시그니처

UE5에서 NewObject는 UObject 파생 클래스의 인스턴스를 생성하는 핵심 팩토리 함수입니다. 내부적으로 StaticConstructObject_Internal을 호출하며, 생성된 객체는 자동으로 GUObjectArray에 등록됩니다.

C++ // 기본 사용법 UMyObject* Obj = NewObject<UMyObject>(this); // Outer 지정 + 이름 지정 UMyObject* NamedObj = NewObject<UMyObject>( GetTransientPackage(), // Outer UMyObject::StaticClass(), // Class FName("MyUniqueObject") // Name ); // Template 기반 생성 (CDO 복제) UMyObject* TemplateObj = NewObject<UMyObject>( this, UMyObject::StaticClass(), NAME_None, RF_NoFlags, ExistingTemplate // 템플릿 객체 );
핵심 포인트

Outer 객체는 소유권 계층을 형성합니다. Outer가 GC에 의해 수집되면, 다른 참조가 없는 한 Inner 객체도 함께 수집 대상이 됩니다. GetTransientPackage()를 Outer로 사용하면 패키지 레벨의 수명을 갖게 됩니다.

NewObject 내부 흐름

단계 함수 설명
1 StaticConstructObject_Internal 파라미터 검증 및 실제 생성 로직 진입
2 StaticAllocateObject 메모리 할당, GUObjectArray에 슬롯 예약
3 FUObjectAllocator::AllocateUObject FUObjectItem 구조체 할당 및 초기화
4 UObject::UObject() C++ 생성자 호출
5 InitProperties CDO 기반 프로퍼티 초기화
6 PostInitProperties 사용자 정의 초기화 콜백
SECTION 02

CreateDefaultSubobject

생성자 내부에서만 사용 가능한 서브오브젝트 생성 메커니즘

생성자 전용 팩토리 함수

CreateDefaultSubobjectCDO(Class Default Object) 생성 과정에서만 호출할 수 있는 특별한 팩토리입니다. 런타임에서 호출하면 assertion이 발생합니다.

C++ AMyActor::AMyActor() { // 컴포넌트 생성 - 생성자에서만 가능 RootComp = CreateDefaultSubobject<USceneComponent>( TEXT("RootComp") ); SetRootComponent(RootComp); MeshComp = CreateDefaultSubobject<UStaticMeshComponent>( TEXT("MeshComp") ); MeshComp->SetupAttachment(RootComp); // 옵션: bTransient = true이면 저장되지 않음 TransientComp = CreateDefaultSubobject<UMyComponent>( TEXT("TransientComp"), /*bTransient=*/ true ); }
주의사항

CreateDefaultSubobject로 생성된 서브오브젝트는 CDO에 포함되며, 인스턴스 생성 시 CDO로부터 복제됩니다. 따라서 생성자 바깥에서 호출하면 안 됩니다. 런타임 서브오브젝트 생성에는 NewObject를 사용하세요.

NewObject vs CreateDefaultSubobject 비교

항목 NewObject CreateDefaultSubobject
호출 시점 언제든 가능 생성자에서만 가능
CDO 포함 불포함 포함
직렬화 수동 관리 필요 자동 직렬화
GC 추적 UPROPERTY 필요 Outer로 자동 추적
블루프린트 디테일 패널 미노출 디테일 패널 노출
SECTION 03

ConstructorHelpers

생성자에서 에셋을 참조하는 안전한 패턴

FObjectFinder와 FClassFinder

ConstructorHelpers는 생성자에서 에셋 경로를 통해 UObject를 찾아 참조하는 유틸리티입니다. 런타임이 아닌 CDO 생성 시점에 에셋을 로드하므로, 패키지 로딩과 GC에 직접적인 영향을 미칩니다.

C++ AMyActor::AMyActor() { // UObject 에셋 찾기 static ConstructorHelpers::FObjectFinder<UStaticMesh> MeshAsset(TEXT("/Game/Meshes/SM_Cube")); if (MeshAsset.Succeeded()) { MeshComp->SetStaticMesh(MeshAsset.Object); } // UClass 에셋 찾기 (블루프린트 클래스) static ConstructorHelpers::FClassFinder<APawn> PawnClass(TEXT("/Game/Blueprints/BP_MyPawn")); if (PawnClass.Succeeded()) { DefaultPawnClass = PawnClass.Class; } }
GC와의 관계

static 키워드로 선언된 ConstructorHelpers는 프로그램 종료까지 에셋에 대한 강한 참조를 유지합니다. 이는 해당 에셋이 절대 GC되지 않는다는 것을 의미합니다. 불필요한 에셋이 메모리에 상주하는 것을 방지하려면 TSoftObjectPtr를 사용한 지연 로딩을 고려하세요.

에셋 참조 방식별 GC 영향

방식 로드 시점 GC 가능 여부 권장 상황
FObjectFinder CDO 생성 시 불가 (static) 항상 필요한 핵심 에셋
TSoftObjectPtr 명시적 로드 가능 조건부 필요 에셋
FSoftObjectPath 명시적 로드 가능 데이터 테이블, 설정
LoadObject 호출 시점 참조 의존 런타임 동적 로딩
SECTION 04

Class Default Object (CDO)

UObject 시스템의 핵심, CDO의 역할과 GC에서의 위치

CDO란 무엇인가?

모든 UClass는 하나의 CDO(Class Default Object)를 가집니다. CDO는 해당 클래스의 "원형(archetype)"으로, 새 인스턴스 생성 시 프로퍼티 초기값의 원본이 됩니다. CDO는 RF_ClassDefaultObject 플래그를 가지며, GC의 Root Set에 포함되어 절대 수집되지 않습니다.

C++ // CDO 접근 방법 const UMyObject* CDO = GetDefault<UMyObject>(); // Mutable CDO 접근 (에디터 전용) UMyObject* MutableCDO = GetMutableDefault<UMyObject>(); // UClass를 통한 CDO 접근 UMyObject* CDO2 = UMyObject::StaticClass()->GetDefaultObject<UMyObject>(); // CDO 여부 확인 if (MyObj->HasAnyFlags(RF_ClassDefaultObject)) { // 이 객체는 CDO입니다 }

CDO와 GC의 관계

CDO의 GC 특성

CDO는 다음과 같은 GC 특성을 가집니다:

  • Root Set 포함: UClass가 로드된 동안 CDO는 절대 GC되지 않음
  • 참조 전파: CDO가 참조하는 모든 UObject도 GC에서 보호됨
  • 블루프린트 CDO: BP 클래스의 CDO는 BP 에셋 언로드 시에만 해제 가능
  • 메모리 영향: CDO의 프로퍼티에 큰 에셋 참조가 있으면 해당 에셋도 메모리에 상주

CDO 생성 흐름

생성 순서 // 1. 엔진 초기화 시 또는 클래스 최초 사용 시 UClass::CreateDefaultObject() ├── StaticAllocateObject() // 메모리 할당 ├── UObject::UObject() // 기본 생성자 호출 ├── InitProperties(CDO) // 부모 CDO에서 프로퍼티 복사 ├── PostInitProperties() // 사용자 초기화 └── PostCDOContruct() // CDO 전용 후처리 // 2. 이후 인스턴스 생성 시 NewObject<T>() ├── StaticAllocateObject() ├── InitProperties(Instance) // CDO에서 프로퍼티 복사! └── PostInitProperties()
SECTION 05

UObject 소멸 프로세스

UObject가 파괴되는 과정과 GC의 역할

소멸 단계별 상세 분석

UObject의 소멸은 C++의 delete와 달리 비동기적 다단계 프로세스입니다. GC가 Unreachable로 판정한 객체는 즉시 메모리에서 해제되지 않고 여러 단계를 거칩니다.

소멸 흐름 // AActor의 경우 1. MarkAsGarbage() // GC가 Unreachable 마킹 2. ConditionalBeginDestroy() // 소멸 시작 조건 확인 3. BeginDestroy() // 리소스 해제 시작 (오버라이드 가능) 4. IsReadyForFinishDestroy() // 비동기 작업 완료 확인 5. FinishDestroy() // 최종 정리 (오버라이드 가능) 6. ~UObject() // C++ 소멸자 7. FreeUObjectMemory() // 메모리 반환 // AActor 전용 추가 단계 Destroy() // 명시적 파괴 요청 └── World->DestroyActor() ├── OnDestroyed 브로드캐스트 ├── 컴포넌트 UnregisterComponent └── MarkAsGarbage() // GC 수집 대상으로 마킹
UE5에서의 PendingKill 변화

UE5에서는 MarkPendingKill()이 deprecated되고 MarkAsGarbage()로 대체되었습니다. gc.PendingKillEnabled=0(기본값)일 때 IsValid()는 MarkAsGarbage된 객체에 대해 false를 반환하지만, raw 포인터 비교는 여전히 유효합니다. TObjectPtr를 사용하면 이 동작이 자동으로 처리됩니다.

SUMMARY

핵심 요약

이 강의에서 배운 내용
  • NewObject<T>()는 런타임 UObject 생성의 표준 방법이며, 내부적으로 GUObjectArray에 자동 등록됩니다
  • CreateDefaultSubobject는 생성자에서만 사용 가능하며, CDO에 포함되어 자동으로 직렬화와 GC 추적이 됩니다
  • ConstructorHelpers의 static 에셋 참조는 GC를 방해하므로, 불필요한 에셋 상주를 피하려면 TSoftObjectPtr를 고려해야 합니다
  • CDO는 GC Root Set에 포함되어 절대 수집되지 않으며, CDO가 참조하는 객체도 보호됩니다
  • UObject 소멸은 BeginDestroy → FinishDestroy의 비동기 다단계 프로세스로 진행됩니다
  • UE5에서 PendingKill은 MarkAsGarbage()로 대체되었으며, TObjectPtr가 이를 자동 처리합니다
PRACTICE

도전 과제

배운 내용을 직접 실습해보세요

실습 1: NewObject와 Outer 관계 실험

UMyTestObject 클래스를 만들고, Outer를 다르게 지정하여 NewObject로 3개의 인스턴스를 생성하세요. GetOuter()로 소유권 계층을 로그에 출력하고, Outer가 Destroy될 때 Inner 객체의 IsValid() 상태가 어떻게 변하는지 확인하세요.

실습 2: CreateDefaultSubobject vs NewObject 비교

AActor 파생 클래스에서 CreateDefaultSubobject로 UStaticMeshComponent를 생성하고, BeginPlay에서 NewObject로 같은 컴포넌트를 생성해보세요. 블루프린트 에디터의 디테일 패널에서 두 컴포넌트의 노출 차이를 확인하세요.

심화 과제: CDO 메모리 영향 프로파일링

ConstructorHelpers::FObjectFinder로 대용량 StaticMesh를 참조하는 Actor와 TSoftObjectPtr로 같은 에셋을 참조하는 Actor를 각각 만들고, stat memory로 두 방식의 메모리 사용량 차이를 비교 분석하세요.