본문 바로가기

C#/이것이 C#이다

[ 이것이 C#이다 ] Chapter 16 . 리플렉션과 애트리뷰트

 [ 학습 흐름 ]
  1. 리플렉션
  2. Object.GetType()메소드와 Type클래스
  3. 리플렉션을 이용하여 객체를 생성하는 방법을 이해한다 .
  4. 애트리뷰트가 무엇인지 이해한다 .
  5. 애트리뷰트를 작성하고 사용하는 방법을 익힌다 .

[ 리플렉션 ]

[ 리플렉션 ]

- 객체를 X_RAY사진처럼 객체의 형식정보를 들여다보는 기능 .

- 프로그램 실행중 객체의 형식이름/프로퍼티 목록/메소드 목록/필드/이벤트 목록까지 열어 볼 수 있다 .

- 형식의 이름만 있다면 동적으로 인스턴스를 만들 수 있고 , 인스턴스의 메소드를 호출 가능하다 .

- 새로운 데이터 형식을 동적으로 만들 수 있다 .

- 런타임중 형식정보를 다룰 수 있는 리플렉션은 강력한 표현력을 선사한다 .


[ Object.GetType()메소드와 Type 클래스 ]

- GetType () 메소드는 Type 형식의 결과를 반환한다 .

- Type 형식은 .Net에서 사용하는 데이터 형식의 모든 정보를 담고 있다 .

int a = 0;

Type type=a.GetType();
FieldInfo[]fields = type.GetFields();

foreach(FieldInfo field in fields)
	Console.WriteLine("Type : {0},Name : {1}",field.FieldType.Name,field.Name);

 

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

 

Type type = a.GetType();
//public 인스턴스 필드 조회
var fields1 = type.GetFields(BindingFlags.Public | BindingFlags.Instance );
//비public 인스턴스 필드 조회
var field2 = type.GetFields(BindingFlags.NonPublic | BindingFlags.Instace );
//public 정적 필드 조회
var fields3 = type.GetFields(BindingFlags.Public | BindingFlags.Static );
//비public 정적 필드 조회
var field4 = type.GetFields(BindingFlags.NonPublic | BindingFlags.Static );

- GetFields() / GetMethods()는 검색옵션의 지정이 가능하다 .

- System.Reflection/BindingFlags 열거형을 이용해서 구성된다 .

- GetFields() / GetMethods()등의 메소드는 BindingFlags매개변수를 받지 않는다면 public 멤버만 반환한다 .

using System;
using System.Linq;
using System.Reflection;

namespace ConsoleApp;

class MainApp
{   
        static void PrintInterfaces(Type type)
        {
            Console.WriteLine("-----Interfaces-----");
            Type[]interfaces = type.GetInterfaces();
            foreach (Type interfaceType in interfaces)
            {
                Console.WriteLine("Name{0}",interfaceType.Name);
            }
            Console.WriteLine();
        }
        static void PrintFields(Type type)
        {
            Console.WriteLine("-----Fields-----");
            FieldInfo[] info = type.GetFields(BindingFlags.Public|BindingFlags.NonPublic|BindingFlags.Static|BindingFlags.Instance);
            foreach (FieldInfo field in info)
            {
                string accessLevel = "protected";
                if (field.IsPublic) accessLevel = "public";
                else if (field.IsPrivate) accessLevel = "private";

                Console.WriteLine("Access : {0} , Name : {1} , Type : {2}",accessLevel,field.Name,field.FieldType.Name);
            }
            Console.WriteLine();
        }
        static void PrintMethod(Type type)
        {
            Console.WriteLine("-----Methods-----");
            MethodInfo[] methos = type.GetMethods();
            foreach(MethodInfo method in methos)
            {
                Console.Write("Type : {0} , Name : {1} , Parameter : ", method.ReturnType.Name, method.Name);
                ParameterInfo[]args = method.GetParameters();
                for(int i=0;i<args.Length; i++)
                {
                    Console.Write("{0}", args[i].ParameterType.Name);
                    if(i<args.Length-1) Console.Write(", ");
                }
                Console.WriteLine();
            }
            Console.WriteLine();
        }
        static void PrintProperties(Type type)
        {
            Console.WriteLine("----Properties-----");
            PropertyInfo[] props = type.GetProperties();
            foreach(PropertyInfo property in props)
            {
                Console.WriteLine("Type:{0},Name:{1}",property.PropertyType.Name,property.Name);
            }
            Console.WriteLine();

        }
    static void Main(string[] args)
    {
        int a = 0;
        Type type=a.GetType();

        PrintInterfaces(type);
        PrintFields(type);
        PrintProperties(type);
        PrintMethod(type);
    }
}

 

- Object.GetType 연산자 이외에도 typeof 연산자와 Type.GetType()메소드가 제공된다 .

//typeof 연산자는 형식의 식별자 자체를 인수로 받는다
Type a = typeof(int);

//Type.GetType() 메소드의 인수는 형식의 네임스페이스를 포함하는 전체이름
Type b = GetType("System.Int32");

[ 리플렉션을 이용해서 객체 생성하기 ]

- Systme.Activator 클래스의 도움으로 리플렉션을 이용해서 동적으로 인스턴스를 만들 수 있다 .

- 또한 프로퍼티에 값을 할당하는 것도 동적으로 가능하다 .

- 메소드 역시 가능한데 , Invoe()메소드를 통해 동적으로 메소드 호출이 가능하다 .

using System;
using System.Linq;
using System.Reflection;

namespace ConsoleApp;

class MainApp
{ 
    class Profile
    {
        string name;
        string phone;
        public Profile(string name,string phone)
        {
            this.name = name;
            this.phone = phone;
        }
        public void Print()
        {
            Console.WriteLine("Name :{0} , Phone :{1}",name,phone);
        }
        public string Name
        {
            get { return name; }
            set { name = value; }
        }
        public string Phone
        { 
            get { return phone; } 
            set {  phone = value; } 
        }
    }
    static void Main(string[] args)
    {
        Type type = Type.GetType("ConsoleApp.Profile");
        MethodInfo methodInfo = type.GetMethod("Print");
        PropertyInfo nameProperty= type.GetProperty("Name");
        PropertyInfo phoneProperty = type.GetProperty("Phone");

        //두번째 매개변수는 호출 메서드의 인수
        object profile = Activator.CreateInstance(type,"박상현","512 - 1234");
        methodInfo.Invoke(profile, null);
        //두번째 매개변수는 인덱서를 위한 자리
        profile = Activator.CreateInstance(type);
        nameProperty.SetValue(profile, "박찬호", null);
        phoneProperty.SetValue(profile, "123", null);

        Console.WriteLine($"{nameProperty.GetValue(profile, null)}");
        Console.WriteLine($"{phoneProperty.GetValue(profile, null)}");
    }
}

[ 형식 내보내기 ]

- 프로그램 실행 중에 새로운 형식을 만들어낼 수 있는 기능을 제공한다 .

- System.Reflectioin.Emit(내보낸다) 네임스페이스의 클래스들을 통해 이루어진다 .

- 즉 , 프로그램이 실행 중에 만들어낸 새 형식을 CLR에 내보낸다는 뜻 .

- 다음 표는 Emit 네임스페이스에서 제공하는 클래스의 목록이다 .

클래스 설명
AssemblyBuilder 동적 어셈블리를 정의하고 나타냄
ConstructorBuilder 동적으로 만든 클래스의 생성자를 정의하고 나타냄
CustomAttributeBuilder 사용자 정의 애트리뷰트를 만든다 
EnumBuilder 열거 형식을 정의하고 나타냄
EventBuilder 클래스의 이벤트를 정의하고 나타냄
FieldBuilder 필드를 정의하고 나타냄
GenericTypeParameterBuilder 동적으로 정의된 형식(클래스)/메소드를 위한 일반화 형식 매개변수를 정의하고 생성
ILGenerator MSIL(Microsoft Intermediate Language)명령어를 생성한다 .
LocalBuilder 메소드나 생성자 내의 지역변수를 나타낸다 .
MethodBuilder 동적으로 만든 클래스의 메소드(또는 생성자)를 정의하고 나타낸다 .
ModuleBuilder 동적 어셈블리 내의 모듈을 정의하고 나타낸다 .
OpCodes ILGenerator 클래스의 멤버를 이용한 내보내기 작업에 사용할 MSIL 명령어의 필드 표현을 제공한다 .
ParametreBuilder 매개변수 정보를 생성하거나 결합한다 
PropertyBuilder 형식(클래스)의 프로퍼티를 정의한다 .
TypeBuilder 실행 중에 클래스를 정의하고 생성한다 .

- 해당 클래스의 사용 요령은 다음과 같다 .

  1. AssemblyBuilder를 이용해서 어셈블리를 만든다 .
  2. ModuleBuider를 이용해서 생성한 어셈블리 안에 모듈을 만들어 넣는다 .
  3. 모듈 안에 TypeBuilder로 클래스 (형식)을 만들어 넣는다 
  4. 클래스안에 MethodBuilder / PropertyBuilder로 메소드나 프로퍼티를 만들어 넣는다
  5. 메소드를 생성했다면 ILGenerator를 이용 ,메소드 안에 CPU가 실행할 IL 명령어를 넣는다 .
using System;
using System.Reflection;
using System.Reflection.Emit    ;


namespace ConsoleApp;

class MainApp
{ 
    static void Main(string[] args)
    {
        //AssemblyBuilder는 생성자가 없어 팩토리 클래스의 도움이 필요 .DefineDynamicAssembly를 통해 AssemblyBuilder 인스턴스 생성
        AssemblyBuilder newAssembly = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("CalculateAssembly"),AssemblyBuilderAccess.Run);

        //모듈 생성
        ModuleBuilder newModule = newAssembly.DefineDynamicModule("Calculator");

        //클래스 생성
        TypeBuilder newType = newModule.DefineType("Sum1To100");

        //메소드 생성
        MethodBuilder newMehtod=newType.DefineMethod("Calculate",MethodAttributes.Public,typeof(int),new Type[0]);

        //IL 명령어 (실행할 코드)생성
        ILGenerator generator = newMehtod.GetILGenerator();
        generator.Emit(OpCodes.Ldc_I4,1);//32 비트 정수 1을 계산 스텍에
        for(int i = 2;i<=100;i++)
        {
            generator.Emit(OpCodes.Ldc_I4, i);//32 비트 정수 i 를 계산 스택에
            generator.Emit(OpCodes.Add);//두개의 값을 꺼내 더한후 그 결과를 다시 계산 스택에
        }
        generator.Emit(OpCodes.Ret);//계산 스택에 담겨 있는 값 반환
        newType.CreateType();//CLR에 클래스 제출

        object sum1To100 = Activator.CreateInstance(newType);
        MethodInfo Calculate = sum1To100.GetType().GetMethod("Calculate");
        Console.WriteLine(Calculate.Invoke(sum1To100, null));
    }
}

