콘텐츠로 건너뛰기
Home » 리플렉션과 애트리뷰트(Reflection and attributes)

리플렉션과 애트리뷰트(Reflection and attributes)

  • 기준
리플렉션과 애트리뷰트(Reflection and attributes)

리플렉션(Reflection)

C#에서는 프로그램 실행 도중에 객체의 정보를 조사하거나, 다른 모듈에 선언된 인스턴스를 생성하거나, 기존 개체에서 형식을 가져오고 해당하는 메소드를 호출, 또는 해당 필드와 속성에 접근할 수 있는 기능을 제공하는 ‘리플렉션(Reflection)’이라는 녀석이 존재합니다. 오늘은 아래와 같은 메소드들을 알아보려고 합니다.

형식메소드설명
TypeGetType()지정된 형식의 Type 개체를 가져옵니다.
MemberInfo[]GetMembers()해당 형식의 멤버 목록을 가져옵니다.
MethodInfo[]GetMethods()해당 형식의 메소드 목록을 가져옵니다
FieldInfo[]GetFields()해당 형식의 필드 목록을 가져옵니다.

설명을 보아하니, 리플렉션(Reflection)이 어떤 역할을 하는 녀석인지 감이 오시나요? 우선, 직접 저 메소드들을 사용해가며 결과를 확인해보도록 합시다.

using System;
using System.Collections.Generic;


using System.Linq;
using System.Text;
using System.Reflection;
using System.Threading.Tasks;

namespace ConsoleApplication43
{
    class Animal
    {
        public int age;
        public string name;

        public Animal(string name, int age)
        {
            this.age = age;
            this.name = name;
        }
        public void eat()
        {
            Console.WriteLine("먹는다!");
        }
        public void sleep()
        {
            Console.WriteLine("잔다!");
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            Animal animal = new Animal("고양이", 4);
            Type type = animal.GetType();

            ConstructorInfo[] coninfo = type.GetConstructors();
            Console.Write("생성자(Constructor) : ");
            foreach (ConstructorInfo tmp in coninfo)
                Console.WriteLine("\t{0}", tmp);
            Console.WriteLine();

            MemberInfo[] meminfo = type.GetMethods();
            Console.Write("메소드(Method) : ");
            foreach (MethodInfo tmp in meminfo)
                Console.Write("\t{0}", tmp);
            Console.WriteLine();

            FieldInfo[] fieldinfo = type.GetFields();
            Console.Write("필드(Field) : ");
            foreach (FieldInfo tmp in fieldinfo)
                Console.Write("\t{0}", tmp);
            Console.WriteLine();
        }
    }
}
생성자(Constructor) :   Void .ctor(System.String, Int32)
메소드(Method) :        Void eat()      Void sleep()    System.String ToString() Boolean Equals(System.Object)   Int32 GetHashCode()     System.Type GetType()
필드(Field) :   Int32 age       System.String name
계속하려면 아무 키나 누르십시오 . . .

코드를 보시면 10행에서 Animal이란 클래스를 선언했습니다. 클래스 내에는 age와 name이란 필드와, 생성자, 그리고 eat와 sleep라는 메소드가 존재합니다. 이제 이것을 Type 형식 메소드를 사용하여, 객체 정보를 가져오는 부분입니다. 33행에서는 animal이란 객체를 만들었고, 34행에서는 animal의 타입을 가져옵니다. 그러곤 이제, 이 타입을 가지고 생성자, 메소드, 필드 등의 정보를 쉽게 가져올 수 있습니다. 36, 42, 48행에서 각각 GetConstructors, GetMethods, GetFields 메소드가 사용되었죠? 아래 foreach문을 사용해 목록을 가져오는데, 결과를 보시면 필드, 메소드, 생성자의 반환형, 매개변수의 반환형, 이름 등 다양한 정보를 제공합니다. 이 말고도 인터페이스, 이벤트 등도 가능합니다. 알고보면 리플렉션은 정말 유용한 기능이 아닐 수 없습니다.

애트리뷰트(Attribute)

애트리뷰트는 클래스에 메타데이터를 추가할수 있도록 제공되는 녀석입니다. 주석과는 달리 클래스부터 시작해서 메소드, 구조체, 생성자, 프로퍼티, 필드, 이벤트, 인터페이스 등 여러가지 요소에 애트리뷰트를 사용할 수 있습니다.  혹시, C#을 공부하시면서, [와 ]로 둘러싸인 코드를 보신적이 있으신가요? 그것이 바로 애트리뷰트입니다. 우리가 필요에 의해서 이 애트리뷰트를 사용해 코드 앞에다 설명을 덧붙일 수 있습니다. 다음은 애트리뷰트의 기본 형식입니다.

