Programming/C#

리플렉션(Reflection)

lee308812 2019. 3. 12. 22:23

[ 리플렉션 ]


- C# 코드가 빌드되어 어셈블리에 포함되는 경우, 그에 대한 모든 정보를 조회할 수 있는 기술을 의미한다.


- 리플렉션을 이용하면 현재 AppDomain의 이름과 그 안에 로드된 어셈블리 목록을 구할 수 있다.

(AppDomain - CLR이 구현한 내부적인 격리공간. 일반적으로는 1개의 공유 AppDomain과 1개의 기본 AppDomain으로 구성됨)


- 닷넷 프로그램이 실행되면 기본적으로 1개의 AppDomain이 있어야 하는데, 이를 "기본 응용프로그램 도메인(default AppDomain)" 이라 한다.


using System;
using System.Reflection;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            AppDomain currentDomain = AppDomain.CurrentDomain;
            Console.WriteLine("Current Domain Name : " + currentDomain.FriendlyName);

            // 어셈블리 목록 조회
            foreach(Assembly asm in currentDomain.GetAssemblies())
            {
                Console.WriteLine("[ASM]" + asm.FullName);

                // 어셈블리에 포함된 모듈 조회
                foreach(Module module in asm.GetModules())
                {
                    Console.WriteLine("-[MODULE]" + module.FullyQualifiedName);

                    // 모듈에 포함된 타입 조회

                    foreach(Type type in module.GetTypes())
                    {
                        //Console.WriteLine("--[TYPE]" + type.FullName);
                    }
                }
            }
        }
    }
}



- 일반적으로 어셈블리 내에 모듈이 1개 이므로, 현실적으로는 어셈블리에서 직접 타입을 구하는게 일반적이다.


- 타입은 각종 멤버(메서드, 프로퍼티, 이벤트, 필드, ...)를 가지므로 Type.GetMembers()를 이용해 열람할 수 있다.

using System;
using System.Reflection;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            AppDomain currentDomain = AppDomain.CurrentDomain;
            Console.WriteLine("Current Domain Name : " + currentDomain.FriendlyName);

            // 어셈블리 목록 조회
            foreach(Assembly asm in currentDomain.GetAssemblies())
            {
                Console.WriteLine("[ASM]" + asm.FullName);

                // 타입 조회
                foreach(Type type in asm.GetTypes())
                {
                    Console.WriteLine("-[TYPE] " + type.FullName);

                    // 멤버 조회
                    foreach(MemberInfo memberInfo in type.GetMembers())
                    {
                        Console.WriteLine("--[MEMBER] " + memberInfo.Name);
                    }
                }
            }
        }
    }
}


- 멤버를 유형별로 구하는 것도 가능하다.

                // 타입 조회
                foreach(Type type in asm.GetTypes())
                {
                    Console.WriteLine("-[TYPE] " + type.FullName);

                    // 클래스에 정의된 생성자를 열거
                    foreach (ConstructorInfo ctorInfo in type.GetConstructors())
                    {
                        Console.WriteLine("--[CTOR] " + ctorInfo.Name);
                    }

                    // 클래스에 정의된 이벤트를 열거
                    foreach (EventInfo eventInfo in type.GetEvents())
                    {
                        Console.WriteLine("--[EVENT] " + eventInfo.Name);
                    }

                    // 클래스에 정의된 필드를 열거
                    foreach (FieldInfo fieldInfo in type.GetFields())
                    {
                        Console.WriteLine("--[FIELD] " + fieldInfo.Name);
                    }

                    // 클래스에 정의된 메서드를 열거
                    foreach (MethodInfo methodInfo in type.GetMethods())
                    {
                        Console.WriteLine("--[METHOD]" + methodInfo.Name);
                    }

                    // 클래스에 정의된 프로퍼티를 열거
                    foreach (PropertyInfo propertyInfo in type.GetProperties())
                    {
                        Console.WriteLine("--[PROPERTY] " + propertyInfo.Name);
                    }
                }


