
Collection 섹션에서, 사전 등록(Pre-Registration) 단계는 ClassInfo, ScriptStructInfo, EnumInfo를 수집하는 것부터 시작합니다. 이렇게 수집된 정보들은 정적 자동 초기화(static automatic initialization) 과정에서 TDeferredRegistry에 전달됩니다. 엔진이 실행되기 전에, 각 생성된 번역 단위(translation unit)에서 Z_CompiledinDeferFile이 모든 리플렉션 타입 정보를 해당 타입별 TDeferredRegistry로 전달합니다. 각 타입별로 별도의 레지스트리가 존재한다는 점에 유의해야 합니다.
using FClassDeferredRegistry = TDeferredRegistry<FClassRegistrationInfo>;
using FEnumDeferredRegistry = TDeferredRegistry<FEnumRegistrationInfo>;
using FStructDeferredRegistry = TDeferredRegistry<FStructRegistrationInfo>;
using FPackageDeferredRegistry = TDeferredRegistry<FPackageRegistrationInfo>;
TDeferredRegistry는 싱글턴(Singleton) 디자인 패턴을 사용하여 필요할 때마다 레지스트리에 데이터를 로드합니다.
/**
* registry singleton 반환
*/
static TDeferredRegistry& Get()
{
static TDeferredRegistry Registry;
return Registry;
}
싱글턴 패턴은 C++에서 정적 초기화 순서 문제를 해결하기 위해 자주 사용되며, 언리얼 엔진도 이 방식을 활용해 레지스트리 싱글턴을 관리합니다. 이 레지스트리는 싱글턴을 getter 함수 내부에 캡슐화하여, 실제로 필요할 때 최초로 생성되도록 보장합니다. 언리얼 엔진의 Deferred Registry는 이러한 싱글턴을 통해 필수 타입(특히 CoreUObject 타입)이 다른 타입보다 먼저 초기화되고 등록되도록 순서를 제어합니다. 이를 통해 엔진 시작 시 중요한 타입들이 우선적으로 준비됩니다.
엔진 소스 코드를 분석하면서, 이러한 초기화 순서와 정적 초기화가 실제로 어떻게 관리되는지 정확히 파악하는 데 어려움이 있었습니다. 하지만 언리얼 엔진 5.3 이상에서 싱글턴에 브레이크포인트를 걸어 동작을 관찰한 결과, 메인 함수가 호출되기 전에 약 3343개의 항목이 Deferred Registry 배열에 초기화되어 추가되는 것을 확인할 수 있었습니다.
콜스택을 더 조사해보면, 할당 과정에서 DLL main이 호출된다는 점을 알 수 있습니다. 이는 Deferred Registry의 할당 순서가 엔진 시작 전에 동적 링크를 통해 관리된다는 것을 의미합니다. 즉, DLL의 동적 로딩 과정에서 각 모듈이 특정 순서로 링크되고, 각 DLL 내부에서 정적 초기화가 로컬하게 이루어지며, 이는 엔진의 모듈형 설계 원칙을 따릅니다.
언리얼 엔진에서 각 UClass는 StaticClass() 함수를 통해 자신의 UClass 객체를 반환합니다. 이 함수는 내부적으로 GetPrivateStaticClass를 호출하며, 이는 정적 초기화 단계에서 IMPLEMENT_CLASS 매크로에 의해 연결됩니다. 특히, UObject의 UClass는 지연 레지스트리(deferred registry) 메커니즘을 통해 가장 먼저 정적 초기화되는 객체 중 하나입니다. UObject는 별도의 리플렉션 파일을 가지며, 이 초기화 과정에서 이후 타입 생성 및 등록의 기반이 되는 프레임워크가 구축됩니다. 이러한 방식 덕분에 UObject 계열 타입들은 엔진 런타임 전반에 걸쳐 올바르게 초기화되고 사용할 준비가 완료됩니다.
NoExportTypes.h 파일은 UObject를 UClass로 선언하면서 특별한 메타데이터 태그를 지정하는 역할을 합니다.
/**
* Direct base class for all UE objects
* @note The full C++ class is located here: Engine\Source\Runtime\CoreUObject\Public\UObject\Object.h
*/
UCLASS(abstract, noexport, MatchedSerializers)
class UObject
{
GENERATED_BODY()
public:
UObject(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get());
UObject(FVTableHelper& Helper);
~UObject();
/**
* Executes some portion of the ubergraph.
* @param EntryPoint The entry point to start code execution at.
*/
UFUNCTION(BlueprintImplementableEvent, meta=(BlueprintInternalUseOnly = "true"))
void ExecuteUbergraph(int32 EntryPoint);
};
또한, UObject의 리플렉션 파일에는 UClass 매크로가 없고 별도의 gen.cpp 및 generated.h 파일이 없는 intrinsic 타입(UObject에서 상속된 내장 타입)에 대한 선언도 포함되어 있습니다. 이러한 intrinsic 타입은 UnrealTypePrivate.h에서 정의된 매크로를 통해 수동으로 리플렉션 정보가 생성됩니다.
DECLARE_CASTED_CLASS_INTRINSIC_WITH_API 매크로는 intrinsic 타입을 수동으로 등록하고, 해당 타입을 특정 API(예: CoreModule)와 연결하는 데 사용됩니다.IMPLEMENT_CORE_INTRINSIC_CLASS 매크로를 통해 UObject에서 상속된 intrinsic 클래스의 리플렉션 정보가 생성됩니다.마지막으로, NoExportTypes.gen.cpp 파일에는 UHT(Unreal Header Tool)가 UObject의 리플렉션 타입 정보를 수집하는 정적 코드 패턴을 삽입합니다.
UClass* Z_Construct_UClass_UObject()
{
if (!Z_Registration_Info_UClass_UObject.OuterSingleton)
{
UECodeGen_Private::ConstructUClass(Z_Registration_Info_UClass_UObject.OuterSingleton, Z_Construct_UClass_UObject_Statics::ClassParams);
}
return Z_Registration_Info_UClass_UObject.OuterSingleton;
}
IMPLEMENT_CLASS(UObject,0)는 UObject의 UClass에 대한 Outer Singleton의 생성과 관련이 있습니다.
GetPrivateStaticClass
OuterPrivate로 전달하며, UClass의 OuterSingleton이 생성된 후 UPackage 객체와 연결합니다. "/Script/"는 UObject가 속한 모듈(일반적으로 CoreUObject)을 나타냅니다.StaticRegisterNativesUMyClass는 UClass에 연결된 네이티브 함수(execCallableFunc, execNativeFunc)를 등록합니다.InternalConstructor 템플릿 래퍼는 C++ 생성자에 대한 함수 포인터가 없기 때문에 생성자 호출을 가능하게 하여 해당 클래스의 생성자를 호출할 수 있도록 합니다.Super는 기반 클래스, WithinClass는 UObject의 Outer 타입을 의미합니다. "Super"는 반드시 기반 클래스가 먼저 UClass로 빌드되어야 하며, 그 후에 서브클래스가 빌드될 수 있음을 의미합니다. WithinClass는 UObject가 빌드된 후 어떤 Outer에 속해야 하는지 제한하는 역할을 하므로, 반드시 소속된 Outer의 UClass*가 미리 빌드되어 있어야 합니다.GetPrivateStaticClassBody
GUObjectAllocator가 UClass 객체를 저장할 메모리를 할당합니다.EC_StaticConstructor 시그니처를 포함합니다. 등록 단계에서 생성 과정은 두 단계로 나뉘며, 표준 C++의 new를 직접 호출하지 않고 GUObjectAllocator를 통해 메모리 할당이 이뤄집니다.InitializePrivateStaticClass가 호출되면, TClass_Super_StaticClass를 받아 먼저 TClass_WithinClass_StaticClass로 ClassWithin을 설정한 뒤, Super::StaticClass()와 WithinClass::StaticClass()를 차례로 호출하여 초기화합니다.COREUOBJECT_API void InitializePrivateStaticClass(
class UClass* TClass_Super_StaticClass,
class UClass* TClass_PrivateStaticClass,
class UClass* TClass_WithinClass_StaticClass,
const TCHAR* PackageName,
const TCHAR* Name
)
{
TRACE_LOADTIME_CLASS_INFO(TClass_PrivateStaticClass, Name);
/* No recursive ::StaticClass calls allowed. Setup extras. */
if (TClass_Super_StaticClass != TClass_PrivateStaticClass)
{
TClass_PrivateStaticClass->SetSuperStruct(TClass_Super_StaticClass);
}
else
{
TClass_PrivateStaticClass->SetSuperStruct(NULL);
}
TClass_PrivateStaticClass->ClassWithin = TClass_WithinClass_StaticClass;
// Register the class's dependencies, then itself.
TClass_PrivateStaticClass->RegisterDependencies();
{
// Defer
TClass_PrivateStaticClass->Register(PackageName, Name);
}}
}
제공된 코드 스니펫에서 SuperStruct는 UStruct* 타입으로, UStruct에 정의되어 있으며 해당 타입의 기반 클래스(부모 클래스)를 가리킵니다. ClassWithin 파라미터는 외부 객체(Outer Object)의 타입을 제한하는 데 사용됩니다.
등록의 초기 단계에서는 UObjectBase::Register()가 호출됩니다. 이후 등록 과정이 진행되면, 이 함수는 UClassRegisterAllCompiledInClasses와 함께 호출되어 최종적으로 타입 등록이 완료됩니다.
/** 이 객체의 등록을 대기열에 추가합니다. */
void UObjectBase::Register(const TCHAR* PackageName,const TCHAR* InName)
{
LLM_SCOPE(ELLMTag::UObject);
TMap<UObjectBase*, FPendingRegistrantInfo>& PendingRegistrants = FPendingRegistrantInfo::GetMap();
FPendingRegistrant* PendingRegistration = new FPendingRegistrant(this);
PendingRegistrants.Add(this, FPendingRegistrantInfo(InName, PackageName));
#if USE_PER_MODULE_UOBJECT_BOOTSTRAP
if (FName(PackageName) != FName("/Script/CoreUObject"))
{
TMap<FName, TArray<FPendingRegistrant*>>& PerModuleMap = GetPerModuleBootstrapMap();
PerModuleMap.FindOrAdd(FName(PackageName)).Add(PendingRegistration);
}
else
#endif
{
if (GLastPendingRegistrant)
{
GLastPendingRegistrant->NextAutoRegister = PendingRegistration;
}
else
{
check(!GFirstPendingRegistrant);
GFirstPendingRegistrant = PendingRegistration;
}
GLastPendingRegistrant = PendingRegistration;
}}
UObjectBase::Register 함수는 등록 정보를 전역 싱글턴 맵과 전역 링크드 리스트에 기록하고 추적합니다. 이러한 방식을 사용하는 이유와 그 배경에 대해서는 이후에 더 자세히 설명합니다.
이 단계의 목표는 엔진 시작 과정에서 PreInit 단계까지 진행하는 것입니다. PreInit 동안 최초의 모듈인 CoreUObject가 로드되며, 이 과정은 할당된 UClass의 UObject에 타입 데이터가 최종적으로 등록되는 단계로 마무리됩니다. 아래 다이어그램은 AppInit까지의 엔진 시작 프로세스를 보여주며, 이에 대한 자세한 내용은 이후에 설명합니다.