[attribute명(positional_parameter, name_parameter = value, ...)]

여기서 positional_parameter는 위치지정 파라미터라고 해서 반드시 적어야하는 부분으로, ” “를 사용하여 작성합니다. 그리고 name_parameter는 명명 파라미터로, 선택적인 정보이며 = 를 사용해서 값을 기입합니다. 이 애트리뷰트는 크게 두가지로 나뉘는데, 사용자가 정의하는 커스텀 애트리뷰트와 내장되어 있는 공통 애트리뷰트로 나뉩니다. 공통 애트리뷰트의 경우는 추가된 정보가 컴파일 방식에 영향을 줄 수 있는데, 반대로 커스텀 애트리뷰트는 영향을 주지 못합니다. 

이제 한번, 애트리뷰트를 사용해가면서 어떤 편리한 점이 있는지 알아보도록 합시다. 공통 애트리뷰트인 Obsolete, Conditional, DllImport 부터 알아보도록 하겠습니다.

#define TEST

using System;
using System.Collections.Generic;
using System.Linq;
using System.Diagnostics;
using System.Text;
using System.Reflection;
using System.Threading.Tasks;

namespace ConsoleApplication43
{
    class Program
    {
        // TEST가 정의되어 있어야만 호출이 가능합니다.
        [Conditional("TEST")]
        public void TestConditional()
        {
            Console.WriteLine("TestConditional!");
        }

        static void Main(string[] args)
        {
            Program program = new Program();

            program.TestConditional();
        }
    }
}
TestConditional!
계속하려면 아무 키나 누르십시오 . . .

자 우선 Conditional 부터 알아보도록 하겠습니다. 코드를 보시게 되면 1행에 #define 전처리기로 TEST라는 것이 정의되어 있습니다. 16행을 보시면 Conditional 애트리뷰트가 쓰였는데, 이 Conditional은 메소드에만 적용할 수 있으며, 메소드를 컴파일 할지 말지의 여부를 조건부로 결정하게 됩니다. 해당 기호가 정의되어 있으면 호출이 포함되고, 그렇지 않다면 호출이 생략되는 것입니다. 궁금하시다면, 1행을 주석처리 해보고 다시 컴파일하시면 아래와 같은 결과만 나오게 됩니다. 즉, TEST 기호가 정의되어 있지 않아 TestConditional 메소드는 생략된 것이나 다름이 없습니다.

계속하려면 아무 키나 누르십시오 . . .

이번에는 Obsolete 입니다.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Diagnostics;
using System.Text;
using System.Reflection;
using System.Threading.Tasks;

namespace ConsoleApplication43
{
    class Program
    {
        [Obsolete("OldMethod()는 더이상 사용하지 않으므로, NewMethod()를 사용해주세요.")]
        public void OldMethod() { Console.WriteLine("OLD!"); }

        public void NewMethod() { Console.WriteLine("NEW!"); }

        static void Main(string[] args)
        {
            Program program = new Program();

            program.OldMethod();
            program.NewMethod();
        }
    }
}
OLD!
NEW!
계속하려면 아무 키나 누르십시오 . . .
경고 1 'ConsoleApplication43.Program.OldMethod()'은(는) 사용되지 않습니다. 'OldMethod()는 더이상 사용하지 않으므로, NewMethod()를 사용해주세요.'
C:\Users\h4ckfory0u\Documents\Visual Studio 2012\Projects\ConsoleApplication43\ConsoleApplication43\Program.cs
23
13
ConsoleApplication43

13행을 보시면 Obsolete 애트리뷰트가 쓰였습니다. 이 Obsolete 애트리뷰트는 클래스 내에서 더이상 사용되지 않는 함수를 사용하려고 할때 이 요소를 쓰지 말것을 권고하는 경고 혹은 에러를 발생시킵니다. 두번째 인수에 true를 덧붙이면, 컴파일 시 구형 메소드가 쓰였을때 최신 메소드 사용을 권고하는 메세지와 함께, 에러가 발생합니다.

[Obsolete("OldMethod()는 더이상 사용하지 않으므로, NewMethod()를 사용해주세요.", true)]
..
오류 1 'ConsoleApplication43.Program.OldMethod()'은(는) 사용되지 않습니다. 'OldMethod()는 더이상 사용하지 않으므로, NewMethod()를 사용해주세요.'
C:\Users\h4ckfory0u\Documents\Visual Studio 2012\Projects\ConsoleApplication43\ConsoleApplication43\Program.cs
23
13
ConsoleApplication43

이번에는 DllImport 입니다.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Diagnostics;
using System.Text;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading.Tasks;

