상세 컨텐츠

본문 제목

[C#] 리플렉션

.NET/C#

by 클리엘 클리엘 2021. 4. 19. 12:44

본문

728x90

1. 형식 가져오기

 

객체의 메서드나 필드, 프로퍼티 등 객체에 관한 모든 정보를 들여다보는 것을 리플렉션이라고 하며 이 리플렉션은 GetType() 메서드를 통해 이루어집니다.

 

.NET 세계에서 모든 객체의 조상은 Object인데 이 Object는 GetType()라는 메서드를 구현하고 있고, 따라서 Object를 상속받는 .NET의 모든 객체는 이 GetType() 메서드를 통해 자신의 형식 정보를 반환할 수 있습니다.

Type type = Type.GetType("System.Int32");

MemberInfo[] member = type.GetMembers();

foreach(MemberInfo mi in member) {
	Console.WriteLine($"{mi.Name}");
}

만약 형식이 아닌 객체의 인스턴스를 대상으로 해야 한다면 typeof 연산자나 객체의 GetType() 메서드를 사용하면 됩니다.

int i = 10;
Type type = i.GetType();

GetType()을 통해 반환된 Type형식을 통해서 GetMembers() 메서드를 호출하여 int형식의 Member목록 이름을 확인하고 있습니다.

 

또한 메서드를 호출할 때 System.Reflection.BindingFlags 열거형을 사용하면 필드 검색 시 특정한 필드만을 걸러서 표시할 수도 있습니다. 아래 예제는 멤버 목록 중에서 public이며 정적인 것만을 불러오도록 한 것입니다.

Type type = Type.GetType("System.Int32");

MemberInfo[] member = type.GetMembers(BindingFlags.Public | BindingFlags.Static);

foreach(MemberInfo mi in member) {
	Console.WriteLine($"{mi.Name}");
}

참고로 Type에서 형식 정보를 확인하는 데 사용 가능한 메서드의 목록을 확인해 보려면

 

Type 클래스 (System) | Microsoft Docs

 

Type 클래스 (System)

클래스 형식, 인터페이스 형식, 배열 형식, 값 형식, 열거형 형식, 형식 매개 변수, 제네릭 형식 정의 및 개방형 생성 제네릭 형식이나 폐쇄형 생성 제네릭 형식에 대한 형식 선언을 나타냅니다.Re

docs.microsoft.com

위 페이지에서 '메서드'부분을 살펴보시면 됩니다.

 

이렇게 리플렉션을 활용하면 단순히 메서드나 필드 목록을 불러오는 것뿐만이 아니라 아예 객체의 인스턴스를 생성하고 생성된 인스턴스의 메서드를 호출하거나 속성에 값을 확인하고 설정할 수도 있습니다. 일반적으로 생성된 인스턴스와 방법만 다를 뿐 실제 사용되는 개념은 거의 같습니다.

 

예를 들어보기 위해 다음과 같은 클래스를 작성하고

class Student
{
	public string Name { get; set; } = string.Empty;
	public int Grade { get; set; } = 0;

	public void GetInfo()
	{
		Console.WriteLine($"{Grade}학년 {Name] 학생");
	}
}

리플렉션을 통해 Student의 인스턴스를 생성해 보도록 하겠습니다.

Type type = typeof(Student);
Student student = (Student)Activator.CreateInstance(type);

Activator클래스의 CreateInstance메서드는 전달받은 매개변수 형식의 인스턴스를 생성해 반환하는 메서드입니다. 참고로 CreateInstance는 일반화를 지원하므로 CreateInstance<List<string>>()와 같은 형태로도 구현이 가능합니다.

 

이러한 같은 방법으로 인스턴스를 받고 나면 클래스의 인스턴스를 생성할 때와 동일한 방법으로 속성이나 메서드에 접근할 수 있습니다.

 

그런데 형 변환을 통하지 않으면 객체를 object형으로 받아야 하는데

Type type = typeof(Student);
object student = Activator.CreateInstance(type);

이런 경우에도 Type의 GetProperty메서드를 통해 속성에 접근할 수 있으며

PropertyInfo name = type.GetProperty("Name");
name.SetValue(student, "홍길동");
PropertyInfo grade = type.GetProperty("Grade");
grade.SetValue(student, 5);

//혹은
name.GetValue(stdent) //값을 가져올때

GetMethod로 객체의 메서드를 다룰 수 있습니다. 참고로 메서드를 호출하려면 Invoke메서드를 사용해야 합니다.

MethodInfo method = type.GetMethod("GetInfo");
method.Invoke(student, null);

2. 형식 내보내기

 

리플렉션은 이처럼 특정 형식의 메서드나 프로퍼티 등의 정보를 확인하고 직접 값을 할당하며 메서드를 호출할 수 있지만 프로그램 실행 중 동적으로 새로운 형식을 생성해 낼 수도 있습니다.

 

새로운 형식을 만들어 내기 위해서는 우선 다음과 같이 AssemblyBuilder를 통해 새로운 어셈블리부터 생성합니다.

AssemblyBuilder myAssembly = AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName("CalculatorAssembly"), AssemblyBuilderAccess.Run);

