Reflection of Unity C#

Unity를 C#으로 개발하다보면 “리플렉션”, 즉 reflection 이라는 단어를 종종 듣는다. 이는 C/C++에서 넘어온 개발자들이라면 다소 생소한 단어일 수 있다. Java는 java.lang.reflection 이라는 패키지로 reflection을 지원하고, C#에서는 System.reflection 네임스페이스를 통해서 지원한다.

Reflection의 한글 뜻은 “거울에 비친 상” 이라는 뜻이다. Language에서 “상” 이라는 의미가 뭘까 라고 떠올렸을 때 언뜻 잘 떠오르지 않는 것이 Reflection 개념에 대한 1차 Hurdle이다.

Programming Language에서 Reflection이라는 말은 Runtime 시에 Object 객체의 형태나 타입에 대한 정보를 읽어 내는 것을 말한다. C++에서도 제한적이지만 RTTI(Run-time type information) 메커니즘을 통해서 Reflection 기능을 제공하고 있다. C++ RTTI 메커니즘 중 대표적인 것이 dynamic_cast와 typeid 이다. 아래는 dynamic_cast의 예를 보자. (Refrence)

위 코드를 실행하면 “basePointer is pointing to a Derived class object” 이 출력된다.

스크린샷 2015-03-03 오전 12.18.54

즉, dynamic_cast는 부모 클래스의 포인터 변수에서 자식 Instance의 Address를 저장하고 있더라도, 실제 자식의 타입이 특정 타입이 맞는지 체크해 준다. 맞으면 해당 Instance의 address를 틀리면 NULL을 리턴한다. 사실 C++에서 Parent Class에 virtual function을 선언하여 파생 클래스들을 다룬다면 굳이 RTTI를 사용하지 않고도 Runtime시에 실행되는 Method들을 Polymorphism(다형성)의 특성을 이용하여 제어할 수는 있다.
아래의 코드는 C++ RTTI의 다른 형태인 typeid의 용례다. (Refrence)

실행 시 출력은 다음과 같다. 원 Class 이름과 다른 runtime 시의 type name이 적절하게 출력이 된다. (Class 와 이름이 정확히 일치하지 않는 이유 : Reference)

스크린샷 2015-03-03 오전 12.20.39

즉, Class Pointer들이 담고 있는 진짜 Instance들을 알아낼 때 사용 한다.

C#은 Source Code를 .Net Assembly, 즉 CIL(Common Intermediate Language) 형태로 컴파일 한 후에 .Net Runtime(CLR(Common Language Runtime))으로 실제 실행 시키는데, 이 때 CIL 안에 Meta Data 정보를 담고 있다. Unity에서는 ScriptAssemblies 폴더 아래에 보면, Assembly-CSharp.dll 등 여러개의 컴파일된 .NET Assembly 파일들이 있다. 이 .Net Assembly 안의 저장된 Meta Data를 통해서 내부 Class들의 정보들을 알아 냄으로써 Reflection 기능이 동작할 수 있게 된다. 이를 통해서 Method Invocation, Serialization, Object Dump, Database Interface 등의 작업이 가능하다. Type을 묻고, Class Descriptor를 얻은 다음에 해당 클래스의 Method와 Field Descriptor들을 통해서 세부 접근할 수 있게 되는 것이다. C++의 Reflection 기능이었던 RTTI가 단순 타입 체크 정도 수준으로 지원한 반면에, C#의 Reflection은 Instance의 Type을 체크하거나, Field 정보를 알아오는 것은 물론, Method를 실행할 수도 있고, Field나 Method를 동적으로 추가할 수도 있다. 아래는 C#의 기본적인 Reflection의 예이다.

출력은 아래와 같다.

ReflectionTest라는 Static Class가 자기 안에 있는 3개의 Int32 Field와 1개의 String Field의 정보를 Runtime 시에 Reflection 형태로 접근하는 것을 보여준다. typeof operator로 읽어서 FieldInfo Class를 통해서 GetFields, GetValue Method들을 Access하고 있다.

Unity 엔진의 C#에서도 동일하게 Reflection을 사용할 수 있다. 그러나, iOS의 경우에는 자체 저수준 가상 머신인 LLVM 환경 에서 제한적으로 Reflection을 허용한다. 그 LLVM 상에서 돌아가는 Mono Runtime에 맞도록 미리 컴파일하는 AOT(Ahead of Time) 컴파일 방식을 채용하고 있어서 Reflection 기능 중에 매우 큰 강점인 동적인 코드 생성을 할 수 없다. 즉, Reflection.Emit 같이 JIT(Just In Time) Compile되어서 동적으로 코드를 생성하여 Native Code로 변환시키는 동작을 보안상 할 수 없게 된 것이다. 또한 C# Reflection 기능을 이용하는 Linq 쿼리는 동적 코드 생성이 발생하는 지뢰밭이므로 사용은 가능하나 섣불리 사용했다가는 iOS에서 낭패를 볼 수 있다. 다만 AOT를 요구하는 iOS 플랫폼이라도 Field 타입과 Method 리스트를 받아오거나, Method를 실행시키는 등의 기본적인 Reflection 기능들은 모두 동작한다.

이 Reflection의 용도는 다양하다. Unity에서 커스텀 Inspector를 만들 수도 있다. .NET Assembly Dll 파일을 Reflection 통해 분석하면, C++에서 Access할 수 있는 Interface Class 들을 자동으로 만들 수도 있다. 전반적으로, 해당 클래스의 타입을 알고 싶을 때, 속도와 크게 관련이 없을 때는 Reflection을 이용해서 정보들을 당겨와서 이용하면 된다.

다음은 Inspector에서 선택된 Game Object에 Attach되어 있는 스크립트의 Method들을 Inspector에서 Reflection을 이용하여 알아내고, 실행할 수 있게 해주는 Custom Inspector 스크립트의 예이다. (Reference)

custom_inspector

아래 코드는 Object에 Attach 시키는 스크립트이다.

아래는 Scripts 폴더에 정의하는 스크립트이고,

마지막으로 Editors 폴더에 정의하여 실행시에 버튼을 표시하고, 클릭 시에 Method를 Invoke시키는 Reflection 구현이 포함된 클래스이다.

Reflection 기능을 잘 이용한 Asset을 만들 수도 있다. vBug는 Reflection 이용의 끝판왕 이라고 할 만한 Unity Asset Tool 이다.

다만, Reflection 코드의 Runtime이라는 특성을 보면 알 수 있듯이 실시간으로 무엇을 체크한다는 것은 효율적인 면에서 Static보다는 느리다는 것을 짐작할 수 있다. 실제로 Reflection은 속도면에서는 Direct Access하는 것보다 수천배 느리다. GetComponent를 Update() 구문에서 남발하지 않듯이 Reflection 기능들은 Performance를 요하는 부분에서는 사용하지 않거나 매우 주의해서 사용해야 한다. (Benchmark Reference)