
리플렉션은 런타임에 타입 정보를 획득하는 시스템입니다. 이 타입 정보는 객체와 연관된 정적 세부 정보를 제공합니다. 가비지 컬렉션은 클래스 객체를 생성할 때 리플렉션이 없어도 정상적으로 동작할 수 있습니다. 타입 시스템은 런타임에 타입을 얻는 과정을 의미하며, 타입 정보를 활용해 객체를 역으로 생성하거나 속성을 읽고 수정할 수 있습니다. 현재 C++은 리플렉션 타입 시스템을 지원하지 않으며(C++ RTTI 제외), C#은 이를 지원합니다.
C# 리플렉션 예시와 주요 포인트:

dynamic_cast 연산자는 기반 클래스 포인터나 참조가 파생 클래스 객체를 가리킬 때, 이를 파생 클래스의 포인터나 참조로 변환하는 데 사용됩니다. 이 연산자는 가상 함수가 포함된 클래스에만 적용됩니다. 참조 변환이 실패하면 bad_cast 예외가 발생하고, 포인터 변환이 실패하면 null이 반환됩니다.
내부적으로 dynamic_cast는 가상 함수 테이블에 저장된 타입 정보를 활용해 기반 클래스 포인터가 실제로 파생 클래스 객체를 가리키는지 판단합니다. 주 목적은 런타임에 객체 포인터가 특정 하위 클래스의 인스턴스인지 확인하는 것입니다.
언리얼 헤더 툴(UHT)은 언리얼 엔진에서 현재 사용되는 솔루션입니다.
초기 언리얼 엔진은 C++로 자체 리플렉션 시스템을 구현해야 했습니다. 이후 버전에서는 언리얼 헤더 툴의 로직이 더 나은 라이브러리 지원을 위해 C# 언어로 이전되었습니다.
이 설계의 주요 장점은 C++ 코드에 단순히 빈 태그 매크로를 추가하는 것만으로 비교적 작은 변경만으로도 리플렉션 기능을 도입할 수 있다는 점입니다. 이를 통해 프로그래머는 원래의 클래스 선언 구조를 해치지 않고 메타데이터와 코드를 자연스럽게 연결할 수 있습니다.
언리얼 헤더 툴의 영감을 이해하기 위해 아래 이미지는 Clang Compilation Driver를 보여줍니다. 요약하면, 컴파일러 드라이버는 CPP 파일을 토큰으로 파싱하며, 이는 어휘 분석(Lexical Analysis) 과정의 일부입니다. 이후 추상 구문 트리(AST)를 순회하여 소스 코드를 "LLVM 중간 표현"으로 생성하고, 이후 특정 플랫폼의 툴체인으로 컴파일됩니다.

컴파일러 이론을 너무 깊이 파고들지는 않겠지만, 일반적인 어휘 분석기(lexical analyzer) 아키텍처는 아래 이미지와 같습니다.

Token(토큰): 소스 코드에서 정보 단위를 나타내는 문자 시퀀스입니다.
Pattern(패턴): 토큰이 사용하는 설명 방식을 패턴이라 합니다.
Lexeme(렉심): 토큰의 패턴과 일치하는 소스 코드 내 문자 시퀀스로, 토큰의 인스턴스라고도 합니다.
이제 이러한 개념을 바탕으로 언리얼 엔진 도구의 기본을 살펴볼 수 있습니다.

