Notice
Recent Posts
Recent Comments
Link
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
Archives
Today
Total
관리 메뉴

nomad-programmer

[Programming/C#] 리플렉션 (Reflection) 본문

Programming/C#

[Programming/C#] 리플렉션 (Reflection)

scii 2020. 9. 25. 19:40

리플렉션은 객체를 X-Ray 사진처럼 객체의 형식 정보를 들여다보는 기능이다. 이 기능을 이용하면 프로그램 실행 중에 객체의 형식 이름부터 프로퍼티 목록, 메소드 목록, 필드, 이벤트 목록까지 모두 열어볼 수 있다.
형식의 이름만 있다면 동적으로 인스턴스를 만들 수도 있고, 그 인스턴스의 메소드를 호출할 수도 있다. 심지어 새로운 데이터 형식을 동적으로 만들 수도 있다. 이렇듯 런타임에 형식 정보를 다룰 수 있게 하는 리플렉션은 한층 더 강력한 표현력을 선사한다.
.NET은 모든 형식을 들여다 볼 수 있도록 장치를 설계했다. 바로 모든 데이터 형식의 조상인 Object 형식에 GetType() 메소드를 만들어 놓았다.

Object.GetType() 메소드와 Type 클래스

Object는 모든 데이터 형식의 조상이다. 이 말은 즉, 모든 데이터 형식은 Object 형식이 갖고 있는 다음의 메소드를 물려받아 갖고 있다는 뜻이다.

  • Equals()
  • GetHashCode()
  • GetType()
  • ReferenceEquals()
  • ToString()

GetType() 메소드는 객체의 형식 정보를 반환하는 기능을 한다. 모든 데이터 형식이 GetType() 메소드를 갖고 있기 때문에 어떤 객체에 대해서도 이 메소드를 호출해서 그 객체의 형식 정보를 얻어낼 수 있다.
GetType() 메소드는 Type 형식의 결과를 반환한다. Type형식은 .NET 에서 사용되는 데이터 형식의 모든 정보를 담고 있다. 형식 이름, 소속되어 있는 어셈블리 이름, 프로퍼티 목록, 메소드 목록, 필드 목록, 이벤트 목록, 심지어는 이 형식이 상속하고 있는 인터페이스의 목록까지도 갖고 있다.
Object.GetType() 메소드와 Type 형식을 사용하는 방법은 다음과 같다.

int a = 0;

Type type = a.GetType();
FieldInfo[] fields = type.GetFields(); // 필드 목록 조회

foreach(FieldInfo field in fields)
    Console.WriteLine("Type:{0}, Name: {1}", field.FieldType.Name, field.Name);

Type 형식의 메소드를 이용하면 다른 정보들도 뽑아낼 수 있다. 사용 빈도가 높은 메소드들은 아래의 표와 같다.

메소드 반환 형식 설명
GetConstructors() ConstructorInfo[] 해당 형식의 모든 생성자 목록 반환
GetEvents() EventInfo[] 해당 형식의 이벤트 목록 반환
GetFields() FieldInfo[] 해당 형식의 필드 목록 반환
GetGenericArguments() Type[] 해당 형식의 형식 매개 변수 목록 반환
GetInterfaces() Type[] 해당 형식이 상속하는 인터페이스 목록 반환
GetMembers() MemberInfo[] 해당 형식의 멤버 목록 반환
GetMethods() MethodInfo[] 해당 형식의 메소드 목록 반환
GetNestedTypes() Type[] 해당 형식의 내장 형식 목록 반환
GetProperties() PropertyInfo[] 해당 형식의 프로퍼티 목록 반환

GetField() 메소드나 GetMethods() 같은 메소드는 검색 옵션을 지정할 수 있다. public 항목만 조회할 수 있고, 비 public 항목만 조회할 수도 있다. 그리고 public과 비 public 항목을 같이 조회할 수도 있다. 또한 static 항목만 조회할 수도 있고 인스턴스 항목만 조회할 수도 있다. 아니면 모든 조건들을 포함하는 조건을 만들 수도 있다. 이러한 검색 옵션은 System.Reflection.BindingFlags 열거형을 이용해서 구성된다.

다음은 BindingFlags 열거형을 이용해서 GetFields() 메소드에 검색 옵션을 입력하는 예제이다.

Type type = a.GetType();

// public 인스턴스 필드 조회
var fields1 = type.GetFields(BindingFlags.Public | BindingFlags.Instance);

// 비 public 인스턴스 필드 조회
var fields2 = type.GetFields(BindingFlags.NonPublic | BindingFlags.Instance);

// public 정적 필드 조회
var fields3 = type.GetFields(BindingFlags.Public | BindingFlags.Static);

// 비 public 정적 필드 조회
var fields4 = type.GetFields(BindingFlags.NonPublic | BindingFlags.Static);

GetFields()나 GetMethods() 등의 메소드는 BindingFlags 매개 변수를 받지 않는 버전으로도 오버로딩되어 있다. 이 경우 이들 메소드는 public 멤버만 반환한다. 

int 형식의 주요 정보를 출력하는 예제

using System;
using System.Reflection;

namespace test
{
    internal class Program
    {
        static void PrintInterfaces(Type type)
        {
            Console.WriteLine("----- Interfaces -----");

            Type[] interfaces = type.GetInterfaces();
            foreach (Type i in interfaces)
                Console.WriteLine("Name: {0}", i.Name);
            Console.WriteLine();
        }

        static void PrintFields(Type type)
        {
            Console.WriteLine("----- Fields -----");

            FieldInfo[] fields = type.GetFields(
                BindingFlags.NonPublic | BindingFlags.Public |
                BindingFlags.Static | BindingFlags.Instance);
            foreach (FieldInfo field in fields)
            {
                string accessLevel = "protected";
                if (field.IsPublic) accessLevel = "public";
                else if (field.IsPrivate) accessLevel = "private";

                Console.WriteLine("Access: {0}, Type: {1}, Name: {2}",
                    accessLevel, field.FieldType.Name, field.Name);
            }

            Console.WriteLine();
        }

        static void PrintMethods(Type type)
        {
            Console.WriteLine("----- Methods -----");

            MethodInfo[] methods = type.GetMethods();
            foreach (MethodInfo method in methods)
            {
                Console.Write("Type: {0}, Name: {1}, Parameter: ",
                    method.ReturnType.Name, method.Name);

                ParameterInfo[] args = method.GetParameters();
                for (int i = 0; i < args.Length; i++)
                {
                    Console.Write("{0}", args[i].ParameterType.Name);
                    if (i < args.Length - 1)
                        Console.Write(", ");
                }

                Console.WriteLine();
            }

            Console.WriteLine();
        }

        static void PrintProperties(Type type)
        {
            Console.WriteLine("----- Properties -----");

            PropertyInfo[] properties = type.GetProperties();
            foreach (PropertyInfo property in properties)
            {
                Console.WriteLine("Type: {0}, Name: {1}",
                    property.PropertyType.Name, property.Name);
            }

            Console.WriteLine();
        }

        static void PrintConstructors(Type type)
        {
            Console.WriteLine("----- Constructors -----");

            ConstructorInfo[] constructors = type.GetConstructors();
            foreach (ConstructorInfo construct in constructors)
            {
                Console.WriteLine("Name: {0}", construct.Name);
            }

            Console.WriteLine();
        }

        static void PrintNestedTypes(Type type)
        {
            Console.WriteLine("----- NestedTypes -----");

            Type[] nestedTypes = type.GetNestedTypes();
            foreach (Type ntype in nestedTypes)
            {
                Console.WriteLine("Name: {0}", ntype.Name);
            }

            Console.WriteLine();
        }

        public static void Main(string[] args)
        {
            int a = 0;
            Type type = a.GetType();

            PrintInterfaces(type);
            PrintFields(type);
            PrintProperties(type);
            PrintMethods(type);
            PrintConstructors(type);
            PrintNestedTypes(type);
        }
    }
}


/* 결과

----- Interfaces -----
Name: ISpanFormattable
Name: IComparable
Name: IComparable`1
Name: IConvertible
Name: IEquatable`1
Name: IFormattable

----- Fields -----
Access: private, Type: Int32, Name: m_value
Access: public, Type: Int32, Name: MaxValue
Access: public, Type: Int32, Name: MinValue

----- Properties -----

----- Methods -----
Type: Int32, Name: CompareTo, Parameter: Object
Type: Int32, Name: CompareTo, Parameter: Int32
Type: Boolean, Name: Equals, Parameter: Object
Type: Boolean, Name: Equals, Parameter: Int32
Type: Int32, Name: GetHashCode, Parameter: 
Type: String, Name: ToString, Parameter: 
Type: String, Name: ToString, Parameter: String
Type: String, Name: ToString, Parameter: IFormatProvider
Type: String, Name: ToString, Parameter: String, IFormatProvider
Type: Boolean, Name: TryFormat, Parameter: Span`1, Int32&, ReadOnlySpan`1, IFormatProvider
Type: Int32, Name: Parse, Parameter: String
Type: Int32, Name: Parse, Parameter: String, NumberStyles
Type: Int32, Name: Parse, Parameter: String, IFormatProvider
Type: Int32, Name: Parse, Parameter: String, NumberStyles, IFormatProvider
Type: Int32, Name: Parse, Parameter: ReadOnlySpan`1, NumberStyles, IFormatProvider
Type: Boolean, Name: TryParse, Parameter: String, Int32&
Type: Boolean, Name: TryParse, Parameter: ReadOnlySpan`1, Int32&
Type: Boolean, Name: TryParse, Parameter: String, NumberStyles, IFormatProvider, Int32&
Type: Boolean, Name: TryParse, Parameter: ReadOnlySpan`1, NumberStyles, IFormatProvider, Int32&
Type: TypeCode, Name: GetTypeCode, Parameter: 
Type: Type, Name: GetType, Parameter: 

----- Constructors -----

----- NestedTypes -----

*/

Object.GetType() 메소드를 사용하지 않고 형식 정보를 얻는 방법

Object.GetType() 메소드는 반드시 객체의 인스턴스가 있어야 호출이 가능하다. 예를 들어 순수하게 int 형식의 정보를 열어보는 코드를 작성하려고 해도 int의 인스턴스를 만들고 초기화를 해야 한다. C#에서는 Object.GetType() 외에도 형식 정보를 얻을 수 있는 typeof 연산자와 Type.GetType() 메소드를 제공한다.
typeof 연산자와 Type.GetType() 메소드는 똑같이 Type 형식을 반환하지만, typeof 연산자는 형식의 식별자 자체를 매개 변수로 받고 Type.GetType() 메소드는 형식의 전체 이름, 즉 네임스페이스를 포함한 형식 이름을 매개 변수로 받는다는 점이 다르다.

// typeof 연산자의 매개 변수는 int
Type a =typeof(int);
Console.WriteLine(a.FullName);

// Type.GetType() 메소드의 매개 변수는 형식의 네임스페이스를 포함하는 전체 이름
Type b = Type.GetTyep("System.Int32");
Console.WriteLine(b.FullName);

리플렉션을 이용하여 객체 생성 및 이용

리플렉션을 이용하여 특정 형식의 인스턴스를 만들고, 데이터를 할당하며, 메소드를 호출하는 등 이렇게 코드 안에서 런타임에 특정 형식의 인스턴스를 만들 수 있게 되면 조금 더 프로그램이 동적으로 동작할 수 있도록 구성할 수 있다.

리플렉션을 이용하여 동적으로 인스턴스를 만들기 위해서는 System.Activator 클래스의 도움이 필요하다.인스턴스를 만들고자 하는 형식의 Type 객체를 매개 변수로 넘기면, Activator.CreateInstance() 메소드는 입력받은 형식의 인스턴스를 생성하여 반환한다.

object a = Activator.CreateInstance(typeof(int));

일반화를 지원하는 버전의 CreateInstance() 메소드도 있다. List<int>의 인스턴스를 만들고 싶다면 다음과 같이 하면 된다.

List<int> list = Activator.CreateInstance<List<int>>();

인스턴스 생성만 동적으로 할 수 있는 것이 아니다. 객체의 프로퍼티에 값을 할당하는 것도 동적으로 할 수 있다. PropertyInfo 객체는 SetValue()와 GetValue()라는 메소드를 갖고 있는데 GetValue()를 호출하면 프로퍼티로부터 값을 읽을 수 있고, SetValue()를 호출하면 프로퍼티에 값을 할당할 수 있다.

다음은 PropertyInfo.SetValue() 메소드를 이용하여 동적으로 프로퍼티에 값을 기록하고 읽는 예제 코드이다.

using System;
using System.Reflection;

namespace test
{
    class Profile
    {
        public string Name { get; set; }
        public string Phone { get; set; }
    }

    internal class Program
    {
        public static void Main(string[] args)
        {
            Type type = typeof(Profile);
            object profile = Activator.CreateInstance(type);

            PropertyInfo name = type.GetProperty("Name");
            PropertyInfo phone = type.GetProperty("Phone");

            name.SetValue(profile, "C#", null);
            phone.SetValue(profile, "000-000-000", null);

            Console.WriteLine("{0}, {1}",
                name.GetValue(profile, null),
                phone.GetValue(profile, null));
        }
    }
}


/* 결과

C#, 000-000-000

*/

PropertyInfo 클래스는 프로퍼티뿐 아니라 인덱서의 정보를 담을 수도 있다. SetValue()와 GetValue()의 마지막 매개 변수는 인덱서의 인덱스를 위해 사용된다. 프로퍼티는 인덱서가 필요 없으므로 null로 할당한 것이다.

리플렉션을 이용해 메소드를 호출하는 방법

메소드의 정보를 담는 MethodInfo 클래스에는 Invoke() 라는 메소드가 있다. 이 메소드를 이용하면 동적으로 메소드를 호출하는 것이 가능해진다.

using System;
using System.Reflection;

namespace test
{
    class Profile
    {
        public string Name { get; set; }
        public string Phone { get; set; }

        public void Print()
        {
            Console.WriteLine("{0}, {1}", Name, Phone);
        }
    }

    internal class Program
    {
        public static void Main(string[] args)
        {
            Type type = typeof(Profile);
            Profile profile = (Profile) Activator.CreateInstance(type);
            profile.Name = "C#";
            profile.Phone = "000-000-000";

            MethodInfo method = type.GetMethod("Print");

            // null 매개 변수가 오는 자리에는 Invoke() 메소드가 호출할 메소드의 매개 변수가 와야 한다.
            // Profile.Print() 메소드는 매개 변수가 없으므로 null을 넘겼다.
            method.Invoke(profile, null);
        }
    }
}


/* 결과

C#, 000-000-000

*/

동적으로 인스턴스, 프로퍼티, 값 할당 메소드 호출의 예제

using System;
using System.Reflection;

namespace test
{
    class Profile
    {
        private string name;
        private string phone;

        public Profile()
        {
            name = "";
            phone = "";
        }

        public Profile(string name, string phone)
        {
            this.name = name;
            this.phone = phone;
        }

        public void Print()
        {
            Console.WriteLine($"{name}, {phone}");
        }

        public string Name
        {
            get => name;
            set => name = value;
        }

        public string Phone
        {
            get => phone;
            set => phone = value;
        }
    }

    internal class Program
    {
        public static void Main(string[] args)
        {
            // Type type = Type.GetType("DynamicInstance.Profile");
            Type type = typeof(Profile);
            MethodInfo methodInfo = type.GetMethod("Print");
            PropertyInfo nameProperty = type.GetProperty("Name");
            PropertyInfo phoneProperty = type.GetProperty("Phone");
            
            object profile = Activator.CreateInstance(type, "C++", "555-555-555");
            methodInfo.Invoke(profile, null);
            
            profile = Activator.CreateInstance(type);
            nameProperty.SetValue(profile, "C#", null);
            phoneProperty.SetValue(profile, "000-000-000", null);
            
            Console.WriteLine("{0}, {1}",
                nameProperty.GetValue(profile, null),
                phoneProperty.GetValue(profile, null));
        }
    }
}


/* 결과

C++, 555-555-555
C#, 000-000-000

*/

형식 내보내기

리플렉션을 이용하면 런타임에 원하는 형식의 정보를 읽어낼 수 있을 뿐 아니라, 그 형식의 인스턴스도 만들 수 있으며 심지어는 프로퍼티나 필드에 값을 할당하고 메소드를 호출할 수도 있다. 이 정도만 해도 충분히 강력한 기능이라고 할 수 있는데, C#은 한 술 더 떠서 프로그램 실행 중에 새로운 형식을 만들어 낼 수 있는 기능도 제공하고 있다.

동적으로 새로운 형식을 만드는 작업은 System.Reflection.Emit 네임스페이스에 있는 클래스들을 통해 이루어진다. Emit은 영어로 레이저 밤 등을 "내뿜다" 또는 지폐 등을 "발행하다"라는 뜻을 가지고 있는데, 리플렉션에서의 Emit은 프로그램이 실행 중에 만들어 낸 새 형식을 CLR의 메모리에 "내보낸다"는 의미로 생각하면 이해하는데 도움이 된다.

다음 표는 Emit 네임스페이스에서 제공하는 클래스의 목록을 나타낸다. 이들 클래스들은 코드 요소를 만든다는 의미에서 ~Builder의 꼴의 이름을 갖고 있다. 예를 들어 새 형석을 만드는 클래스는 TypeBuilder, 새 메소드를 만드는 클래스는 MethodBuilder라는 이름을 갖고 있다.

클래스 설명
AssemblyBuilder 동적 어셈블리를 정의하고 나태냄
ConstructorBuilder 동적으로 만든 클래스의 생성자를 정의하고 나타냄
CustomAttributeBuilder 사용자 정의 어트리뷰트를 만듦
EnumBuilder 열거 형식을 정의하고 나타냄
EventBuilder 클래스의 이벤트를 정의하고 나타냄
FieldBuilder 필드를 정의하고 나타냄
GenericTypeParameterBuilder 동적으로 정의된 형식(클래스)과 메소드를 위한 일반화 형식 매개 변수를 정의하고 생성
ILGenerator MSIL(Microsoft Intermediate Language) 명령어를 생성
LocalBuilder 메소드나 생성자 내의 지역 변수를 나타냄
MethodBuilder 동적으로 만든 클래스의 메소드(또는 생성자)를 정의하고 나타냄
ModuleBuilder 동적 어셈블리 내의 모듈을 정의하고 나타냄. 
OpCodes ILGenerator 클래스의 멤버를 이용한 내보내기 작업에 사용할 MSIL 명령어의 필드 표현을 제공
ParameterBuilder 매개 변수 정보를 생성하거나 결합
PropertyBuilder 형식(클래스)의 프로퍼티를 정의
TypeBuilder 실행 중에 클래스를 정의하고 생성
  1. AssemblyBuilder를 이용해서 어셈블리를 만든다.
  2. ModuleBuilder를 이용해서 1에서 생성한 어셈블리 안에 모듈을 만들어 넣는다.
  3. TypeBuilder를 2에서 생성한 모듈 안에 클래스(형식)를 만들어 넣는다.
  4. 3에서 생성한 클래스 안에 메소드(MethodBuilder 이용)나 프로퍼티(PropertyBuilder 이용)를 만들어 넣는다.
  5. 4에서 생성한 것이 메소드라면, ILGenerator를 이용해서 메소드 안에 CPU가 실행할 IL 명령들을 넣는다.
.NET 프로그램의 계층 구조

[어셈블리] -> [모듈] -> [클래스] -> [메소드] 또는 [프로퍼티]

AssemblyBuilder는 스스로를 생성하는 생성자가 없다. 그래서 다른 팩토리 클래스(객체의 생성을 담당하는 클래스)의 도움을 받아야 한다.

어셈블리가 .NET 프로그램의 최상위 파일 단위이기 하지만, 메모리 안에서는 그렇지 않다. .NET 프로그램을 실행하면 메모리에 프로세스가 생성되고, 그 안에 AppDomain이 만들어진다. AppDomain은 어셈블리를 로딩해서 실행할 코드를 메모리에 적재한다. 그렇기때문에 AssemblyBuilder 객체를 만들 수 있는 클래스가 System.AppDomain이다.

동적으로 새로운 형식을 만드는 예제

using System;
using System.Reflection;
using System.Reflection.Emit;

namespace test
{
    internal class Program
    {
        public static void Main(string[] args)
        {
            AssemblyBuilder newAssembly =
                AppDomain.CurrentDomain.DefineDynamicAssembly(
                    new AssemblyName("CalculatorAssembly"), AssemblyBuilderAccess.Run);

            ModuleBuilder newModule = newAssembly.DefineDynamicModule("Calculator");

            TypeBuilder newType = newModule.DefineType("Sum1To100");

            // typeof(int) -> 반환 형식
            // new Type[0] -> 매개 변수
            MethodBuilder newMethod = newType.DefineMethod(
                "Calculate", MethodAttributes.Public, typeof(int), new Type[0]);

            ILGenerator generator = newMethod.GetILGenerator();

            // Emit은 프로그램이 실행 중에 만들어 낸 새 형식을 CLR의 메모리에 내보낸다. 라는 뜻이다.
            // 32비트 정수(1)를 계산 스택에 넣는다.
            generator.Emit(OpCodes.Ldc_I4, 1);

            for (int i = 2; i <= 100; i++)
            {
                generator.Emit(OpCodes.Ldc_I4, i);
                // 계산 후 계산 스택에 담겨 있는 두 개의 값을 꺼내서 더한 후, 그 결과를 다시 계산 스택에 넣는다.
                generator.Emit(OpCodes.Add);
            }

            generator.Emit(OpCodes.Ret);
            newType.CreateType();

            object sum1To100 = Activator.CreateInstance(newType);
            MethodInfo Calculate = sum1To100.GetType().GetMethod("Calculate");
            Console.WriteLine(Calculate.Invoke(sum1To100, null));
        }
    }
}

/* 결과

5050

*/
Comments