[ 리플렉션 ]
- 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 프로그래밍 (위키북스, 정성태 저)
'Programming > C#' 카테고리의 다른 글
C# 2.0 변경점(2) - yield return/break, partial class, Nullable, 익명 메서드, 정적 클래스 (0) | 2019.03.31 |
---|---|
C# 2.0 변경점(1) - 제네릭스(Generics), ?? 연산자 (0) | 2019.03.31 |
MSSQL Database 연동(2) (0) | 2019.03.11 |
MSSQL Database 연동(1) (0) | 2019.03.05 |
app.config (0) | 2019.03.05 |