PART 1 · 강의 2/4

UObject 메타데이터

FUObjectItem, GUObjectArray, Index 시스템의 구조와 GC에서의 역할을 분석합니다

SECTION 01

GUObjectArray 글로벌 객체 배열

모든 UObject를 추적하는 전역 자료구조

GUObjectArray의 구조

GUObjectArrayFUObjectArray 타입의 전역 변수로, 프로세스 내 모든 UObject의 메타데이터를 저장합니다. GC의 Mark 단계에서 이 배열을 순회하며 Reachability를 분석합니다.

엔진 소스 (간략화) // UObjectArray.h class FUObjectArray { // 청크 기반 할당 (기본 64K 엘리먼트/청크) enum { NumElementsPerChunk = 65536 }; // 실제 저장소: FUObjectItem 배열들의 배열 FChunkedFixedUObjectArray ObjObjects; // 여유 인덱스 리스트 (재사용) TArray<int32> ObjAvailableList; // 리스너 (생성/삭제 알림) TArray<FUObjectCreateListener*> UObjectCreateListeners; TArray<FUObjectDeleteListener*> UObjectDeleteListeners; public: // 객체 인덱스로 접근 FUObjectItem* IndexToObject(int32 Index); // 새 객체 등록 void AllocateUObjectIndex(UObjectBase* Object); // 객체 해제 (인덱스 재사용 큐에 추가) void FreeUObjectIndex(UObjectBase* Object); };
청크 기반 할당의 이점

GUObjectArray는 하나의 거대한 연속 배열이 아니라 청크(Chunk) 단위로 분할됩니다. 이를 통해 메모리 재할당 없이 동적 확장이 가능하며, GC 순회 시 캐시 효율을 높입니다. UE5에서는 청크당 65,536개의 슬롯을 사용합니다.

SECTION 02

FUObjectItem 구조체

개별 UObject의 메타데이터를 저장하는 핵심 구조체

FUObjectItem의 내부 구조

각 UObject는 GUObjectArray 내에서 FUObjectItem으로 표현됩니다. 이 구조체에는 GC에 필요한 모든 플래그와 메타데이터가 포함되어 있습니다.