[ 애트리뷰트 ]

[ 애트리뷰트 ]

- 코드에 대한 부가 정보를 기록하고 읽을 수 있는 기능

- 주석은 사람이 읽고 쓰는 정보라면 애트리뷰트는 사람이 쓰고 컴퓨터가 읽는 것 


[메타 데이터 ]

- 메타데이터란 데이터의 데이터 .

- C#코드도 데이터지만 해당 코드에 대한 정보도 존재하는데 , 이를 메타데이터라 한다 .

- 애트리뷰트 / 리플렉션을 통해 얻는 정보들도 C# 코드의 메타 데이터 .


[ 애트리뷰트 사용하기 ]

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


namespace ConsoleApp;
class MyClass
{
    [Obsolete("OldMethod는 폐기되었습니다 . NewMethod()를 사용해주세요")]
    public void OldMethod()
    {
        Console.WriteLine("Old Method");
    }
    public void NewMethod()
    {
        Console.WriteLine("New Method");
    }
}

class MainApp
{ 
    static void Main(string[] args)
    {
       MyClass myClass = new MyClass();
        myClass.OldMethod();
        myClass.NewMethod();
    }
}

 

- 정상적으로 실행하지만 컴파일시 경고목록에 다음과 같이 경고메시지를 띄운다 .