참고로 AssemblyBuilder클래스는 System.Reflection.Emit 네임스페이스에 속해 있으므로 해당 네임스페이스를 선언해야 합니다.

 

AssemblyBuilder클래스는 생성자가 없으며 실행코드를 메모리에 올려주는 AppDomain을 이용해야 합니다.  AppDomain에서 CurrentDomain은 현재 실행 중인 AppDomain을 반환하고 여기에 생성할 어셈블리 이름(예제에서는 CalculatorAssembly)으로 DefineDynamicAssembly() 메서드를 사용하면 AssemblyBuilder객체의 인스턴스를 생성할 수 있습니다.

 

위와 같이 Assembly를 생성하고 나면 다음으로는 Assembly내부에 들어갈 Module를 생성해야 합니다.

ModuleBuilder myModule = myAssembly.DefineDynamicModule("CalculatorModule");

CalculatorModule이라는 이름으로 위에서 생성한 Assembly안에 Module을 생성하였습니다. 가만히 보면 필요한 요소에 따라 ~Builder라는 클래스를 사용하고 새로운 형식을 만들어 내는데 DefineDynamic~ 형태의 메서드를 사용하고 있습니다. 거의 비슷한 방법으로 진행이 되고 있죠.

 

Module을 생성하고 나면 이제 클래스를 만들어 넣어야 합니다.

TypeBuilder myType = myModule.DefineType("CalculatorClass");

Class는 TypeBuilder클래스를 사용하며 예제에서는 Module내부에 CalculatorClass이름으로 Class를 생성하였습니다.

 

Class를 만들었으면 이제 메서드를 생성해 보도록 하겠습니다.

MethodBuilder myMethod = myType.DefineMethod("mySum", MethodAttributes.Public, typeof(int), new Type[] { typeof(int), typeof(int) });

메서드는 2개의 int형 매개변수를 받아 합한 결과를 반환하는 것을 목적으로 하고 있습니다. 이를 위해 Public접근성을 가지며 2개의 int형 매개변수를 받는 mySum이라는 메서드를 생성하고자 합니다. 만약 반환 값이 없거나 매개변수가 필요하지 않은 상황이라면 null로 처리할 수 있습니다.

 

메서드까지 만들었으면 메서드 안에 동작할 코드를 만들어 넣어야 합니다. 코드는 약간 어려울 수 있는데 IL 명령어를 직접 작성해야 합니다.

ILGenerator generator = myMethod.GetILGenerator();

generator.Emit(OpCodes.Ldarg_1);    
generator.Emit(OpCodes.Ldarg_2);
generator.Emit(OpCodes.Add);
generator.Emit(OpCodes.Ret);

IL코드는 MethodBuilder로 생성된 인스턴스의 GetILGenerator() 메서드를 사용해 ILGenerator객체를 생성하는 것으로 시작하며 Emit을 통해 실제 형식 코드를 만들고 있습니다.

 

첫 번째와 두 번째의 Ldarg_1과 Ldarg_2는 첫번째 매개 변숫값과 두번째 매개 변숫값을 의미하며 이 값을 Stack에 담아두는 동작을 수행합니다. 그리고 세 번째 Add를 통해 스택에 있는 값을 더하게 되고 마지막 Ret에 의해 계산 결과를 반환하는 것입니다.

 

