UObject 생성과 소멸
NewObject, CreateDefaultSubobject, ConstructorHelpers, CDO의 동작 원리와 GC와의 관계를 이해합니다
NewObject를 통한 UObject 생성
런타임에서 UObject를 생성하는 가장 기본적인 방법
NewObject<T>() 함수 시그니처
UE5에서 NewObject는 UObject 파생 클래스의 인스턴스를 생성하는 핵심 팩토리 함수입니다. 내부적으로 StaticConstructObject_Internal을 호출하며, 생성된 객체는 자동으로 GUObjectArray에 등록됩니다.
// 기본 사용법
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 |
사용자 정의 초기화 콜백 |
CreateDefaultSubobject
생성자 내부에서만 사용 가능한 서브오브젝트 생성 메커니즘
생성자 전용 팩토리 함수
CreateDefaultSubobject는 CDO(Class Default Object) 생성 과정에서만 호출할 수 있는 특별한 팩토리입니다. 런타임에서 호출하면 assertion이 발생합니다.
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로 자동 추적 |
| 블루프린트 | 디테일 패널 미노출 | 디테일 패널 노출 |
ConstructorHelpers
생성자에서 에셋을 참조하는 안전한 패턴
FObjectFinder와 FClassFinder
ConstructorHelpers는 생성자에서 에셋 경로를 통해 UObject를 찾아 참조하는 유틸리티입니다. 런타임이 아닌 CDO 생성 시점에 에셋을 로드하므로, 패키지 로딩과 GC에 직접적인 영향을 미칩니다.
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;
}
}
static 키워드로 선언된 ConstructorHelpers는 프로그램 종료까지 에셋에 대한 강한 참조를 유지합니다. 이는 해당 에셋이 절대 GC되지 않는다는 것을 의미합니다. 불필요한 에셋이 메모리에 상주하는 것을 방지하려면 TSoftObjectPtr를 사용한 지연 로딩을 고려하세요.
에셋 참조 방식별 GC 영향
| 방식 | 로드 시점 | GC 가능 여부 | 권장 상황 |
|---|---|---|---|
FObjectFinder |
CDO 생성 시 | 불가 (static) | 항상 필요한 핵심 에셋 |
TSoftObjectPtr |
명시적 로드 | 가능 | 조건부 필요 에셋 |
FSoftObjectPath |
명시적 로드 | 가능 | 데이터 테이블, 설정 |
LoadObject |
호출 시점 | 참조 의존 | 런타임 동적 로딩 |
Class Default Object (CDO)
UObject 시스템의 핵심, CDO의 역할과 GC에서의 위치
CDO란 무엇인가?
모든 UClass는 하나의 CDO(Class Default Object)를 가집니다. CDO는 해당 클래스의 "원형(archetype)"으로, 새 인스턴스 생성 시 프로퍼티 초기값의 원본이 됩니다. CDO는 RF_ClassDefaultObject 플래그를 가지며, GC의 Root Set에 포함되어 절대 수집되지 않습니다.
// 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 특성을 가집니다:
- 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()
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에서는 MarkPendingKill()이 deprecated되고 MarkAsGarbage()로 대체되었습니다. gc.PendingKillEnabled=0(기본값)일 때 IsValid()는 MarkAsGarbage된 객체에 대해 false를 반환하지만, raw 포인터 비교는 여전히 유효합니다. TObjectPtr를 사용하면 이 동작이 자동으로 처리됩니다.
핵심 요약
- NewObject<T>()는 런타임 UObject 생성의 표준 방법이며, 내부적으로 GUObjectArray에 자동 등록됩니다
- CreateDefaultSubobject는 생성자에서만 사용 가능하며, CDO에 포함되어 자동으로 직렬화와 GC 추적이 됩니다
- ConstructorHelpers의 static 에셋 참조는 GC를 방해하므로, 불필요한 에셋 상주를 피하려면 TSoftObjectPtr를 고려해야 합니다
- CDO는 GC Root Set에 포함되어 절대 수집되지 않으며, CDO가 참조하는 객체도 보호됩니다
- UObject 소멸은 BeginDestroy → FinishDestroy의 비동기 다단계 프로세스로 진행됩니다
- UE5에서 PendingKill은 MarkAsGarbage()로 대체되었으며, TObjectPtr가 이를 자동 처리합니다
도전 과제
배운 내용을 직접 실습해보세요
UMyTestObject 클래스를 만들고, Outer를 다르게 지정하여 NewObject로 3개의 인스턴스를 생성하세요. GetOuter()로 소유권 계층을 로그에 출력하고, Outer가 Destroy될 때 Inner 객체의 IsValid() 상태가 어떻게 변하는지 확인하세요.
AActor 파생 클래스에서 CreateDefaultSubobject로 UStaticMeshComponent를 생성하고, BeginPlay에서 NewObject로 같은 컴포넌트를 생성해보세요. 블루프린트 에디터의 디테일 패널에서 두 컴포넌트의 노출 차이를 확인하세요.
ConstructorHelpers::FObjectFinder로 대용량 StaticMesh를 참조하는 Actor와 TSoftObjectPtr로 같은 에셋을 참조하는 Actor를 각각 만들고, stat memory로 두 방식의 메모리 사용량 차이를 비교 분석하세요.