엔진 소스 (간략화) struct FUObjectItem { // 실제 UObject 포인터 class UObjectBase* Object; // 플래그 비트필드 int32 Flags; // 클러스터 인덱스 (Cluster GC용) int32 ClusterRootIndex; // 시리얼 번호 (약한 참조 검증용) int32 SerialNumber; // 주요 플래그들 enum EFlags { EInternalObjectFlags::RootSet = 1 << 0, // GC 루트 EInternalObjectFlags::GarbageCollectionKeepFlags, EInternalObjectFlags::Unreachable = 1 << 2, // 도달 불가 EInternalObjectFlags::ClusterRoot = 1 << 3, // 클러스터 루트 EInternalObjectFlags::ReachableInCluster = 1 << 4, EInternalObjectFlags::PendingKill = 1 << 5, // (레거시) EInternalObjectFlags::Garbage = 1 << 5, // UE5: 가비지 마킹 }; };

SerialNumber의 역할

SerialNumberTWeakObjectPtr의 유효성 검사에 사용됩니다. 객체가 파괴되고 같은 인덱스 슬롯에 새 객체가 할당되더라도, SerialNumber가 다르므로 Weak 참조가 잘못된 객체를 가리키지 않습니다.

C++ // TWeakObjectPtr 내부 동작 (간략화) class FWeakObjectPtr { int32 ObjectIndex; // GUObjectArray 내 인덱스 int32 ObjectSerialNumber; // 생성 시점의 SerialNumber UObject* Get() const { FUObjectItem* Item = GUObjectArray.IndexToObject(ObjectIndex); if (Item && Item->SerialNumber == ObjectSerialNumber) return Item->Object; // 유효 return nullptr; // 무효 (파괴됨 또는 재사용됨) } };
SECTION 03

InternalIndex와 오브젝트 플래그

UObject의 식별 체계와 GC 관련 플래그 시스템

InternalIndex 시스템

모든 UObject는 생성 시 GUObjectArray 내의 고유 인덱스(InternalIndex)를 부여받습니다. 이 인덱스는 GC, Weak 참조, 네트워킹 등 다양한 시스템에서 객체 식별에 사용됩니다.

C++ // UObjectBase에 저장된 인덱스 class UObjectBase { int32 InternalIndex; // GUObjectArray 내 위치 // 인덱스를 통한 빠른 메타데이터 접근 FUObjectItem* GetUObjectItem() const { return GUObjectArray.IndexToObject(InternalIndex); } }; // 인덱스 재사용 메커니즘 // 객체 파괴 시: 인덱스가 ObjAvailableList에 추가 // 새 객체 생성 시: ObjAvailableList에서 인덱스를 꺼내 재사용 // 빈 슬롯이 없으면: 배열 끝에 새 슬롯 추가

EObjectFlags (외부 플래그)

플래그 GC 영향
RF_NoFlags 0x0 기본 상태, GC 대상
RF_Public 0x01 패키지 외부에서 참조 가능
RF_Standalone 0x02 외부 참조 없이도 존속
RF_Transient 0x4000 직렬화되지 않음, GC는 정상 동작
RF_ClassDefaultObject 0x10 CDO - Root Set에 포함
RF_WasLoaded 0x80 디스크에서 로드됨
RF_Standalone의 활용

RF_Standalone 플래그가 설정된 객체는 다른 객체에서 참조하지 않더라도 GC에 의해 수집되지 않습니다. 에디터에서 에셋을 독립적으로 유지하는 데 사용되며, 런타임에서는 주의해서 사용해야 메모리 누수를 방지할 수 있습니다.

SECTION 04

GC를 위한 객체 순회 최적화

대량의 UObject를 효율적으로 순회하는 엔진 내부 기법

OpenForDisregardForGC

엔진 초기화 시 생성되는 핵심 객체들은 DisregardForGC 영역에 배치됩니다. 이 영역의 객체들은 GC 순회에서 제외되어 성능을 향상시킵니다.

C++ // 엔진 초기화 시 GUObjectArray.OpenDisregardForGC(); // ... 엔진 핵심 객체 생성 (UClass, CDO 등) ... GUObjectArray.CloseDisregardForGC(); // 이 시점 이후 생성된 객체만 GC 대상 // DisregardForGC 설정 확인 int32 MaxObjectsNotConsideredByGC; // DefaultEngine.ini: gc.MaxObjectsNotConsideredByGC // 기본값: 플랫폼별 상이 (PC: ~약 400,000)
GC 성능 최적화 원리

GUObjectArray의 앞부분은 엔진 핵심 객체(UClass, CDO 등)로 채워집니다. CloseDisregardForGC() 이후에 생성된 객체만 GC가 순회하므로, 엔진이 수만 개의 클래스 메타데이터를 매 GC 사이클마다 확인하는 비용을 줄입니다. gc.MaxObjectsNotConsideredByGC로 이 경계를 조정할 수 있습니다.

SUMMARY

핵심 요약

이 강의에서 배운 내용
  • GUObjectArray는 모든 UObject의 메타데이터를 청크 단위로 저장하는 전역 배열입니다
  • FUObjectItem에는 GC 플래그, 클러스터 인덱스, SerialNumber 등 GC 핵심 정보가 담겨 있습니다
  • SerialNumber는 TWeakObjectPtr의 유효성 검증에 사용되며, 인덱스 재사용 시 안전성을 보장합니다
  • EObjectFlagsEInternalObjectFlags는 GC의 객체 처리 방식을 결정합니다
  • DisregardForGC 영역을 통해 엔진 핵심 객체를 GC 순회에서 제외하여 성능을 최적화합니다
PRACTICE

도전 과제

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

실습 1: FUObjectItem 플래그 조회

런타임에서 GUObjectArray를 순회하며 특정 클래스(예: ACharacter)의 모든 인스턴스에 대해 FUObjectItem의 EInternalObjectFlags를 로그에 출력하세요. RootSet, GarbageCollectionKeepFlags 등의 플래그 상태를 확인하세요.

실습 2: Object Index 모니터링

GUObjectArray의 MaxObjectsNotConsideredByGC 값을 확인하고, 대량의 UObject를 생성/삭제하면서 ObjectArrayNum 변화를 stat obj 명령어로 관찰하세요. 인덱스 재사용 패턴을 분석하세요.

심화 과제: 커스텀 UObject 통계 시스템

FUObjectArray::ForEachObjectOfClass를 활용하여 클래스별 UObject 인스턴스 수와 메모리 사용량을 실시간으로 집계하는 콘솔 명령어를 구현하세요. 에디터 Output Log에서 결과를 확인하세요.