🚀 들어가며
이전 글에서는 C++에서 다운캐스팅이 왜 위험한지,
그리고 dynamic_cast 없이 타입을 확인하기 위한
TypeId/TypeInfo 기반 커스텀 RTTI 시스템을 직접 구현해봤습니다.
그 과정을 보면 자연스럽게 이런 질문이 생깁니다.
❓ 그런데 Unreal Engine은 왜 C++의 기본 RTTI를 그대로 사용하지 않을까?
C++에는 이미 dynamic_cast와 typeid가 있습니다.
그런데 Unreal Engine은 여기에 의존하지 않고, 자체적인 타입 시스템과 리플렉션 시스템을 가지고 있습니다.
대표적으로 다음과 같은 코드가 있습니다.
UCLASS()
class AMyActor : public AActor
{
GENERATED_BODY()
};
처음 언리얼을 공부하면 UCLASS(), GENERATED_BODY() 같은 매크로가 굉장히 낯설게 느껴집니다.
하지만 이 매크로들은 단순히 코드 생성을 편하게 하기 위한 장식이 아닙니다.
👉 언리얼 엔진이 C++ 위에 자신만의 객체 시스템을 만들기 위한 핵심 장치입니다.
이번 글에서는 왜 Unreal Engine이 RTTI를 직접 구현하는지,
그리고 그 구조가 게임 엔진에서 왜 필요한지 정리해보겠습니다.
🧠 RTTI와 Reflection은 다른 개념입니다
먼저 용어부터 정리해야 합니다.
RTTI는 Run-Time Type Information의 약자입니다.
쉽게 말하면 실행 중에 객체의 실제 타입을 확인할 수 있게 해주는 기능입니다.
대표적으로 다음 두 가지가 있습니다.
dynamic_cast<Derived*>(base); typeid(*object);
이 기능을 사용하면:
- 현재 객체가 어떤 타입인지 확인할 수 있고
- 부모 포인터에서 자식 타입으로 안전하게 다운캐스팅할 수 있습니다
하지만 여기서 중요한 점이 있습니다.
C++ RTTI는 기본적으로
- 타입 확인
- 안전한 다운캐스팅
정도에 초점이 맞춰져 있습니다.
반면 게임 엔진이 필요로 하는 것은 훨씬 더 많습니다.
- 타입 이름
- 부모 클래스 정보
- 멤버 변수 목록
- 함수 목록
- 에디터 노출 정보
- 직렬화 정보
- 네트워크 복제 정보
- 가비지 컬렉션 추적 정보
- 블루프린트 연동 정보
즉 Unreal Engine이 필요한 것은 단순 RTTI가 아니라 Reflection 시스템입니다.
🎯 C++ RTTI만으로 부족한 이유
지금까지 설명한 Unreal Engine Reflection 시스템 구조를 그림으로 정리해보면 다음과 같습니다.
핵심은 언리얼은 단순 타입 확인을 넘어서, 엔진 전체가 사용할 수 있는 메타데이터 시스템을 직접 구축했다는 점입니다.