[ 호출자 정보 애트리뷰트 ]

- C,C++에서 사용하는 __FILENAME(소스파일 이름) / __LINE(행 번호) / __FUNCTION(함수이름) 매크로의 기능을 

C#은 제공하지 않는다 .

- 위 매크로는 컴파일러에 의해 치환되어 코드에 들어간다 .

- C# 5.0부터는 호출자 정보 애트리뷰트로 위의 기능을 대신한다 .

- 호출자 정보는 메소드의 매개변수에 사용되며 , 메소드 호출자 이름/호출자 메서드 정의 소스파일 경로/소스파일내 번호

등을 알 수 있다 .

- 응용 프로그램의 이벤트를 로그 파일이나 화면에 출력하여 해당이벤트가 어느 코드에서 발생하는지 알 수 있다 .

애트리뷰트 설명
CallerMemberNameAttribute 현재 메소드를 호출한 메소드 또는 프로퍼티의 이름을 나타낸다 .
CallerFilePathAttribute 현재 메소드가 호출된 소스 파일 경로 . 이때의 경로는 소스코드를 컴파일할 때의 전체경로 
CallerLineNumberAttribute 현재 메소드가 호출된 소스 파일 내의 행 번호를 나타낸다 .

 

using System;
using System.Reflection;
using System.Reflection.Emit    ;
using System.Runtime.CompilerServices;


