외로운 Nova의 작업실

c# 언어 공부 - 11(리플렉션) 본문

Programming/C#

c# 언어 공부 - 11(리플렉션)

Nova_ 2022. 5. 12. 03:35

안녕하세요. 오늘은 c# 언어의 LINQ기능에 이어서 리플렉션에대해서 배워보도록 하겠습니다.

 

<리플렉션>

리플렉션은 객체의 형식 정보를 들여다보는 기능입니다.

객체의 형식정보에는 프로퍼티 목록, 메소드 목록, 필드, 이벤트 목록까지 객체의 모든게 있습니다.

이러한 리플렉션 기능을 위해서 c# 개발자는 객체의 모든 조상 Object에 GetType()메소드를 만들어 놨습니다.

GetType()메소드는 Type형식의 객체를 반환합니다.

Type객체는 그객체의 모든 정보가 들어가게됩니다.

코드를 먼저 보시죠.

using System;
using System.Reflection;

namespace StudyCSharp
{
    class MainApp
    {
        static void Main(string[] args)
        {
            int b = 12;

            Type b_type = b.GetType(); //b의 type객체를 반환합니다.
            FieldInfo[] fields = b_type.GetFields();  // b의 type 객체에서 field에대한 정보를 뽑아냅니다.

            foreach(FieldInfo field in fields)
            {
                Console.WriteLine(field.Name);
            }
        }   
    }

}

위 코드의 콘솔 실행 결과는 아래와 같습니다.

리플렉션

실행결과로 알 수 있듯이 b객체의 필드에는 MaxValue아 MinValue가 있다는 것입니다.

Type객체에서 GetFields()메소드를 이용해서 필드에대한 정보를 볼 수 있었습니다.

하지만, 필드말고도 여러가지 것들을 볼 수 있다고 했었죠?

아래는 여러가지 것들을 뽑아내는 Type객체의 메소드들의 대한 표입니다.

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

그렇다면 객체의 필드에는 Private 한정자와 Public 한정자가 있기마련인데, GetFields()로 객체의 필드에대한 정보를 가져오면 한정자에 관계없이 모든 필드를 가져올까요?

답변은 아닙니다. 원래는 GetFields()같은 정보를 가져오는 메소드들의 매개변수로 public을 가져올지 private를 가져올지에대한 정보를 줘야합니다.

하지만 아무것도 쓰지 않으면 public 변수들만 가져오게됩니다.

그럼 private를 가져오려면 어떻게 매개변수를 줘야할까요?

바로 System.Reflection.BindingFlags 열거형을 이용합니다.

백문이 불여일견, 코드를 보시죠.

using System;
using System.Reflection;

namespace StudyCSharp
{
    class MainApp
    {
        static void Main(string[] args)
        {
            test b = new test();

            Type b_type = b.GetType(); //b의 형식을 반환합니다.
            FieldInfo[] public_fields = b_type.GetFields(BindingFlags.Public | BindingFlags.Instance);  // b의 형식에서 public인 인스턴스 필드를 반환합니다. 
            FieldInfo[] private_fields = b_type.GetFields(BindingFlags.NonPublic | BindingFlags.Instance); //b의 형식에서 public이 아닌 인스턴스 필드를 반환합니다.
            foreach(FieldInfo field in public_fields)
            {
                Console.WriteLine(field.Name);
            }
            Console.WriteLine("-----------------------------------------------------");
            foreach (FieldInfo field in private_fields)
            {
                Console.WriteLine(field.Name);
            }
        }   

        class test
        {
            public int pub = 3;
            private int pri = 3;
        }
    }

}

위의 실행결과는 아래와 같습니다.

BindingFlags

그렇다면 BindingFlags는 더 있을텐데, 어디서 그 내용을 볼 수 있을까요?

https://docs.microsoft.com/ko-kr/dotnet/api/system.reflection.bindingflags?view=net-6.0

 

BindingFlags 열거형 (System.Reflection)

리플렉션으로 멤버 및 형식에 대한 검색을 수행하는 방법과 바인딩을 제어하는 플래그를 지정합니다.

docs.microsoft.com