위 구조처럼 Unreal Engine의 Reflection 시스템은 단순 RTTI 기능만 제공하는 것이 아닙니다.
Reflection 시스템을 기반으로
- 에디터 프로퍼티 노출
- 블루프린트 연동
- 직렬화
- 네트워크 복제
- 가비지 컬렉션
같은 엔진 핵심 기능들이 서로 연결됩니다.
즉, Unreal의 RTTI 시스템은 엔진 전체를 지탱하는 메타 시스템이라고 볼 수 있습니다.
C++의 기본 RTTI는 타입 확인에는 사용할 수 있습니다.
하지만 게임 엔진을 만들기에는 정보가 너무 부족합니다.
예를 들어, 다음 클래스를 생각해보겠습니다.
class Player
{
public:
int HP;
float Speed;
void Attack();
};
C++ RTTI만으로는 실행 중에 이런 질문에 답하기 어렵습니다.
- 이 클래스의 멤버 변수 목록은 무엇인가?
- HP 변수의 이름은 무엇인가?
- HP 변수의 타입은 int인가?
- 이 변수를 에디터에 표시할 수 있는가?
- 이 값을 저장 파일에 직렬화할 수 있는가?
- 이 값을 네트워크로 복제할 수 있는가?
- Attack 함수를 블루프린트에서 호출할 수 있는가?
하지만 언리얼 엔진은 이런 기능이 필요합니다.
왜냐하면 언리얼은 단순 C++ 라이브러리가 아니라
👉 에디터, 블루프린트, 직렬화, GC, 네트워크 복제까지 포함한 게임 제작 플랫폼이기 때문입니다.
🎮 게임 엔진이 타입 정보를 필요로 하는 이유
게임 엔진에서는 객체를 단순히 C++ 코드 안에서만 다루지 않습니다.
예를 들어 언리얼 에디터에서는 객체의 속성을 화면에 표시해야 합니다.
UPROPERTY(EditAnywhere) int HP;
이렇게 작성하면 언리얼 에디터에서 HP 값을 수정할 수 있습니다.
그런데 C++ 컴파일러는 기본적으로 변수 이름, 에디터 노출 여부, 메타데이터 같은 정보를 런타임에 제공하지 않습니다.
따라서 언리얼은 별도의 시스템을 통해 이런 정보를 수집해야 합니다.
이때 사용되는 것이 다음 매크로들입니다.
UCLASS() USTRUCT() UENUM() UPROPERTY() UFUNCTION() GENERATED_BODY()
이 매크로들은 언리얼이 클래스를 분석하고, 필요한 메타데이터를 생성하기 위한 표식입니다.
🧩 GENERATED_BODY()는 왜 필요할까?
GENERATED_BODY()는 언리얼을 처음 공부할 때 가장 이상하게 느껴지는 매크로 중 하나입니다.
하지만 역할은 명확합니다.
👉 언리얼 객체 시스템에 필요한 코드를 자동으로 삽입하는 역할입니다.
개념적으로 보면 다음과 같은 코드들이 생성됩니다.
- 클래스 등록 코드
- StaticClass() 관련 코드
- 타입 정보 접근 코드
- 직렬화 관련 코드
- 리플렉션 메타데이터 연결 코드
- 생성자 보조 코드
즉 개발자는 아래 코드처럼 작성하지만
UCLASS()
class AMyActor : public AActor
{
GENERATED_BODY()
};
언리얼 빌드 과정에서는 Unreal Header Tool이 이 코드를 분석하고, 엔진이 사용할 수 있는 추가 코드를 생성합니다.
흐름을 단순화하면 다음과 같습니다.
헤더 파일 작성 ↓ UHT(Unreal Header Tool)가 매크로 분석 ↓ .generated.h 파일 생성 ↓ 컴파일 시 생성 코드 포함 ↓ 런타임에 언리얼 타입 시스템 사용 가능
즉 GENERATED_BODY()는 단순한 장식이 아니라,
언리얼 리플렉션 시스템에 클래스가 참여하기 위한 입구입니다.
🧠 언리얼의 타입 확인 방식
C++에서는 보통 다음과 같이 타입을 확인합니다.
Derived* derived = dynamic_cast<Derived*>(base);
반면 언리얼에서는 보통 다음과 같은 방식이 사용됩니다.
if (Object->IsA(AMyActor::StaticClass()))
{
AMyActor* Actor = Cast<AMyActor>(Object);
}
또는 간단히
AMyActor* Actor = Cast<AMyActor>(Object);
이 구조는 우리가 이전 글에서 직접 구현했던 구조와 개념적으로 비슷합니다.
객체가 자신의 타입 정보를 가지고 있음 ↓ 타입 정보를 비교함 ↓ 맞으면 안전하게 캐스팅함 ↓ 틀리면 nullptr 반환
즉 언리얼의 Cast<T>()는 단순히 C++의 dynamic_cast를 감싼 것이 아니라,
언리얼 객체 시스템의 타입 정보를 기반으로 동작합니다.
🔍 UObject와 UClass
언리얼 객체 시스템의 핵심은 UObject와 UClass입니다.
단순화하면 이렇게 생각할 수 있습니다.
UObject └── 실제 객체 UClass └── 해당 객체의 타입 정보
각 UObject는 자신이 어떤 UClass에 속하는지 알 수 있습니다.
개념적으로는 다음과 비슷합니다.
UClass* ClassInfo = Object->GetClass();
그리고 타입 비교는 이런 식으로 이루어집니다.
Object->IsA(AMyActor::StaticClass());
여기서 중요한 점은, UClass가 단순히 타입 이름만 가지는 것이 아니라는 점입니다.
UClass는 다음과 같은 정보를 담을 수 있습니다.
- 클래스 이름
- 부모 클래스 정보
- 프로퍼티 목록
- 함수 목록
- 메타데이터
- 생성 정보
- 직렬화 정보
즉, 언리얼의 타입 정보는 C++ RTTI보다 훨씬 풍부합니다.
⚙️ 왜 C++ 기본 RTTI를 그대로 쓰지 않을까?
이제 핵심 질문으로 돌아가 보겠습니다.
❓ 왜 Unreal Engine은 C++ RTTI를 그대로 사용하지 않을까?
이유는 크게 네 가지로 정리할 수 있습니다.
1. 표준 RTTI는 정보가 부족하다
C++ RTTI는 타입 확인과 다운캐스팅에는 사용할 수 있습니다.
하지만 언리얼이 필요로 하는 정보는 훨씬 많습니다.
- 변수 이름
- 함수 이름
- 프로퍼티 메타데이터
- 에디터 노출 여부
- 블루프린트 호출 가능 여부
- 직렬화 대상 여부
- 네트워크 복제 여부
이런 정보는 C++ 기본 RTTI만으로 얻기 어렵습니다.
그래서 언리얼은 자체적인 메타데이터 시스템을 구축합니다.
2. 에디터와 연결되어야 한다
언리얼 엔진의 강점은 에디터입니다.
개발자는 C++로 클래스를 작성하고, 디자이너는 에디터에서 해당 객체의 속성을 수정합니다.
예를 들어
UPROPERTY(EditAnywhere) float MoveSpeed;
이 코드가 의미를 가지려면, 엔진은 실행 중 또는 에디터 단계에서 다음을 알아야 합니다.
- MoveSpeed라는 변수가 존재한다
- 타입은 float이다
- 에디터에서 수정 가능하다
- 어떤 카테고리에 표시할 수 있다
C++ RTTI는 이런 정보를 제공하지 않습니다.
따라서 언리얼은 Reflection 시스템이 필요합니다.
3. 가비지 컬렉션이 필요하다
언리얼의 UObject 계열 객체는 가비지 컬렉션 대상이 될 수 있습니다.
그러려면 엔진은 객체가 다른 UObject를 참조하고 있는지 알아야 합니다.
예를 들어
UPROPERTY() AActor* TargetActor;
이런 포인터가 있다면, 언리얼 GC는 이 참조를 추적해야 합니다.
하지만 단순한 C++ 원시 포인터(raw pointer)만으로는 엔진이 이 포인터의 의미를 알 수 없습니다.
그래서 UPROPERTY()매크로가 중요합니다.
UPROPERTY()로 표시된 멤버는 언리얼 리플렉션 시스템에 등록되고, GC가 추적할 수 있는 정보가 됩니다.
즉 언리얼의 Reflection 시스템은 단순 타입 확인용이 아닙니다.
👉 GC와 객체 생명주기 관리에도 직접 연결됩니다.
4. 블루프린트와 연결되어야 한다
언리얼은 C++만 사용하는 엔진이 아닙니다.
블루프린트와 C++가 서로 연결됩니다.
예를 들어
UFUNCTION(BlueprintCallable) void Attack();
이렇게 작성하면 블루프린트에서 C++ 함수를 호출할 수 있습니다.
이것이 가능하려면 엔진은 다음 정보를 알아야 합니다.
- Attack이라는 함수가 존재한다
- 블루프린트에서 호출 가능하다
- 인자는 무엇인가
- 반환값은 무엇인가
C++ 기본 RTTI는 이런 정보를 제공하지 않습니다.
그래서 언리얼은 UFUNCTION 메타데이터를 통해 함수를 등록하고, 블루프린트 또는 에디터 시스템과 연결합니다.
🎯 우리가 만든 커스텀 RTTI와 언리얼 RTTI의 차이
이전 글에서 만든 TypeId/TypeInfo 기반 RTTI는 매우 단순한 구조였습니다.
타입마다 고유 ID 부여 ↓ ID 비교 ↓ 맞으면 캐스팅
이 구조는 타입 확인만 놓고 보면 충분히 유용합니다.
하지만 언리얼의 타입 시스템은 여기서 훨씬 더 나아갑니다.
TypeId 기반 커스텀 RTTI → 타입 확인 중심 Unreal Reflection → 타입 확인 + 메타데이터 + 에디터 + GC + 직렬화 + 블루프린트
즉 언리얼의 시스템은 단순 RTTI가 아니라, 엔진 전체를 지탱하는 메타 시스템입니다.
🧠 구조를 단순화해서 보면
언리얼 Reflection 시스템을 아주 단순화하면 다음과 비슷하게 볼 수 있습니다.
class TypeInfo
{
public:
const char* Name;
TypeInfo* Parent;
std::vector<PropertyInfo> Properties;
std::vector<FunctionInfo> Functions;
};
그리고 객체는 자신의 타입 정보를 가리킵니다.
class Object
{
public:
TypeInfo* GetTypeInfo() const;
};
타입 확인은 다음처럼 할 수 있습니다.
bool IsA(TypeInfo* targetType)
{
TypeInfo* current = GetTypeInfo();
while (current)
{
if (current == targetType)
return true;
current = current->Parent;
}
return false;
}
이 구조가 확장되면
- 타입 이름 확인
- 부모 클래스 확인
- 프로퍼티 순회
- 함수 호출
- 직렬화
- GC 참조 추적
같은 기능으로 이어질 수 있습니다.
물론 실제 언리얼 코드는 훨씬 복잡합니다.
하지만 핵심 방향은 비슷합니다.
👉 “객체가 자기 타입 정보를 알고 있고, 엔진은 그 정보를 이용해 객체를 관리”하는 구조입니다.
🔥 중요한 포인트
여기서 가장 중요한 점은 이것입니다.
Unreal Engine이 RTTI를 직접 구현한 이유는 단순히 dynamic_cast를 피하기 위해서가 아닙니다.
더 큰 이유는
👉 C++ 언어가 기본 제공하지 않는 엔진 수준의 메타데이터 시스템이 필요했기 때문입니다.
즉, 언리얼의 Reflection 시스템은 다음을 가능하게 합니다.
- 런타임 타입 확인
- 안전한 캐스팅
- 에디터 속성 노출
- 블루프린트 연동
- 직렬화
- 네트워크 복제
- 가비지 컬렉션
이 정도 기능은 C++ 기본 RTTI만으로는 처리하기 어렵습니다.
🎮 게임 엔진 관점에서 보면
게임 엔진은 단순히 C++ 객체를 생성하고 삭제하는 프로그램이 아닙니다.
게임 엔진은 객체를
- 생성하고
- 찾고
- 저장하고
- 복제하고
- 에디터에 노출하고
- 스크립트와 연결하고
- GC로 관리해야 합니다
그러려면 객체에 대한 정보가 코드 바깥에서도 필요합니다.
즉, “C++ 컴파일러만 아는 타입 정보”로는 부족합니다.
엔진도 타입 정보를 알아야 합니다.
그래서 언리얼은 C++ 위에 자체 객체 시스템을 만든 것입니다.
🧩 마무리
이번 글에서는 왜 Unreal Engine이 RTTI를 직접 구현하는지 살펴봤습니다.
정리하면 다음과 같습니다.
- C++ RTTI는 타입 확인에는 사용할 수 있지만 메타데이터가 부족합니다.
- 언리얼은 에디터, 블루프린트, GC, 직렬화, 네트워크 복제를 위해 더 풍부한 타입 정보가 필요합니다.
- UCLASS, UPROPERTY, UFUNCTION, GENERATED_BODY는 언리얼 리플렉션 시스템에 참여하기 위한 장치입니다.
- 언리얼의 Cast, IsA, StaticClass는 자체 타입 시스템을 기반으로 동작합니다.
- 따라서 언리얼의 RTTI는 단순 캐스팅 보조 기능이 아니라 엔진 전체를 연결하는 핵심 시스템입니다.
결국 핵심은 이것입니다.
Unreal Engine은 C++ RTTI를 대체하려고 한 것이 아니라,
게임 엔진에 필요한 더 큰 Reflection 시스템을 만든 것입니다.
이 구조를 이해하면, 언리얼의 GENERATED_BODY()가 단순한 매크로가 아니라 엔진 객체 시스템의 입구라는 점을 이해할 수 있습니다.