namespace ConsoleApp;
public static class Trace
{
    public static void WriteLine(string message, [CallerFilePath]string file = "", [CallerLineNumber]int line = 0, [CallerMemberName]string member="")
    {
        Console.Write("{0}(Line:{1}) {2} : {3}", file, line, member, message);
    }
}

class MainApp
{ 
    static void Main(string[] args)
    {
        Trace.WriteLine("즐거운 프로그래밍!");
    }
}

 


[ 내가 만드는 애트리뷰트 ]

using System;
using System.Reflection;
using System.Reflection.Emit    ;
using System.Runtime.CompilerServices;


namespace ConsoleApp;
//어트리뷰트의 어트리뷰트 . 클래스 대상으로 여러번 사용 가능하게 함 . Target은 논리합 연산자로 결합이 가능하다 .
[System.AttributeUsage(System.AttributeTargets.Class,AllowMultiple =true)]

class History:System.Attribute
{
    string programmer;
    public double version;
    public string changes;
    public History(string programmer)
    {
        this.programmer = programmer;
        this.version = 1.0;
        this.changes = "First Release";
    }
    public string GetProgrammer()
    {
        return programmer;
    }
}
//커스텀 어트리뷰트
[History("sean",version =0.1,changes ="2017 - 11- 01 Created class stub")]
[History("Bob", version = 0.2, changes = "2017 - 11- 15 Added Func()Method")]

class MyClass
{
    public void Func()
    {
        Console.WriteLine("Func");
    }

}
class MainApp
{ 
    static void Main(string[] args)
    {
        Type type = typeof(MyClass);
        Attribute[] attributes = Attribute.GetCustomAttributes(type);
        Console.WriteLine("MyClass Chage history ....");

        foreach (Attribute attribute in attributes)
        {
            History h = attribute as History;
            if (h != null)
                Console.WriteLine("Ver:{0},Progremmer : {1},Chages : {2}",h.version,h.GetProgrammer(),h.changes);
        }
    }
}

 

- 어트리뷰트를 직접 만들어 활용 할 수 있다 .

- 위와 같이 History 어트리뷰트와 리플렉션을 활용 , 손쉽게 Release 노트를 만들 수 있다 .