여기까지 원하는 형식을 모두 완성하였습니다. 이제 TypeBuilder객체의 CreateType() 메서드를 호출해 생성한 클래스를 CLR에 넘겨줍니다.

myType.CreateType();

새로운 형식을 만드는 모든 과정이 끝났습니다. 남은 건 새롭게 만든 형식의 인스턴스를 동적으로 생성해 해당 형식에서 만들어진 메서드를 Invoke로 호출하는 것입니다.

object CalculatorClass = Activator.CreateInstance(myType);
MethodInfo mySum = CalculatorClass.GetType().GetMethod("mySum");

Console.WriteLine(mySum.Invoke(CalculatorClass, new object[]{ 10, 20 }));


3. 어트리뷰트(Attribute)

 

(1) 개요

 

어트리뷰트는 클래스나 구조체, 메서드, 프로퍼티 등에 C#컴파일러가 읽을 수 있도록 특정 정보를 기록해 두는 것을 말합니다.

 

예를 들어 아래와 같은 클래스가 존재할 때

class Student
{
	public string Name { get; set; } = string.Empty;
	public int Grade { get; set; } = 0;

	public void GetInfo()
	{
		Console.WriteLine($"{Grade}학년 {Name} 학생");
	}
}

위 클래스를 사용하는 프로그램에서 GetInfo() 메서드를 호출할 때 더 이상 사용되지 않음을 공지하려면 어트리뷰트를 아래와 같이 설정해 줄 수 있습니다.

[Obsolete("정보 확인용 메서드 - 사용하지 않음")]
public void GetInfo()
{
	Console.WriteLine($"{Grade}학년 {Name} 학생");
}

또한 C/C++로 만들어진 dll을 참조하기 위한 목적으로 DllImport를 사용하기도 하는데 이것 또한 어트리뷰트에 해당합니다.

[DllImport("user32.dll")]
public static extern int ReleaseCapture();

어트리뷰트는 사용목적에 따라 다양한 형식이 존재하며 아래 페이지를 참고하시기 바랍니다.

 

Attribute 클래스 (System) | Microsoft Docs

 

Attribute 클래스 (System)

사용자 지정 특성에 대한 기본 클래스를 나타냅니다.Represents the base class for custom attributes.

docs.microsoft.com

 

(2) 호출자 정보 어트리뷰트

 

프로그램 내에서 특정 메서드가 호출될 때 호출자의 소스 파일명(CallerFilePath), 메서드를 호출한 메서드 혹은 프로퍼티 이름(CallerMemberNameAttribute), 호출 코드가 실행되는 라인 번호(CallerLineNumberAttribute)등을 확인할 수 있습니다. 이를 위해서 특정 메서드에 아래와 같은 3개의 매개변수를 어트리뷰트를 사용해 지정해 주기만 하면 됩니다.

using System;
using System.Runtime.CompilerServices;

namespace MyCS
{

	class HelloWorld
	{
		static void Main(string[] args)
		{
			Student student = new Student();
			student.Name = "홍길동";
			student.Grade = 6;
			student.GetInfo();
		}
	}


	class Student
	{
		public string Name { get; set; } = string.Empty;
		public int Grade { get; set; } = 0;

		public void GetInfo([CallerFilePath] string filePath = "", [CallerMemberName] string memberName = "", [CallerLineNumber] int lineNumber = 0)
		{
			Console.WriteLine($"{Grade}학년 {Name} 학생");
			Console.WriteLine($"호출자 정보 : {filePath} - {memberName} - {lineNumber}");
		}
	}
}

(3) 사용자 지정 어트리뷰트

 

어트리뷰트는 직접 구현할 수도 있습니다. 어트리뷰트는 하나의 클래스이며 필요한 어트리뷰트의 클래스는 System.Attribute를 상속받아 구현됩니다.

class myWarningAttribute : System.Attribute
{
	public string WarningName
	{
		get;
		set;
	}

	public string WarningDate
	{
		get;
		set;
	}
}