UHT의 실행 경로는 다음과 같이 일반화할 수 있습니다.
BaseDirectory, OutputDirectory, ClassHeaders, Public Headers 등 정보가 담깁니다.Classes Folder"에 대한 절대 경로를 가지고 있으며, 마찬가지로 Public Folders는 "PublicUObjectHeaders" 폴더에 위치합니다. 같은 원리로 Private은 "PrivateUObjectHeaders" 폴더에 위치합니다.UPackage를 생성합니다. 이 패키지는 UObjects를 자신의 Outer와 함께 캡슐화합니다. 이 시점에서 "헤더가 파서를 위해 준비"됩니다.{에서 };까지의 데이터 구조를 스코프하여 괄호 사이의 모든 내용을 파싱합니다. 헤더 파서는 UHT 타입별로 특정 파서를 위임하며, 각 매크로 타입(UClass, UStruct, UProperty, UInterface, UFunction 등)에 대해 별도의 파서가 존재합니다.Engine, Source 두 개의 서브 테이블을 포함합니다.Run 델리게이트를 호출합니다. 흥미롭게도 Epic은 자신만의 Exporter를 플러그인으로 실행할 수 있는 훅 포인트를 제공합니다. 이를 통해 엔진과 함께 자체 코드 생성 시스템을 UHT와 병행해 구현할 수 있습니다.CodeGen은 타입 시스템 코드를 generated.h, gen.cpp 파일로 생성하는 전용 Exporter이며, 이 파일들은 Intermediate 폴더에 저장됩니다.UHT.cs 파일을 보면 이 프로세스가 잘 구현되어 있고, 전체 흐름을 명확하게 확인할 수 있습니다.
StepReadManifestFile(manifestFilePath);
StepPrepareModules();
StepPrepareHeaders();
StepParseHeaders();
StepPopulateTypeTable();
StepResolveInvalidCheck();
StepBindSuperAndBases();
RecursiveStructCheck();
StepResolveBases();
StepResolveProperties();
StepResolveFinal();
StepResolveValidate();
StepCollectReferences();
TopologicalSortHeaderFiles();
// 참조 디렉터리를 삭제 중이라면, 해당 작업이 완료될 때까지 대기
if (_referenceDeleteTask != null)
{
Logger.LogTrace("Step - Waiting for reference output to be cleared.");
_referenceDeleteTask.Wait();
}
Logger.LogTrace("Step - Starting exporters.");
StepExport();
또한, CURRENT_FILE_ID와 EVENT_PARAMS 매크로가 정의되어 있습니다.
FNativeClassHeaderGenerator::FNativeClassHeaderGenerator(const UPackage, const TSet<FUnrealSourceFile>&, FClasses&, bool)
ObjectMacros.h 내부에는 IDE의 구문 강조 및 자동 완성 정보를 제공하기 위해 UC 네임스페이스가 정의되어 있습니다.
namespace UC
{
// UCLASS 매크로에서 사용할 수 있는 유효한 키워드들
enum
{
/// 이 키워드는 에디터에서 클래스가 표시되는 액터 그룹을 설정하는 데 사용됩니다.
classGroup,
/// 이 클래스를 인스턴스화할 때 항상 지정된 클래스의 Outer를 갖도록 선언합니다. 이는 하위 클래스에 상속되며, 오버라이드하지 않는 한 유지됩니다.
Within, /* =OuterClassName */
/// 이 클래스를 블루프린트에서 변수로 사용할 수 있는 타입으로 노출합니다.
BlueprintType,
/// 이 클래스를 블루프린트에서 변수로 사용할 수 없도록 합니다.
NotBlueprintType,
/// 이 클래스를 블루프린트 생성 시 허용되는 기본 클래스로 노출합니다. 기본값은 NotBlueprintable이며, 상속 시 변경될 수 있습니다.
Blueprintable,
/// 이 클래스를 블루프린트 생성 시 허용되지 않는 기본 클래스로 지정합니다. 기본값은 NotBlueprintable이며, 상속 시 변경될 수 있습니다.
NotBlueprintable,
...
앞서 언급했듯이, 언리얼의 오브젝트 시스템 설계는 타입 시스템에서 출발합니다. 언리얼은 C# 기반의 Unreal Header Tool(UHT)을 활용해 리플렉션 정보를 수집하고 코드를 생성합니다. 이 리플렉션 과정의 이해는 UObject의 생성과 활용, 특히 블루프린트, 가비지 컬렉션, 직렬화 등 다양한 시스템과의 연계 방식을 파악하는 데 중요한 통찰을 제공합니다.
코드가 아닌 데이터 파일로 리플렉션 정보를 생성하지 않는 핵심 이유는 동기화에 있습니다. 리플렉션 데이터를 C++ 코드로 생성하면, 컴파일 시점에 바이너리와의 정합성을 보장할 수 있습니다. 이는 패키지의 정적 초기화와 동적 링크 과정이 UClass와 UObject의 리플렉션 계층 구조 생성과 밀접하게 연결되어 있을 때 특히 중요합니다.
간단한 Hello 클래스 리플렉션 예시
#include "Hello.generated.h"
UCLASS()
class Hello
{
public:
UPROPERTY()
int Count;
UFUNCTION()
void Say();
};

간단히 말해, 모든 프로그램은 타입과 중첩 함수로 구성됩니다. 타입에는 Enum, struct, class가 있으며, 클래스 내부에는 필드, 함수, 그리고 서브타입(중첩 타입)을 정의할 수 있습니다.
색상 체계:
타입은 개념적으로 두 가지로 나눌 수 있습니다: 집합 타입(Aggregate Type)과 원자 타입(Atomic Type)
UFunction: 함수의 입력 및 출력 파라미터로 속성만 가질 수 있습니다.
UScriptStruct: C++의 POD(Plain Old Data) 구조체와 유사하며, 속성만 포함할 수 있습니다. 언리얼 엔진에서는 리플렉션, 직렬화, 네트워크 복제 등을 지원하는 "경량" UObject 역할을 합니다. 일반 UObject와 달리 가비지 컬렉터의 관리 대상이 아니므로 메모리 할당과 해제를 직접 관리해야 합니다. UScriptStruct는 UStruct를 상속합니다.
UClass: 속성과 함수 모두를 포함할 수 있으며, 가장 자주 사용되는 타입입니다.
C++의 가상 클래스처럼, UInterface는 여러 인터페이스를 상속할 수 있습니다. 단, 함수만 포함해야 합니다.
일반 클래스는 UObject를 상속해야 하며, UInterface는 특별한 클래스이지만 여전히 UClass에 저장됩니다.
필드의 타입 인스턴스는 언리얼 엔진에서 필드의 타입을 나타냅니다. Unreal의 Property 시스템은 다양한 하위 카테고리를 가지며, FProperty는 템플릿을 통해 여러 하위 클래스로 인스턴스화됩니다. 이 타입은 FField 클래스를 상속합니다.
Link()가 호출되면 FField는 Int32 Offset을 할당받습니다.
이 과정에 대한 자세한 내용은 문서 후반부 또는 UObject Construction & Post Initialization Document에서 더 깊이 다룹니다.
이 설계는 Blueprint Virtual Machine에서 UMyObject의 구체적인 타입을 몰라도 복사/할당이 가능하도록 해줍니다.
Struct는 커스텀 생성자를 가지며, TArray는 Memcopy로 복사할 수 없습니다. 이를 해결하기 위해 FProperty는 InitializeValue_Container, CopyCompleteValue와 같은 가상 함수를 제공합니다.
UMetaData는 TMap<FName, FString> 형태의 키-값 쌍으로, 오직 에디터에서만 사용됩니다. 이 쌍은 에디터에 분류, 표시 이름, 안내 메시지 등의 정보를 제공합니다.
FField는 리플렉션 데이터 객체의 기반 클래스입니다.
"FField"라는 이름은 선언이든 정의든 타입 시스템 내에서 필드로 간주될 수 있음을 의미합니다.
참고: FField는 최근에 도입된 타입입니다. 엔진 소스를 살펴보면, 블루프린트와의 호환성을 위해 UField와의 브릿지 계층이 존재함을 알 수 있습니다. 앞으로 Epic은 UProperty에서 FProperty로의 전환을 통해 사용 비용을 낮추려는 의도를 가지고 있습니다.
FField 기반 클래스의 의도는 무엇인가요?
Dazhao는 UProperty, UStruct, UEnum을 직접 UObject에서 상속받는 것이 직관적으로 보일 수 있지만, 그렇게 하지 않는 명확한 이유가 있다고 설명합니다:
FField가 왜 UObject를 상속해야 할까요?
특히 직렬화는 리플렉션이 제공하는 가장 중요한 기능으로 간주됩니다.
핵심 원리는 불필요한 요소와 필수 기능을 결합하되, 모든 데이터 타입이 UObject를 상속하도록 통일하는 것입니다. 이렇게 하면 직렬화 등 작업에서 별도의 기능 세트를 두 번 구현할 필요가 없습니다. 이 방식이 완전히 순수해 보이지 않을 수 있지만, 장점이 단점을 상회합니다. 객체에서는 Instance->GetClass()로 UClass 객체를 얻을 수 있고, UClass 자체에서 GetClass()를 호출하면 UClass가 반환되어 객체와 타입 데이터를 쉽게 구분할 수 있습니다.
UStruct: FField의 연결 리스트를 포함하며, 제약이 적어 확장성이 높습니다. 현재는 중첩 타입이 없지만, UStruct는 중첩 타입과 속성 모두를 포함할 수 있는 구조입니다. UFunction은 속성만 가질 수 있고, UScriptStruct는 오직 속성만 포함하며 함수는 가질 수 없습니다. UStruct의 UStruct* SuperStruct 포인터는 상속받은 기반 클래스를 참조하는 데 사용됩니다.
MetaData: 컴파일된 클래스에 포함되어 부팅 시 언팩 및 설정됩니다. 일반적으로 메타데이터는 에디터에서만 주로 사용되며, 런타임에서는 활용 빈도가 낮습니다.