- AppDomain 내에 어셈블리를 로드하는 간단한 방법은, CreateInstanceFrom 메서드를 이용해 어셈블리 파일의 경로와 최초 생성될 객체 타입을 정의해야 한다. (네임스페이스를 포함하여 FQDN을 알아야 한다.


- Default AppDomain에 로드된 어셈블리는 해제할 수 없다.


- 각 AppDomain들은 격리되어있다. static으로 만들고 여러 개의 AppDomain에서 동일한 클래스를 로드했더라도, AppDomain 마다 하나씩 존재하게 된다.


using System;
using System.Reflection;
using System.Runtime.Remoting;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            AppDomain newAppDomain = AppDomain.CreateDomain("MyAppDomain");

            string dllPath = @"C:\Users\lee30\documents\visual studio 2017\Projects\ConsoleApp1\LogWriter\bin\Debug\LogWriter.dll";

            ObjectHandle objHandle = newAppDomain.CreateInstanceFrom(dllPath, "ClassLibrary1.Class1");

            Console.WriteLine("엔터키를 치기 전까지 ClassLibrary1.dll 파일을 지울 수 없습니다.");
            Console.ReadLine();

            AppDomain.Unload(newAppDomain); // AppDomain을 해제해야 속한 어셈블리들이 모두 해제
        }
    }
}


- 리플렉션으로 메타데이터를 조회 가능한 것 뿐만 아니라, 타입을 생성할 수도 있고, 그 타입에 정의된 메서드를 호출할 수 있으며, 필드/프로퍼티의 값을 바꾸는 것도 가능하다.


using System;
using System.Reflection;
using System.Runtime.Remoting;

namespace ConsoleApp1
{
    public class SystemInfo
    {
        bool _is64bit;

        public SystemInfo()
        {
            _is64bit = Environment.Is64BitOperatingSystem;
            Console.WriteLine("SystemInfo Created.");
        }

        public void WriteInfo()
        {
            Console.WriteLine("OS == {0}bits", (_is64bit == true) ? "64" : "32");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            SystemInfo sysInfo = new SystemInfo();
            sysInfo.WriteInfo();

            // 리플렉션을 이용해 동일하게 수행
            Type systemInfoType = Type.GetType("ConsoleApp1.SystemInfo");
            // type 정보로 해당 객체를 생성할 수 있다.
            object objInstance = Activator.CreateInstance(systemInfoType);

            // 생성자를 리플렉션으로 구해서 호출
            ConstructorInfo ctorInfo = systemInfoType.GetConstructor(Type.EmptyTypes); // 기본 생성자
            object objInstance2 = ctorInfo.Invoke(null); // 인자 없음

            // 메서드를 리플렉션으로 구해서 호출
            MethodInfo methodInfo = systemInfoType.GetMethod("WriteInfo");
            methodInfo.Invoke(objInstance, null); // (호출될 객체의 인스턴스, 인자)

            // private 접근( 접근자가 private인(NonPublic), 인스턴스 멤버(Instance) )
            FieldInfo fieldInfo = systemInfoType.GetField("_is64bit", BindingFlags.NonPublic | BindingFlags.Instance);
            // 기존값을 구해서
            object oldValue = fieldInfo.GetValue(objInstance);
            // 새로운 값을 쓴다.
            fieldInfo.SetValue(objInstance, !Environment.Is64BitOperatingSystem);
            // 확인한다.
            methodInfo.Invoke(objInstance, null);
        }
    }
}


- 별도의 DLL파일을 로드할 때는 다음과 같이 하면된다. DLL파일이 EXE파일과 같은 폴더에 있다면 DLL파일 이름만 넣어줘도 된다.


using System;
using System.Reflection;
using System.Runtime.Remoting;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            string dllPath = @"C:\Users\lee30\documents\visual studio 2017\Projects\ConsoleApp1\LogWriter\bin\Debug\LogWriter.dll";

            Assembly asm = Assembly.LoadFrom(dllPath);

            // 리플렉션을 이용해 타입을 구한다.
            Type systemInfoType = asm.GetType("ClassLibary1.SystemInfo");

            // 생성자를 리플렉션으로 구해서 호출
            ConstructorInfo ctorInfo = systemInfoType.GetConstructor(Type.EmptyTypes); // 기본 생성자
            object objInstance = ctorInfo.Invoke(null); // 인자 없음

            // 메서드를 리플렉션으로 구해서 호출
            MethodInfo methodInfo = systemInfoType.GetMethod("WriteInfo");
            methodInfo.Invoke(objInstance, null); // (호출될 객체의 인스턴스, 인자)

            // private 접근( 접근자가 private인(NonPublic), 인스턴스 멤버(Instance) )
            FieldInfo fieldInfo = systemInfoType.GetField("_is64bit", BindingFlags.NonPublic | BindingFlags.Instance);
            // 기존값을 구해서
            object oldValue = fieldInfo.GetValue(objInstance);
            // 새로운 값을 쓴다.
            fieldInfo.SetValue(objInstance, !Environment.Is64BitOperatingSystem);
            // 확인한다.
            methodInfo.Invoke(objInstance, null);
        }
    }
}