일단 System.Attribute를 상속받은 클래스를 생성하는 것만으로 새로운 어트리뷰트가 만들어집니다. 그리고 새로 만들 어트리뷰트의 목적에 따라 다른 일반적인 클래스처럼 메서드나 속성 등을 선언해 어트리뷰트를 구현하면 됩니다.

[myWarningAttribute(WarningName = "경고1", WarningDate = "2020-12-12")]
class Student
{
	public string Name { get; set; } = string.Empty;
	public int Grade { get; set; } = 0;

	public void GetInfo()
	{
		Console.WriteLine($"{Grade}학년 {Name} 학생");

		Type type = typeof(Student);
		Attribute[] attribute = Attribute.GetCustomAttributes(type);

		foreach(Attribute attr in attribute)
		{
			myWarningAttribute myWarning = attr as myWarningAttribute;

			Console.Write($"{myWarning.WarningName} - {myWarning.WarningDate}");
		}
	}
}

예제에서는 WarningName과 WarningDate2개의 속성을 선언하였으며 해당 어트리뷰트를 Student클래스에 적용하고 있습니다. 어트리뷰트에 사용된 특성은 Attribute의 GetCustomAttribute() 메서드를 통해 확인할 수 있습니다.

 

그런데 만약 어트리뷰트를 아래와 같이 작성해 놓고

[myWarningAttribute(WarningName = "경고1", WarningDate = "2020-12-12")]

또 하나의 어트리뷰트를 작성하려고 하면

[myWarningAttribute(WarningName = "경고1", WarningDate = "2020-12-12")]
[myWarningAttribute(WarningName = "경고2", WarningDate = "2020-12-20")]

오류가 발생할 것입니다. 위와 같은 구현을 가능하게 하려면 System.AttributeUsage를 활용해야 합니다.

[System.AttributeUsage(System.AttributeTargets.Class, AllowMultiple=true)]
class myWarningAttribute : System.Attribute

System.AttributeTargets열거형에서 Class를 지정하고 있는데 이는 지금 적용하고자 하는 어트리뷰트의 대상을 지정하는 것입니다. 이 대상으로 지정할 수 있는 나머지 내용은 아래 페이지에서 확인하시면 되며 만약 적용대상이 둘 이상이라면 | 연산자로 결합해 지정해야 합니다.

 

AttributeTargets 열거형 (System) | Microsoft Docs

 

AttributeTargets 열거형 (System)

특성을 적용하는 데 유효한 애플리케이션 요소를 지정합니다.Specifies the application elements on which it is valid to apply an attribute.

docs.microsoft.com

그리고 두 번째 AllowMultiple속성에서는 값을 true로 지정하고 있는데 이 속성이 바로 어트리뷰트를 여러 번 작성할 수 있도록 해주는 속성에 해당합니다.

[myWarningAttribute(WarningName = "경고1", WarningDate = "2020-12-12")]
[myWarningAttribute(WarningName = "경고2", WarningDate = "2020-12-20")]
class Student
{
	public string Name { get; set; } = string.Empty;
	public int Grade { get; set; } = 0;

	public void GetInfo()
	{
		Console.WriteLine($"{Grade}학년 {Name} 학생");

		Type type = typeof(Student);
		Attribute[] attribute = Attribute.GetCustomAttributes(type);

		foreach(Attribute attr in attribute)
		{
			myWarningAttribute myWarning = attr as myWarningAttribute;

			Console.Write($"{myWarning.WarningName} - {myWarning.WarningDate}");
		}
	}
}


[System.AttributeUsage(System.AttributeTargets.Class, AllowMultiple=true)]
class myWarningAttribute : System.Attribute
{
	public string WarningName
	{
		get;
		set;
	}

	public string WarningDate
	{
		get;
		set;
	}
}

 

728x90

'.NET > C#' 카테고리의 다른 글

[C#] dynamic  (0) 2021.04.30
[C#] LINQ  (0) 2021.04.22
[C#] 리플렉션  (0) 2021.04.19
[C#] 대리자(Delegate)  (0) 2021.04.15
[C#] 이벤트(Event)  (0) 2021.04.12
[C#] 람다식  (0) 2021.04.06

관련글 더보기

댓글 영역