의미
'Reflection'의 사전적 의미는 반사, 반영으로 Reflective한 프로그래밍은 프로세스가 '자기성찰적' 능력을 발휘할 수 있도록 하는 메커니즘입니다. JAVA, C# 등 많은 언어(주로 스크립트 언어와 같이 높은 수준의 VM 프로그램 언어)에 이러한 Reflection API가 내장되어있고, 이를 사용하면 런타임에 코드를 검사할 수 있습니다. 좀 더 자세히 얘기하자면, Reflection은 런타임에 인스턴스의 데이터 타입 정보를 확인할 수 있는 기능입니다.
Reflection은 종종 객체 지향 프로그래밍의 맥락에서 이야기됩니다. 런타임에 Codebase Entities(실체)를 검색하기 위해 Reflection을 사용하는 경우가 많습니다. 각종 언어에서 제공해주는 Reflection API를 사용하면 시스템 내에서 클래스, 메서드, 속성, 타입 등을 검사할 수 있고 이를 통해 보다 동적인 기능을 구축할 수 있게 됩니다.
활용
Reflection의 일반적인 용도 중 하나는 테스트입니다. Reflection은 클래스의 내부 행동을 노출시킴으로써 클래스를 모방하는 데 도움이 될 수 있습니다. 일반적으로 protected나 private 접근제한자를 가진 클래스 멤버 함수는 테스트할 수 없습니다. Reflection을 이용하면, Visibility 제약을 오버라이드하여 유닛 테스트에서 public으로 사용하도록 할 수 있습니다. PHP로 작성한 다음 예를 통해 그 내용을 확인할 수 있습니다.
class Test
{
protected int $Value; // 임의의 int형 protected 속성 선언
// 생성자 메서드
public function __construct(int $Value) {
$this -> Value = $Value;
}
// Value의 두 배 값을 반환하는 메서드
protected function computeValue() : int {
return ($this -> Value * 2);
}
}
// 01. 리플렉션을 사용하지 않은 경우 :
$t = new Test(10); //임의의 Test객체 생성 후 10으로 초기화.
assert($t -> computeValue() === 20); // protected 메서드 호출.
// 메서드에 공개적 접근 불가 오류 발생 - the method isn't publicly accessible
// 02. 리플렉션을 사용한 경우 :
// ReflectionMethod를 사용하여 Test클래스의 computeValue 메서드를 가져오기.
$reflectionMethod = new ReflectionMethod(Test::CLASS, "computeValue");
$reflectionMethod -> setAccessible(true); // 해당 메서드에 접근 가능하도록 설정.
$t = new Test(10); // 임의의 Test객체 생성 후 10으로 초기화.
assert($reflectionMethod -> invoke($t) === 20); // 오류 없이 정상 실행.
위의 코드는 단순히 리플렉션 테스트를 위해 임시로 작성한 것이지 실제로 리플렉션을 테스트에 광범위하게 사용하는 것은 코드베이스에 더 큰 문제가 있음을 나타내는 경우가 많습니다. 클래스 인터페이스가 지나치게 제한적이고 책임에 맞지 않다는 것을 의미하기도 합니다. 그래서 대부분의 경우 protected 메서드를 자체 public 인터페이스를 노출하는 새로운 클래스를 만드는 방식으로 리팩토링하는 것이 더 적절합니다. 그 방법은 아래와 같습니다.
class Calculator {
public function computeValue() : int {
return ($this -> Value * 2);
}
}
class Test {
protected int $Value;
protected Calculator $Calculator;
public function __construct(int $Value, Calculator $Calculator) {
$this -> Value = $Value;
$this -> Calculator = $Calculator;
}
}
Calculator 컴포넌트는 이제 테스트 가능한 public 인터페이스를 갖춘 자체 독립형 장치가 되었습니다. 이제 테스트 클래스에 계산 로직을 구현하는 계산기가 제공되는 셈입니다.
[참고 자료]
https://www.howtogeek.com/devops/what-is-reflection-in-programming/
언리얼의 리플렉션 시스템
언리얼 엔진 테크놀로지의 근간을 이룬다고 할만큼 언리얼 내의 다수의 시스템(Editor Detail Panel, GC, Network Reflication, BP<->C++ Comunication 등)에 탑재된 것으로, 프로그램이 실행시간에 자기 자신을 조사하는 기능입니다. 당연하게도 C++은 어떠한 형태의 리플렉션도 지원해주지 않기 때문에, 언리얼은 자체적으로 C++ 클래스, 구조체, 함수, 멤버변수, 열거형 등과 관련된 정보를 수집, 질의 조작하는 별도의 리플렉션 시스템을 구축했습니다. 언리얼에서는 그래픽 용어(반사)로의 혼동을 피하기 위해 이러한 리플렉션을 '프로퍼티 시스템'이라고 부릅니다.
사용법
UHT가 프로젝트를 컴파일할 때 리플렉션 관련 정보를 수집합니다. 헤더의 마지막 #include 구문에 아래와 같은 특수한 include를 추가해주면 UHT에 해당 파일이 리플렉션될 것임을 알려줄 수 있습니다.
#include "FileName.generated.h"
위와 같은 헤더를 추가했다면, UENUM(), UCLASS(), USTRUCT(), UFUNCTION(), UPROPERTY() 등의 매크로를 멤버 선언 전에 사용하고 괄호 안에 다양한 추가 지정자 키워드를 통해 속성(블루프린트에서 호출 가능한지, 에디터 내에서 편집 가능한지, 보여줄지 등)을 지정해줄 수 있습니다. 지정자 키워드 각각의 의미와 사용법은 ObjectBase.h 에서 확인할 수 있습니다.
예제
// 이동식 유닛(병사)의 기본 클래스
#include "StrategyTypes.h"
#include "StrategyChar.generated.h"
UCLASS(Abstract) // UCLASS 매크로를 사용하여 리플렉션 되었음을 나타냄.
class AStrategyChar : public ACharacter, public IStrategyTeamInterface
{
// 이 매크로는 클래스 본문에 추가적인 함수나 typedef를 주입하기 때문에,
// 리플렉션 된 클래스나 구조체(이 경우 USTRUCT)에 필수적이다.
GENERATED_UCLASS_BODY() // 위의 UCLASS 매크로와 짝을 이룸.
// 이 pawn이 죽을 때 얼마나 많은 자원의 가치가 있는지에 관한 변수.
UPROPERTY(EditAnywhere, Category=Pawn)
int32 ResourcesToGather;
// 무기 슬롯에 무기를 장착하는 함수.
UFUNCTION(BlueprintCallable, Category=Attachment)
void SetWeaponAttachment(class UStrategyAttachment* Weapon);
// 무기가 장착되었는지를 검사.
UFUNCTION(BlueprintCallable, Category=Attachment)
bool IsWeaponAttached();
protected:
// 전투 애니메이션
UPROPERTY(EditDefaultsOnly, Category=Pawn)
UAnimMontage* MeleeAnim;
// 갑옷 부착 슬롯
UPROPERTY()
UStrategyAttachment* ArmorSlot;
// 아래와 같이 리플렉션된 프로퍼티와 아닌 것을 같은 클래스에 섞어도 됨.
// 다만 포인터를 리플렉션하지 않으면 GC가 레퍼런스를 확인할 수 없기 때문에 위험.
uint8 MyTeamNum; // 팀원 수
//이하 코드 생략
};
작동원리
UBT와 UHT가 함께 런타임 리플렉션을 강화시키는 데 필요한 데이터를 생성합니다.
- UBT : 헤더 스캔 -> 리플렉션된 유형이 최소 하나 있는 헤더가 들어있는 모듈을 기억. -> 그 헤더 중 어떤 것이든 지난 번 컴파일 이후 변경되었다면, UHT를 실행하여 리플렉션 데이터 수집, 업데이트.
- UHT : 헤더 파싱 -> 리플렉션 데이터 셋 빌드 -> (모듈별.generated.inl에 기여하는)리플렉션 데이터가 들어있는 C++ 코드 생성(헤더별.generated.h), 다양한 헬퍼 및 thunk 함수 생성.
리플렉션 데이터를 C++ generated 코드로 저장하는 것의 장점은 Binary와의 동기화가 보장된다는 점입니다. 따라서 오래되거나 버전이 맞지 않는 리플렉션 데이터를 로드할 일은 없는데, 나머지 엔진 코드와 함께 컴파일되기 때문입니다. 그리고 특정 플랫폼/컴파일러/최적화 콤보의 패킹 작동 방식을 역 엔지니어링 하려 하기 보다는, C++ 표현식을 사용하여 시작 시 멤버 오프셋 등을 계산하기 때문입니다. UHT도 generated 헤더를 소모하지 않는 독립형 프로그램으로 만들어졌기에, UE3의 스크립트 컴파일러에서 흔히 발생했던 '닭이냐 계란이냐 문제'가 생기지 않습니다.
generated 함수에는 StaticClass() / StaticStruct() 같은 것이 포함되어 있어, 유형에 대한 리플렉션 데이터를 구하는 것이 쉬우며, 블루프린트나 네트워크 Reflication에서 C++함수를 호출하는 데 사용되는 thunk를 구하는 것도 쉬워집니다.
[참고 자료]
https://www.unrealengine.com/en-US/blog/unreal-property-system-reflection
'메타버스SW아카데미' 카테고리의 다른 글
언리얼 프로젝트 폴더관리와 gitignore (2) | 2023.07.05 |
---|---|
GitHub란 무엇일까? (0) | 2023.07.04 |