위 링크에서 좀 더 볼 수 있습니다.

또한 구글링을 하면 좀 더 예시에대해서 많이 볼 수 있습니다.

 

<리플렉션을 이용해서 동적으로 객체 생성하기>

다시한번, 리플렉션은 객체의 정보를 열어보는 기능입니다. 

이 목적을 실행하기위해 GetType()을 c#개발자들이 준비해둔거죠.

근데 이러한 리플렉션을 이용해서 동적으로(프로그램 실행중에) 객체를 생성 할 수 있습니다.

생각해보시죠, 지금까지 배운것으로 프로그램을 실행중에 객체를 생성 할 수 있는 방법은 없습니다.

동적으로 객체를 만들기 위해서는 System.Activator 클래스의 도움이 필요합니다.

Activator.CreateInstance() 메소드에 객체로 만들고자하는 형식의 Type을 매개변수로 주게되면, 그 형식의 객체를 생성하여 반환합니다.

아래는 예시입니다.

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

위 코드에서 Typeof는 클래스의 Type 형식을 반환합니다.

원래는 GetType()메소드로 매개변수에 객체를 주었지만, 위 방법을 활용하면 클래스의 Type형식도 반환할 수 있습니다.

그렇다면 동적으로 객체를 생성하는 코드를 보도록 하겠습니다.

using System;
using System.Reflection;

namespace StudyCSharp
{
    class MainApp
    {
        class Profile
        {
            public string name
            {
                get; set;
            }
            public int age
            {
                get; set;
            }
        }
        static void Main(string[] args)
        {
            Type type = typeof(Profile); //프로파일 클래스의 형식을 가져옵니다.
            object profile = Activator.CreateInstance(type); //profile형식을 가진 객체를 반환합니다.

            PropertyInfo name = type.GetProperty("name"); //profile 형식에서 name이란 이름가진 프로퍼티에대한 정보를 가져옵니다.
            PropertyInfo age = type.GetProperty("age"); //profile 형식에서 age란 이름을 가진 프로퍼티에대한 정보를 가져옵니다.

            name.SetValue(profile, "창모", null); //name 프로퍼티를 profile객체에 창모라는 string으로 값을 저장합니다.
            age.SetValue(profile, 26, null); //age프로퍼티를 profile객체에 26이라는 int 값으로 저장합니다.

            Console.WriteLine(name.GetValue(profile, null) + age.GetValue(profile, null).ToString()); 
        }

    }

}

주석과 함께 읽으면 코드 해석이 좀 용이하실겁니다.

SetValue()와 GetValue()의 마지막 매개변수에 null은 원래 인덱서의 인덱스를 위해 존재합니다.

프로퍼티는 인덱스가서가 아니므로 null을 넣어줍니다.

실행결과는 아래와 같습니다.

위 코드를 좀 더 머릿속에서 상상해보면, 프로퍼티를 여러개 만들어서 둥둥 띄워놓고, 내가 넣고 싶은 객체에 떠다니는 프로퍼티를 넣는 느낌이라고 상상 할 수 있습니다.

원래 전 시간까지 배웟던 프로퍼티는 둥둥 띄워놓지못하고 객체에 무조건 박혀있는 느낌이였는데 말이죠...

우리는 이제 모든 형식들을 둥둥 띄울 수 있습니다!

위 코드에서 만약 "창모"와 26을 Console.ReadLine()으로 받는다면 프로그램 실행중에도 2가지의 프로퍼티를 가진 '새로운' 객체를 만들 수 있을 겁니다.

리플렉션이없다면 새로운게 아니라 기존에 있는 만들어져있는 객체를 수정할 수 밖에는 없겠죠..

 

<리플렉션을 이용해서 동적으로 클래스 생성하기>

위에서는 동적으로 객체를 생성하는 것을 배웠습니다.

이미 만들어 놓은 클래스를 프로그램 실행중에 객체를 새롭게 생성하는 방법이지요.

하지만, c#의 리플렉션은 동적으로 객체가 아닌 클래스도 만들 수 있습니다.

구현 방법은 리플렉션의 Emit클래스에서 제공하는 메소드들을 사용하면 됩니다.

