나만의 작은 도서관
[TIL][C++] 250801 MMO 서버 개발 71일차: 언리얼 프로젝트 소스코드에서 리플렉션 매크로를 사용하는 근본적인 이유 본문
Today I Learn
[TIL][C++] 250801 MMO 서버 개발 71일차: 언리얼 프로젝트 소스코드에서 리플렉션 매크로를 사용하는 근본적인 이유
pledge24 2025. 8. 1. 23:40주의사항: 해당 글은 일기와 같은 기록용으로, 다듬지 않은 날것 그대로인 글입니다.
언리얼 프로젝트 소스코드에서 리플렉션 매크로를 사용하는 근본적인 이유
사라지는 이름들
- 기본적으로 C++은 컴파일이 되고 나면 어떤 클래스가 있고 그 안에 어떤 함수가 있는지에 대한 정보가 사라지게 된다. 왜냐하면 컴파일 과정을 지나면 함수를 호출하는 코드에서 함수의 주소만 남기 때문이다.
- 즉, 이미 컴파일이 끝난 시점이라면 외부에서 정의한 함수의 이름으로 함수를 호출하려 해도 함수 이름에 대한 정보가 더 이상 남아있지 않기 때문에 호출할 수 없다.
- (참고로, DLL과 같이 런타임에 함수를 찾아야하는 경우, 이름으로 호출할 수 있기는 하다. 하지만 이는 설명에서 예외로 둔다.)
위 특성에 따른 문제점: 언리얼 엔진이 C++에 정의한 함수를 이름으로 호출할 수 없다.
- 프로젝트 솔루션에서 코드를 짜는 이유는 특정 기능을 추가하기 위함일 것이다. 그런데 외부 프로그램인 “언리얼 엔진”은 위와 같은 이유로 프로젝트에 짠 함수를 이름을 통해 호출할 수 없다. 함수 이름을 알고 있어도 쓰지는 못한다니. 정말 모순이 아닐 수 없다.
그래서 있는 시스템 “리플렉션 시스템”
- 컴파일이 되는 순간 C++의 클래스, 함수, 변수 가릴 것 없이 이름에 대한 정보가 싹 다 날아가니 언리얼 엔진 측에선 컴파일되기 전에 엔진에서 알아야 할 데이터를 긁어와야 한다. 엔진에서 이름과 이름에 대응하는 요소의 매핑 정보를 알고만 있는다면 엔진에서도 해당 기능을 사용할 수 있을 것이다. 그래서 있는 것이 바로 “리플렉션 시스템”이다.
리플렉션 시스템의 활용 원리
- 리플렉션 시스템은 프로젝트의 모든 소스 코드를 긁어오지 않는다. 오로지 개발자가 매크로 함수를 붙여서 “긁어와야함!”을 지정한 대상만 긁어온다.
- 매크로 함수로 지정된 대상은 UHT에 의해 xxx.generated.h라는 추가적인 헤더 파일에 정보가 저장되며, 나중에 이 파일을 통해 C++ 클래스, 함수, 변수 등의 정보를 언리얼 엔진이 인식할 때 사용된다.
- 게임이 시작되면, 언리얼 엔진은 메모리에 로드된 모든 클래스, 구조체, 함수에 대한 정보를 UClass, UFunction, UProperty와 같은 “내부적인 객체”로 만들어서 관리한다.
- 즉, UCLASS(), UFUNCTION()과 같은 매크로 함수들을 사용하면 그대로 UClass, UFunction과 같은 객체로 변환된다는 것이다.
객체에는 무엇이 들어있을까? ex. UFunction
- UFUNCTION 매크로 함수에 대해 예를 들자면, 여러 가지 메타데이터를 저장하고 있겠지만 대표적인 것들만 보자면 아래와 같다.
// 1. 기본 함수 정보
// 함수 포인터
Native Func;
// 함수(지정자) 플래그(BlueprintCallable, BlueprintPure 등)
EfunctionFlags FunctionFlags;
// 매개변수 개수
uint16 NumParams;
// 매개변수 크기
uint16 ParamsSize;
// 반환값 오프셋
uint16 ReturenValueOffset;
Q. 정작 중요한 함수 이름을 저장하는 부분이랑 매개변수 리스트를 저장하는 부분이 없는데요?
- UFunction의 경우 여러 번의 상속을 받은 클래스이다. 즉, UObject → UField → UStruct → UFunction 순으로 상속 관계에 놓여있는데, 공통되는 요소는 상위 클래스들의 멤버 변수에 존재하므로, 이름은 “UObject 클래스의 이름 시스템”에, 매개변수 리스트는 “UStruect 클래스”에 존재한다.
class COREUOBJECT_API UFunction : public UStruct
{
// UFunction은 UStruct를 상속하고, UStruct는 UField를 상속하며,
// UField는 UObject를 상속합니다.
// 따라서 UObject의 이름 시스템을 사용:
// FName GetFName() const; // UObject에서 상속된 함수
// FString GetName() const; // UObject에서 상속된 함수
};
// UObject 기반 클래스의 이름 저장
class COREUOBJECT_API UObjectBase
{
private:
FName NamePrivate; // 여기에 함수 이름이 저장됨
// ...
};
// 실제 사용 예시)
// C++에서 함수 이름 접근
UFunction* MyFunc = MyClass->FindFunction(FName("MyFunctionName"));
FString FunctionName = MyFunc->GetName(); // "MyFunctionName" 반환
FName FunctionFName = MyFunc->GetFName(); // FName 형태로 반환
// 함수 생성 시 이름 설정
UFunction* NewFunction = NewObject<UFunction>(GetClass(), FName("NewFunctionName"));
결론
- C++은 컴파일 방식의 언어이며, 컴파일을 하면 함수, 변수, 클래스 할 것 없이 모든 변수의 이름에 대한 정보가 사라진다.
- 언리얼 엔진은 c++ 코드에 정의된 함수, 변수, 클래스를 엔진 내에서 사용하기 위해 리플렉션 시스템을 도입하였고, 리플렉션 시스템은 이를 위해 전처리 단계에서 호출에 필요한 메타데이터를 긁어 온다음, 엔진에 등록한다.
- 긁어온 메타데이터들은 UClass, UFunction과 같은 객체에 저장되어 있으며, 이러한 객체들을 통해 엔진에서 C++에 제작한 함수, 변수, 클래스들을 사용한다.