정적 초기화 단계에서는 각 번역 단위(translation unit)에서 타입 데이터 구조가 정적으로 할당됩니다. Deferred Registry 호출 시 콜스택을 살펴보면, DLLMain()이 WinMain()보다 먼저 호출될 수 있는 일련의 과정을 확인할 수 있습니다. DLL의 링크 및 로딩 과정은 복잡하므로, 여기서는 핵심적인 부분만 정리합니다.
DLL의 엔트리 포인트는 C 런타임 환경을 구축하며, 메모리 예약 및 DLL 연결을 위한 스레드 생성을 담당합니다. DLL이 로드될 때 운영체제는 보통 "DllMainCRTStartup"을 통해 DLLMain()을 호출합니다.
언리얼 엔진에서는 DLLMain이 호출되는 과정을 살펴보면, 자체적으로 커스텀 CRTStartup을 구현하는 등 특수한 방식을 사용하는 것을 확인할 수 있습니다. 이 과정은 정적 및 비지역(static/non-local) 데이터의 생성자 초기화를 지원합니다.
모듈식 빌드 환경에서는 로더 실행 파일이 UE 모듈 DLL의 로딩 순서를 자체적으로 관리하며, 로더가 먼저 정적 초기화를 거친 후 DLL을 로드합니다. DLL의 정적 변수들은 엔진이 WinAPI의 LoadModule을 호출할 때 초기화됩니다.
이처럼 복잡한 과정은 정적 초기화와 동적 DLL 로딩이 체계적으로 조율되도록 하여, 언리얼 엔진의 구조적 초기화가 안정적으로 이루어지도록 보장합니다.
WinMain(): Windows 애플리케이션(.exe)의 진입점으로, 운영체제(OS)가 프로세스를 시작할 때 호출합니다. OS 관점에서 int main() 호출을 감싸는 역할을 합니다.
main(): C++에서 프로그램의 기본 진입점이며, 반드시 int를 반환해야 합니다. 대소문자를 구분하므로 Main()과는 다릅니다.
DllMain(): DLL의 진입점 함수로, DLL은 독립적으로 실행되지 않고 main() 또는 WinMain()에서 시작된 프로세스에 붙어서 동작합니다.
요약하면, 이 진입점들은 각각의 목적에 따라 프로세스 시작과 엔진 동작의 캡슐화를 담당합니다. 엔진 루프에서는 GuardedMain이 이러한 시작 단계를 관리합니다.
int32 WINAPI WinMain(_In_ HINSTANCE hInInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ char* pCmdLine, _In_ int32 nCmdShow)
{
int32 Result = LaunchWindowsStartup(hInInstance, hPrevInstance, pCmdLine, nCmdShow, nullptr);
LaunchWindowsShutdown();
return Result;
}
WinMain 내부에서는 LaunchWindowsStartup을 호출하여 Windows 환경의 초기화와 필수 작업을 처리합니다. 이후 GuardedMainWrapper()가 호출되고, 이 함수에서 다시 GuardedMain()을 호출하여 프로그램의 핵심 동작이 시작됩니다.
/**
* Static guarded main function. Rolled into own function so we can have error handling for debug/ release builds depending * on whether a debugger is attached or not. */
int32 GuardedMain( const TCHAR* CmdLine )
{
FTrackedActivity::GetEngineActivity().Update(TEXT("Starting"), FTrackedActivity::ELight::Yellow);
FTaskTagScope Scope(ETaskTag::EGameThread);
#if !(UE_BUILD_SHIPPING)
// If "-waitforattach" or "-WaitForDebugger" was specified, halt startup and wait for a debugger to attach before continuing
if (FParse::Param(CmdLine, TEXT("waitforattach")) || FParse::Param(CmdLine, TEXT("WaitForDebugger")))
{
while (!FPlatformMisc::IsDebuggerPresent())
{
FPlatformProcess::Sleep(0.1f);
}
UE_DEBUG_BREAK();
}
#endif
BootTimingPoint("DefaultMain");
// Super early init code. DO NOT MOVE THIS ANYWHERE ELSE!
FCoreDelegates::GetPreMainInitDelegate().Broadcast();
// make sure GEngineLoop::Exit() is always called.
struct EngineLoopCleanupGuard
{
~EngineLoopCleanupGuard()
{
// Don't shut down the engine on scope exit when we are running embedded
// because the outer application will take care of that.
if (!GUELibraryOverrideSettings.bIsEmbedded)
{
EngineExit();
}
}
} CleanupGuard;
// Set up minidump filename. We cannot do this directly inside main as we use an FString that requires
// destruction and main uses SEH.
// These names will be updated as soon as the Filemanager is set up so we can write to the log file. // That will also use the user folder for installed builds so we don't write into program files or whatever.#if PLATFORM_WINDOWS
FCString::Strcpy(MiniDumpFilenameW, *FString::Printf(TEXT("unreal-v%i-%s.dmp"), FEngineVersion::Current().GetChangelist(), *FDateTime::Now().ToString()));
#endif
FTrackedActivity::GetEngineActivity().Update(TEXT("Initializing"));
int32 ErrorLevel = EnginePreInit( CmdLine );
// exit if PreInit failed.
if ( ErrorLevel != 0 || IsEngineExitRequested() )
{
return ErrorLevel;
}
{
FScopedSlowTask SlowTask(100, NSLOCTEXT("EngineInit", "EngineInit_Loading", "Loading..."));
// Set up minidump filename. We cannot do this directly inside main as we use an FString that requires
// destruction and main uses SEH.
// These names will be updated as soon as the Filemanager is set up so we can write to the log file.
// That will also use the user folder for installed builds so we don't write into program files or whatever.
#if PLATFORM_WINDOWS
FCString::Strcpy(MiniDumpFilenameW, *FString::Printf(TEXT("unreal-v%i-%s.dmp"), FEngineVersion::Current().GetChangelist(), *FDateTime::Now().ToString()));
#endif
FTrackedActivity::GetEngineActivity().Update(TEXT("Initializing"));
int32 ErrorLevel = EnginePreInit( CmdLine );
// exit if PreInit failed.
if ( ErrorLevel != 0 || IsEngineExitRequested() )
{
return ErrorLevel;
}
{
FScopedSlowTask SlowTask(100, NSLOCTEXT("EngineInit", "EngineInit_Loading", "Loading..."));
// EnginePreInit leaves 20% unused in its slow task.
// Here we consume 80% immediately so that the percentage value on the splash screen doesn't change from one slow task to the next.
// (Note, we can't include the call to EnginePreInit in this ScopedSlowTask, because the engine isn't fully initialized at that point)
SlowTask.EnterProgressFrame(80);
SlowTask.EnterProgressFrame(20);
#if WITH_EDITOR
if (GIsEditor)
{
ErrorLevel = EditorInit(GEngineLoop);
}
else
#endif
{
ErrorLevel = EngineInit();
}
}
double EngineInitializationTime = FPlatformTime::Seconds() - GStartTime;
UE_LOG(LogLoad, Log, TEXT("(Engine Initialization) Total time: %.2f seconds"), EngineInitializationTime);
#if WITH_EDITOR
UE_LOG(LogLoad, Log, TEXT("(Engine Initialization) Total Blueprint compile time: %.2f seconds"), BlueprintCompileAndLoadTimerData.GetTime());
#endif
ACCUM_LOADTIME(TEXT("EngineInitialization"), EngineInitializationTime);
BootTimingPoint("Tick loop starting");
DumpBootTiming();
FTrackedActivity::GetEngineActivity().Update(TEXT("Ticking loop"), FTrackedActivity::ELight::Green);
// Don't tick if we're running an embedded engine - we rely on the outer
// application ticking us instead.
if (!GUELibraryOverrideSettings.bIsEmbedded)
{
while( !IsEngineExitRequested() )
{
EngineTick();
}
}
TRACE_BOOKMARK(TEXT("Tick loop end"));
#if WITH_EDITOR
if( GIsEditor )
{
EditorExit();
}
#endif
return ErrorLevel;
}
GuardedMain의 역할은 엔진 루프를 설정하고 제어를 GEngineLoop로 넘기는 것입니다. 이 과정에서 엔진의 틱 함수가 호출되는 것을 확인할 수 있으며, 그 다음 단계가 EnginePreInit입니다.
/**
* PreInits the engine loop
*/
int32 EnginePreInit( const TCHAR* CmdLine )
{
int32 ErrorLevel = GEngineLoop.PreInit( CmdLine );
return( ErrorLevel );
}
PreInit 단계에서는 PreInitPreStartupScreen 함수가 커맨드라인 인자를 받아 호출됩니다. 이 함수 내부에서 중요한 단계 중 하나가 바로 LoadCoreModules()의 호출입니다.
bool FEngineLoop::LoadCoreModules()
{
// Always attempt to load CoreUObject. It requires additional pre-init which is called from it's module's StartupModule method.
#if WITH_COREUOBJECT
#if USE_PER_MODULE_UOBJECT_BOOTSTRAP // otherwise do it later
FModuleManager::Get().OnProcessLoadedObjectsCallback().AddStatic(ProcessNewlyLoadedUObjects);
#endif
return FModuleManager::Get().LoadModule(TEXT("CoreUObject")) != nullptr;
#else
return true;
#endif
}
위 코드에서 볼 수 있듯이, LoadCoreModules 함수는 CoreUObject 모듈의 로딩을 최우선으로 처리합니다. 이때, CoreUObject는 추가적인 사전 초기화(pre-init)가 필요하며, 이는 해당 모듈의 StartupModule 메서드에서 수행됩니다. PreInitPreStartupScreen 등 사전 초기화가 완료되면, 다음 단계로 타입 시스템에 등록될 UObject 초기화가 진행됩니다. 이 과정을 통해 UObject의 시작 단계가 마무리되고, 이후 모든 엔진 모듈의 로딩이 이어집니다.

타입 시스템 등록 과정을 시각화한 다이어그램입니다. 이 다이어그램은 등록 프로세스를 따라가며 전체 흐름을 이해하는 데 도움을 줍니다.
virtual void StartupModule() override
{
// 지금까지 로드된 모든 클래스를 등록합니다. 이는 CVar가 동작하기 위해 필요합니다.
UClassRegisterAllCompiledInClasses();
// InitUObject를 AppInit 단계에서 호출될 수 있도록 델리게이트에 등록합니다.
void InitUObject();
FCoreDelegates::OnInit.AddStatic(InitUObject);
// Core 버전의 비동기 로딩 함수를 CoreUObject 버전으로 대체합니다.
IsInAsyncLoadingThread = &IsInAsyncLoadingThreadCoreUObjectInternal;
IsAsyncLoading = &IsAsyncLoadingCoreUObjectInternal;
SuspendAsyncLoading = &SuspendAsyncLoadingInternal;
ResumeAsyncLoading = &ResumeAsyncLoadingInternal;
IsAsyncLoadingSuspended = &IsAsyncLoadingSuspendedInternal;
IsAsyncLoadingMultithreaded = &IsAsyncLoadingMultithreadedCoreUObjectInternal;
// 런타임 오류 로깅에 스크립트 콜스택 콜백을 등록합니다.
#if UE_RAISE_RUNTIME_ERRORS
FRuntimeErrors::OnRuntimeIssueLogged.BindStatic(&FCoreUObjectModule::RouteRuntimeMessageToBP);
#endif
// CoreUObject 로드 이후 추가 콘텐츠 마운트 포인트 등록을 허용합니다.
FPackageName::OnCoreUObjectInitialized();
#if DO_BLUEPRINT_GUARD
FFrame::InitPrintScriptCallstack();
#endif
}
언리얼 엔진에서 CoreUObject 모듈의 시작 시점에 호출되는 StartupModule 엔트리 포인트는 매우 중요합니다. 이 함수는 FCoreDelegate와 같은 메커니즘을 통해 초기화 과정을 체계적으로 진행하며, InitUObject와 같은 초기화 루틴이 AppInit 단계에서 호출될 수 있도록 등록합니다. 이러한 구조화된 초기화 방식은 UObject 시스템의 신뢰성과 일관성을 보장하며, 엔진 시작 과정에서의 순차적이고 안정적인 동작을 지원합니다.
/** Register all loaded classes */
void UClassRegisterAllCompiledInClasses()
{
SCOPED_BOOT_TIMING("UClassRegisterAllCompiledInClasses");
LLM_SCOPE(ELLMTag::UObject);
FClassDeferredRegistry& Registry = FClassDeferredRegistry::Get();
Registry.ProcessChangedObjects();
for (const FClassDeferredRegistry::FRegistrant& Registrant : Registry.GetRegistrations())
{
UClass* RegisteredClass = FClassDeferredRegistry::InnerRegister(Registrant);
}
}
이 단계의 콜스택에서는 UClass의 생성 과정이 시작됩니다. 먼저, 정적 초기화 단계에서 추가된 모든 요소가 지연 레지스트리(Deferred Registry)에서 로드됩니다. 이 레지스트리의 등록자(Registrant)들을 모두 로드한 후, 핵심 UObject가 StaticClass에서 최초로 inner 등록자 호출을 받는 첫 번째 UClass가 됩니다.
좀 더 정확히 말하면, .gen.cpp 파일에서 리플렉션된 모든 타입에 대해 UClass 인스턴스가 생성됩니다.
에디터 vs 런타임
UClass는 DLL 링크 순서에 따라 에디터와 런타임 환경에서 다르게 동작할 수 있습니다. 특히 모놀리식 빌드에서는 모든 보류 중인 등록자가 ProcessNewlyLoadedObjects에 의해 한 번에 처리됩니다. 언리얼 엔진은 전처리를 위해 필요한 클래스가 반드시 로드되도록 보호 장치를 마련해 두었습니다.
UClass는 SuperStruct와 WithinClass와 같은 의존성에 따라 동작합니다. 정적 초기화 순서가 불확실한 상황에서는 이러한 의존성 순서가 매우 중요합니다. 하지만 UObject 모듈이 가장 먼저 정적으로 로드되기 때문에, 일반적으로 이러한 의존성은 초기화 과정에서 해결됩니다.
에디터 모드에서는 CoreUObject가 가장 먼저 정적으로 링크되고, 이후 다른 핵심 엔진 모듈이 이어집니다. 반면, 게임 모듈의 런타임에서는 DLL 로딩 순서가 반대로 적용되어, 실행 파일이 자신의 실행이 시작된 후 내부적으로 다른 DLL을 로드하고, 해당 DLL의 정적 변수가 초기화됩니다.
등록된 정보의 메타데이터는 UScriptStruct 또는 UEnum에 저장됩니다. UScriptStruct나 UEnum과 같은 다른 UObject 타입을 생성하기 전에 반드시 UClass가 먼저 인스턴스화되어야 하며, 이는 클래스 정보를 저장하는 역할을 합니다. CoreUObject 모듈이 완전히 로드되면, UClass로 설명된 모든 타입을 사용할 수 있게 됩니다.