아래는 Emit클래스안에 들어있는 클래스입니다.

클래스 설명
AssemblyBuilder 동적 어셈블리를 정의하고 나타냅니다.
ModuleBuilder 모듈을 정의하고 나타냅니다.
TypeBuilder 쿨래스를 만들어 넣습니다.
MethodBuilder 클래스안에 메소드를 만들어 넣습니다.
PropertyBuilder 클래스안에 프로퍼티를 만들어 넣습니다.
ILGenderator MSIL명령어를 생성합니다.

 동적으로 클래스를 생성하는 방법은 아래의 순서대로 진행이됩니다.

1. AssemblyBuilder를 이용해서 어셈블리를 만듭니다.

2. ModuleBuilder를 이용해서 1에서 생성한 어셈블리 안에 모듈을 만들어 넣습니다.

3. 2에서 생성한 모듈에 TypeBuilder로 클래스를 만들어 넣습니다.

4. 3에서 생성한 클래스안에 메소드나 프로퍼티를 만들어 넣습니다.

5. 4에서 생성한 것이 메소드라면, ILGenerator를 이용해서 IL명령어들을 넣습니다.

 

백문이 불여일견, 10!를 계산하는 메소드를 가진 클래스를 동적으로 만드는 코드를 짜보도록 하겠습니다.

 

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

namespace StudyCSharp
{
    class MainApp
    {
        
        static void Main(string[] args)
        {

            //-----------------동적으로 클래스 만들기----------------------------------------------------//

            AssemblyBuilder newAssembly = AssemblyBuilder.DefineDynamicAssembly(
                new AssemblyName("factorial"), AssemblyBuilderAccess.Run); //새로운 어셈블리 bulider를만듭니다.

            ModuleBuilder newModule = newAssembly.DefineDynamicModule("factorial"); //새로운 모듈 builder를 만듭니다.

            TypeBuilder newType = newModule.DefineType("10factorial"); //새로운 클래스 builder를 만듭니다.

            MethodBuilder newMethod = newType.DefineMethod(
                "factorial", //메소드 이름
                MethodAttributes.Public, //메소드 접근 한정자
                typeof(int), //반환 형식
                new Type[0]);//매개변수

            ILGenerator generator = newMethod.GetILGenerator(); //새로운 generator를 만듭니다..

            generator.Emit(OpCodes.Ldc_I4, 1); //정수 1을 스택에 넣습니다.

            for(int i = 2; i < 11; i++)
            {
                generator.Emit(OpCodes.Ldc_I4, i); //차례대로 i값을 스택에 넣습니다.
                generator.Emit(OpCodes.Mul); //스택에서 2개의 값을 꺼내 곱하고 다시 스택에 넣습니다.
            }

            generator.Emit(OpCodes.Ret); //계산 스택에 담겨있는 값을 반환합니다.

            newType.CreateType(); //해당 클래스를 메모리에 CLR에 제출합니다.

            //-------------------동적으로 만든 클래스를 실제 객체로 변환--------------------------------------//

            object factorial = Activator.CreateInstance(newType);//newtype 클래스의 새로운 객체를 반환합니다.
            MethodInfo factorial10 = factorial.GetType().GetMethod("factorial"); //객체의 Type객체를 반환하고 factorial이름을 가진 메소드를 반환합니다.
            Console.WriteLine(factorial10.Invoke(factorial, null));


        }

    }

}

위코드의 결과는 아래와 같습니다.

클래스를 동적으로 생성하기

OpCode의 값은 아래 링크에서 보실 수 있습니다.

https://docs.microsoft.com/ko-kr/dotnet/api/system.reflection.emit.opcodes.add?view=net-6.0 

 

OpCodes.Add 필드 (System.Reflection.Emit)

두 개 값을 더하여 결과를 계산 스택으로 푸시합니다.

docs.microsoft.com

조금은 어렵지만, 하나씩하나씩 살펴보고, 아 이렇게 코드를 쓰는구나~ 정도만 알아도 좋을 것 같습니다.

다음시간에는 에트리뷰트에대해서 배워보도록 하겠습니다.

Comments