namespace ConsoleApplication43
{
    class Program
    {
        [DllImport("User32.dll")]
        public static extern int MessageBox(int hParent, string Messgae, string Caption, int Type);

        static void Main(string[] args)
        {
            MessageBox(0, "DllImport!", "DllImport", 3);
        }
    }
}

코드를 보시면 7행에 System.Runtime.InteropServices를 사용하도록 지시하고, 14행을 보시면 DllImport로 User32.dll에 정의되어 있는 함수를 불러와 사용하였습니다. 여기서 extern 키워드는 ‘프로그램 외부’를 의미합니다. 즉, 외부 DLL에 정의되어 있는 함수를 사용할때에는, extern 지정자를 붙여 주어야만 합니다.

사용자 정의 애트리뷰트(Custom Attribute)

커스텀 애트리뷰트는, 사용자가 직접 정의하는 애트리뷰트입니다. 방금 말한 MSDN 링크에 들어가보시면, 모든 애트리뷰트는 System.Attribute 클래스에서 파생되었습니다. 그럼, ‘이 System.Attribute 클래스를 상속하면 우리가 직접 애트리뷰트를 만들수가 있나요?’라는 궁금증이 생기시는 분도 있을텐데, 물론 가능합니다. 이제부터 그것을 알아보려고 합니다. 아래를 한번 볼까요?

using System;
using System.Collections.Generic;
using System.Linq;
using System.Diagnostics;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApplication43
{
    [CustomAttribute("STR", vartmp="TMP")]
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("CustomAttribute!");
        }
    }

    class CustomAttribute : System.Attribute
    {
        private string str;
        private string tmp;

        public CustomAttribute(string str)
        {
            this.str = str;
        }

        public string vartmp
        {
            get
            {
                return tmp;
            }
            set
            {
                tmp = value;
            }
        }
    }
}

이렇게, 우리가 직접 애트리뷰트를 만들 수가 있습니다. 10행을 보시면 우리가 정의한 CustomAttribute를 사용할 수 있고, 19~40행을 보시면 CustomAttribute 클래스는 System.Attribute 클래스로부터 상속받았다는 것을 알 수 있습니다. 그리고 애트리뷰트 클래스의 멤버로 필드, 메소드, 프로퍼티, 생성자 등을 가질 수 있다는 것도 알 수 있습니다. (우리가 아직 프로퍼티에 대해선 배우지 않았는데, 이 프로퍼티(Property)는 다음 강좌에서 배우려고 하니, 지금은 간단히 알아두기만 합시다. 프로퍼티는 29~38행이며, 이 프로퍼티를 이용해 private 멤버에 접근할 수 있습니다.)

그리고, Conditional 특성처럼, System.AttributeUsage를 이용하여, 적용 가능한 요소값을 지정할 수 있습니다. 아래는 코드 요소를 모두 나열한 것입니다.

AttributeTargets설명
All모든 요소
Assembly어셈블리
Module모듈
Inferface인터페이스
Class클래스
Struct구조체
Enum열거형
Constructor생성자
Method메소드
Property프로퍼티
Field필드
Event이벤트
Parameter메소드의 매개 변수
Delegate델리게이트
ReturnValue메소드의 반환 값
VaildOn

(위 링크에서 AllowMultiple은 동일한 애트리뷰트를 여러번 지정할 수 있게끔 해주는 녀석입니다. default: false)
(Inherited는 파생되는 클래스 혹은 재정의되는 메소드 적용여부를 결정하게 해주는 녀석입니다. default: true)

그럼 이제, 아까의 코드에서 CustomAttribute 클래스 정의부분 위에 아래의 코드를 덧붙여보도록 합시다.

[AttributeUsage(AttributeTargets.Method)]

그리고 컴파일을 한다면, 다음과 같은 오류가 발생할 것입니다.

오류 1 'CustomAttribute' 특성은 이 선언 형식에서 사용할 수 없습니다. 'method' 선언에만 사용할 수 있습니다.
C:\Users\h4ckfory0u\Documents\Visual Studio 2012\Projects\ConsoleApplication43\ConsoleApplication43\Program.cs
10
6
ConsoleApplication43

그럼, 클래스 선언에도 사용이 가능하게, 옵션을 하나 더 붙이도록 해봅시다. 아래와 같이 말입니다.

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]

이번에는 오류가 나지않고 정상적으로 컴파일 되었습니다. 이처럼 논리합(|) 연산자를 사용하여 두 개 이상의 요소를 묶어서 지정할 수 있습니다. 애트리뷰트, 상당히 유용한 기능이죠?

댓글 남기기