[ 리플렉션 ]
- 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 |