[ 리플렉션을 이용한 확장 모듈 구현 ]


- 보통 플러그인을 구현한 소프트웨어의 동작 방식은 아래와 같다.


1. EXE 프로그램이 실행되는 경로 아래에 확장 모듈을 담도록 약속된 폴더가 있는지 확인한다.


2. 해당 폴더가 있다면, 그 안에 DLL파일이 있는지 검사하고 로드한다.


3. DLL이 로드됐으면 사전에 약속된 조건을 만족하는 타입이 있는지 확인한다.


4. 조건에 부합하는 타입이 있으면 생성하고, 역시 사전에 약속된 메서드를 실행한다.



[ 플러그인 기능을 제공하는 응용 프로그램 개발자가 구현 ]


using System;
using System.IO;
using System.Reflection;
using System.Runtime.Remoting;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            string pluginPath = @"C:\Users\lee30\documents\visual studio 2017\Projects\ConsoleApp1\LogWriter\bin\Debug";
            
            if(Directory.Exists(pluginPath))
            {
                ProcessPlugin(pluginPath);
            }
            else { Console.WriteLine("디렉토리가 존재하지 않음"); }
        }

        private static void ProcessPlugin(string rootPath)
        {
            foreach(string dllPath in Directory.EnumerateFiles(rootPath, "*.dll"))
            {
                // 확장 모듈을 현재의 AppDomain에 로드
                Assembly pluginDll = Assembly.LoadFrom(dllPath);

                Type entryType = FindEntryType(pluginDll);
                if (entryType == null) continue;

                object instance = Activator.CreateInstance(entryType);

                MethodInfo entryMethod = FindStartupMethod(entryType);
                if (entryMethod == null) continue;

                entryMethod.Invoke(instance, null);
            }
        }

        // DLL에 포함된 모든 타입을 열거한다. 확장 모듈 개발자가 구현한 클래스 중에서
        // 어떤 타입이 진입점에 해당하느냐를 알려주기 위해 PluginAtttribute 이름의 특성이
        // 적용되어 있어야 한다고 가정하자.
        private static Type FindEntryType(Assembly pluginDll)
        {
            foreach(Type type in pluginDll.GetTypes())
            {
                foreach(object objAttr in type.GetCustomAttributes(false))
                {
                    if(objAttr.GetType().Name == "PlugInAttribute")
                    {
                        return type;
                    }
                }
            }

            return null;
        }

        // 클래스에 포함된 method 중에 StartupAttribute가 지정된 메서드를 결정
        private static MethodInfo FindStartupMethod(Type entryType)
        {
            foreach(MethodInfo methodInfo in entryType.GetMethods())
            {
                foreach(object objectAttr in methodInfo.GetCustomAttributes(false))
                {
                    if(objectAttr.GetType().Name == "StartupAttribute")
                    {
                        return methodInfo;
                    }
                }
            }

            return null;
        }
    }
}


[ 확장 모듈 제작자가 구현 ]

using System;

namespace ClassLibary1
{
    [PlugInAttribute]
    public class SystemInfo
    {
        bool _is64bit;

        public SystemInfo()
        {
            _is64bit = Environment.Is64BitOperatingSystem;
            Console.WriteLine("SystemInfo Created.");
        }
        
        [StartupAttribute]
        public void WriteInfo()
        {
            Console.WriteLine("OS == {0}bits", (_is64bit == true) ? "64" : "32");
        }
    }

    public class PlugInAttribute : Attribute
    {
    }

    public class StartupAttribute : Attribute
    {
    }
}

- 출처 : 시작하세요! C# 7.1 프로그래밍 (위키북스, 정성태 저)