특성(Attribute)과 리플렉션(Reflection)

  • 22 minutes to read

C#에서는 특성(Attribute)을 제공합니다. 특성을 사용하여 C#의 여러 구성 요소들에 추가적인 정보(메타 데이터)를 제공하는 방법을 살펴보겠습니다.

> // 특성: 프로그램에서 형식, 멤버 및 다른 엔터티에 대한 추가 선언 정보를 지정 가능

특성(Attribute)

C#에서의 특성(Attribute)은 데코레이터(Decorator)와 애너테이션(Annotation)의 성격을 가지고 있습니다. 말이 조금 어렵죠? 간단히 말해서 여러분들이 작성한 프로그램 코드에 추가적인 설명을 붙이는 것입니다. 자동차 개체를 예로 든다면 튜닝과 비슷합니다.

특성의 여러가지 의미

C# 특성(Attribute)은 다시 한번 아래 내용을 읽고 넘어가면 됩니다.

  • 특성은 프로그램에 메타데이터(Metadata)를 추가합니다.
  • 데코레이터(Decorator)와 애너테이션(Annotation)의 성격을 가지고 있습니다.
  • 꾸밈자(Decorate, Describe, Declarative) 역할을 합니다.
  • 여러 구성 요소들에 추가적인 정보를 제공합니다.

특성의 표현 방법

특성은 특정한 클래스 등의 C# 구성 요소 앞에 [](대괄호)로 표시합니다.

[Obsolete]
public class OldClass {}

닷넷에 내장되어 있는 특성

닷넷에 내장되어 있는 특성들은 굉장히 많이 있는데요. 학습자 입장에서는 우선 Obsolete 특성과 Conditional 특성을 먼저 정리해보면 좋습니다.

  • [Obsolete] 특성
  • [Conditional] 특성

위 2개의 특성에 대한 예제는 잠시 후에 살펴보겠습니다.

특성은 대괄호 기호를 사용하여 멤버 앞에 붙여 사용합니다.

사용자 지정 특성

사용자가 직접 새로운 특성을 만들 수도 있습니다. 이럴때에는 Attribute 클래스를 상속하는 클래스를 통해서 사용자 지정 특성을 만들 수 있습니다.

Obsolete 특성 사용하기

먼저 특성 학습을 위한 최고의 내장된 특성인 Obsolete를 사용해보겠습니다. C# 인터렉티브에서 다음 코드를 순서대로 작성해보세요.

코드: ObsoleteDemo.cs

(1) OldMember()와 NewMemober() 메서드를 작성후 실행해봅니다. 
> void OldMember() => Console.WriteLine("Old Method");
> void NewMember() => Console.WriteLine("New Method");
> OldMember()
Old Method
> NewMember()
New Method

(2) OldMember() 메서드 앞에 [Obsolete] 특성을 붙이고 다시 만들고 실행해봅니다.

> [Obsolete] void OldMember() => Console.WriteLine("Old Method");
> OldMember()
Old Method

위 코드에서는 표시가 나지 않지만, Visual Studio에서는 Obsolete 특성이 적용된 메서드를 호출할 때 경고 메시지를 제공합니다. 다음 그림과 같이 OldMember() 메서드 호출에 밑줄이 생기고 마우스를 올려보면 경고 메시지가 출력됩니다.

그림: Obsolete 특성에서 제공하는 컴파일러 정보

Obsolete 특성에서 제공하는 컴파일러 정보

(3) Obsolete 특성에 추가적인 경고 메시지를 줄 수 있습니다.

> [Obsolete("Using New Member Method")]
. void OldMember() => Console.WriteLine("Old Method");
> OldMember()
Old Method
> NewMember()
New Method

(4) Obsolete 특성의 두 번째 매개 변수에 true 값을 주게되면 해당 메서드를 사용하면 경고가 아닌 에러가 발생됩니다.

> [Obsolete("Using New Member Method", true)]
. void OldMember() => Console.WriteLine("Old Method");
> OldMember()
(1,1): error CS0619: 'OldMember()'은(는) 사용되지 않습니다. 'Using New Member Method'
>

Obsolete 특성은 라이브러리 또는 프레임워크 제작시 기존의 하위 호환성을 위해서 코드는 남겨놓지만, Obsolete 특성이 적용된 메서드는 사용하지 않도록 권장하는 목적으로 많이 사용됩니다.

특성 사용 샘플 코드

C#을 사용하는 많은 영역에서 내장된 특성이 사용됩니다. 다음 샘플 코드는 메서드가 아닌 속성에 특성을 적용한 예입니다. 그냥 한번 살펴보고 넘어가면 됩니다.

@code {
    int currentCount = 0;

    [Parameter] int IncrementAmount { get; set; } = 1;

    void IncrementCount()
    {
        currentCount += IncrementAmount;
    }
}

위 코드 샘플은 C# Blazor의 일부 코드인데요. [Parameter] 특성을 사용하여 외부에서 IncrementAmount 이름의 매개 변수를 지정해서 값을 변경하는 샘플 코드입니다.

특성의 매개 변수

특성에 매개 변수를 전달할 수 있습니다. 형태에 따라서 위치 매개 변수와 이름 매개 변수로 구분할 수 있습니다.

  • 위치 매개 변수: 특성에 전달되는 매개 변수는 위치에 따라서 구분됩니다.
  • 이름 매개 변수: 특성에 구현된 속성 또는 필드(public)에 값을 전달할 때 사용됩니다. 다음 샘플 코드처럼 WebServer 특성에 Namespace 속성에 값을 전달합니다.
    • [WebServer(Namespace="http://www.hawaso.com/")]

[Conditional] 특성 사용하기

닷넷에 내장되어 있는 특성 중에는 [Conditional] 특성이 있습니다. 이 특성을 사용하면 특정 기호(Symbol)에 따라서 실행될지 안될지를 결정할 수 있습니다.

다음 코드를 작성 후 실행하세요.

코드: ConditionalDemo.cs

#define RELEASE //[2][1] 전처리기 지시문으로 RELEASE 기호 정의
using System;
using System.Diagnostics;

public class ConditionalDemo
{
    static void Main()
    {
        DebugMethod();
        ReleaseMethod();
    }

    [Conditional("DEBUG")] //[1] DEBUG 기호(심볼)을 가지는 경우에 실행
    static void DebugMethod() => Console.WriteLine("디버그 환경에서만 표시");

    //[2][2] RELEASE 기호가 있는 경우에 실행
    [Conditional("RELEASE")] static void ReleaseMethod() 
        => Console.WriteLine("릴리스 환경에서만 표시");
}
디버그 환경에서만 표시
릴리스 환경에서만 표시

Visual Studio의 도구모음에는 다음 그림과 같이 Debug와 Release를 구분지을 수 있는 드롭다운리스트를 제공합니다. 이를 사용하여 프로그램에 DEBUG 기호와 RELEASE 기호를 제공할 수 있습니다.

그림: Visual Studio의 빌드 방식 변경 드롭다운리스트

Build Type

[Conditional] 특성은 이러한 기호를 사용하여 특정 메서드를 실행할지 말지를 결정할 수 있습니다.

특성을 사용하여 메서드 호출 정보 얻기

이번에는 특성을 사용하여 메서드의 호출 정보를 얻는 방법을 알아보겠습니다.

코드: CallerInformation.cs

using System.Runtime.CompilerServices;
using static System.Console;

class CallerInformation
{
    static void Main()
    {
        TraceMessage("여기서 무엇인가 실행...");
    }

    public static void TraceMessage(string message,
            [CallerMemberName] string memberName = "",
            [CallerFilePath] string sourceFilePath = "",
            [CallerLineNumber] int sourceLineNumber = 0)
    {
        WriteLine("실행 내용: " + message);
        WriteLine("멤버 이름: " + memberName);
        WriteLine("소스 경로: " + sourceFilePath);
        WriteLine("실행 라인: " + sourceLineNumber);
    }
}
실행 내용: 여기서 무엇인가 실행...
멤버 이름: Main
소스 경로: C:\C#\CallerInformation\CallerInformation\CallerInformation.cs
실행 라인: 8

메서드의 매개 변수 앞에 [CallerMemberName], [CallerFilePath], [CallerLineNumber] 등의 특성을 사용하여 메서드를 호출한 호출자 정보를 얻을 수 있습니다.

Experimental 특성

C# 12.0에서는 [Experimental] 특성이 도입되어 특정 API가 실험적인 기능임을 나타낼 수 있습니다. 이를 통해 해당 API가 향후 변경될 가능성이 있음을 컴파일러 경고로 개발자에게 알릴 수 있습니다.

코드: ExperimentalAttributeDemo.cs

using System;
using System.Diagnostics.CodeAnalysis;

public class ExperimentalAttributeDemo
{
    [Experimental("ExperimentalFeature")]
    static void ExperimentalMethod()
    {
        Console.WriteLine("이것은 실험적인 기능입니다.");
    }

    static void Main()
    {
#pragma warning disable ExperimentalFeature
        ExperimentalMethod();
#pragma warning restore ExperimentalFeature
    }
}
이것은 실험적인 기능입니다.

이 코드를 실행하면 ExperimentalMethod가 실험적인 기능임을 나타내는 컴파일 경고가 발생하며, 콘솔에 "이것은 실험적인 기능입니다."라는 메시지가 출력됩니다.

사용자 지정 특성 만들기

클래스, 메서드 등에 대괄호를 붙여 사용할 수 있는 특성을 직접 원하는 이름으로 만들 수 있습니다. 다음과 같은 코드로 Attribute 클래스를 상속하는 CustomAttribute 클래스는 [Custom] 또는 [CustomAttribute] 특성으로 사용할 수 있습니다.

> public class CustomAttribute : Attribute { }
> [Custom] void Test() => Console.WriteLine("Custom Attribute");

특성은 특정한 클래스에 추가적인 설명(다른 말로 표현하면 태그라 할 수 있음)을 붙이고자 할 때 사용되는 클래스이며 System.Attribute 클래스로부터 상속을 받습니다.

> public class MyAttribute : System.Attribute { }

Attribute 클래스를 상속하여 사용자 지정 특성 만들기

사용자 지정 특성을 만들어보겠습니다. 다음 코드를 작성 후 실행해보세요.

코드: AttributePractice.cs

using System;

// [1] Attribute 클래스를 상속하여 사용자 지정 특성 만들기 
public class SampleAttribute : Attribute
{
    public SampleAttribute() => Console.WriteLine("사용자 지정 특성 사용됨");
}

[Sample]
public class CustomAttributeTest { }

class AttributePractice
{
    static void Main()
    {
        // [2] CustomAttributeTest 클래스에 적용된 특성들 가져오기 
        Attribute.GetCustomAttributes(typeof(CustomAttributeTest));
    }
}
사용자 지정 특성 사용됨

[1]번 코드와 같이 Attribute 클래스를 상속하여 SampleAttribute 이름의 특성을 만들 수 있습니다. 사용자 지정 특성은 ~Attribute로 끝나고 이를 줄여서 [Sample] 형태로 표현할 수 있습니다. [2]번 코드에서는 CustomAttributeTest에 적용된 특성 목록을 가져오면서 SampleAttribute 클래스의 생서자가 호출되어 "사용자 지정 특성 사용됨" 문자열이 출력됩니다.

매개 변수가 있는 사용자 지정 특성 만들기

이번에는 매개 변수를 하나 갖는 사용자 지정 특성을 사용해보겠습니다.

코드: NickNameAttributeTest.cs

using System;

// [1] AttributeUsage 특성을 사용하여 특성에 대한 제약 조건 등 설정
[AttributeUsage(
    AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true)]
public class NickNameAttribute : Attribute
{
    public string Name { get; set; }
    public NickNameAttribute(string name) { Name = name; }
}

// [2] AllowMultiple에 의해서 여러 번 설정 가능
[NickName("길벗")]
[NickName("RedPlus")]
class NickNameAttributeTest
{
    static void Main() => ShowMetaData();

    static void ShowMetaData()
    {
        // 모든 커스텀 어트리뷰트 가져오기 
        Attribute[] attrs =
            Attribute.GetCustomAttributes(typeof(NickNameAttributeTest));
        foreach (var attr in attrs)
        {
            // [A] is 연산자를 사용하여 커스텀 어트리뷰트의 Name 속성 출력
            if (attr is NickNameAttribute)
            {
                NickNameAttribute ais = (NickNameAttribute)attr;
                Console.WriteLine("{0}", ais.Name);
            }
            // [B] as 연산자를 사용하여 커스텀 어트리뷰트의 Name 속성 출력
            NickNameAttribute aas = attr as NickNameAttribute;
            if (aas != null)
            {
                Console.WriteLine("{0}", aas.Name);
            }
        }
    }
}
길벗
길벗
RedPlus
RedPlus

사용자 지정 특성에 [1]번 코드와 같이 또 다른 특성인 AttributeUsage 특성을 사용하여 클래스 또는 메서드에 적용할 수 있음을 알려줄 수 있고 AllowMultiple 속성을 사용하여 한 번 이상을 적용할 수 있는지를 설정할 수 있습니다. [2]번 코드처럼 NickName 특성의 생성자에 전달된 매개 변수는 Name 속성에 저장되어 [A], [B] 코드에서 Name 속성으로 전달된 값을 가져다 사용할 수 있습니다.

리플렉션(Reflection)

리플렉션(Reflection)은 동적으로 특정 어셈블리 또는 형식에 대한 메타데이터를 Type 개체로 반환해 주는 것을 말합니다.

리플렉션을 사용하면 특성에 대한 정보를 얻거나 동적으로 특정 형식을 로드하여 사용할 수 있습니다.

Type 클래스와 Assembly 클래스

Type 클래스로 문자열 개체에 대한 정보를 얻어서 출력하는 간단한 리플렉션 예제는 다음과 같습니다.

> string r = "Reflection";
> Type t = r.GetType();
> t
[System.String]

Assembly 클래스를 사용하면 특정 어셈블리에 대한 정보를 얻을 수 있습니다.

> System.Reflection.Assembly assembly = typeof(System.Random).Assembly;
> assembly
[mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]

Random 클래스의 멤버 리스트를 가져와서 2개만 보여주는 코드는 다음과 같습니다.

> typeof(Random).GetMembers().Take(2)
TakeIterator { [Int32 Next()], [Int32 Next(Int32, Int32)] }

모든 경우의 수를 샘플 코드로는 나열할 수 없지만, 리플렉션을 사용하면 특성 어셈블리와 클래스의 멤버 정보를 얻고 이를 사용할 수 있습니다.

특정 클래스의 메서드와 속성을 동적으로 호출

이번에는 리플렉션을 사용하여 특정 클래스의 메서드와 속성 등의 정보를 동적으로 가져오는 예제를 만들어 보겠습니다.

코드: ReflectionGetMembers.cs

//[?] 리플렉션: 특정 클래스 등에 대한 정보(메타데이터)를 반환시켜주는 기능
using System;
using System.Reflection;

namespace ReflectionGetMembers
{
    class Test
    {
        public static void TestMethod() { }
    }

    class ReflectionGetMembers
    {
        static void Main()
        {
            // Test 클래스에 대한 Type 개체 가져오기 
            Type t = typeof(Test);

            // 원하는 멤버를 조건에 따라 가져오기 
            MemberInfo[] members =
                t.GetMembers(BindingFlags.Static | BindingFlags.Public);

            // 멤버 출력
            foreach (var member in members)
            {
                Console.WriteLine("{0}", member.Name);
            }
        }
    }
}
TestMethod

리플렉션을 사용하여 Test 클래스의 정적 멤버 리스트를 얻은 후 멤버 이름을 출력하는 에제입니다. 리플렉션을 사용하면 이번 예제처럼 특정 클래스의 전체 멤버 리스트 또는 특정 조건에 맞는 멤버를 얻을 수 있습니다.

Type 클래스로 클래스의 멤버 호출

이번에는 Type 클래스로 특정 클래스의 멤버를 호출하는 방법을 알아보겠습니다.

코드: ReflectionGetMethod.cs

using System;
using System.Reflection;
namespace ReflectionGetMethod
{
    public class MemberClass
    {
        public string Name { get; set; } = "길벗출판사";
        public string GetName()
        {
            return Name + ", " + DateTime.Now.ToShortTimeString();
        }
    }

    class ReflectionGetMethod
    {
        static void Main()
        {
            //[1] 리플렉션 기능으로 특정 클래스의 멤버를 동적으로 호출(Invoke)
            MemberClass m = new MemberClass();
            Type t = m.GetType();

            //[a] 속성 읽어오기 및 속성 호출
            PropertyInfo pi = t.GetProperty("Name"); // Name 속성
            Console.WriteLine("속성 호출: {0}", pi.GetValue(m));

            //[b] 메서드 읽어오기 및 메서드 호출
            MethodInfo mi = t.GetMethod("GetName"); // GetName 메서드
            Console.WriteLine("메서드 호출: {0}", mi.Invoke(m, null));

            //[2] 참고: C# 4.0 이상에서는 dynamic 개체로 쉽게 멤버를 동적으로 호출
            dynamic d = new MemberClass(); // dynamic 키워드로 동적 개체 생성 
            Console.WriteLine("속성 호출: {0}", d.Name); // 속성 호출
            Console.WriteLine("메서드 호출: {0}", d.GetName()); // 메서드 호출       
        }
    }
}
속성 호출: 길벗출판사
메서드 호출: 길벗출판사, 오전 1:29
속성 호출: 길벗출판사
메서드 호출: 길벗출판사, 오전 1:29

[a] 코드처럼 GetProperty() 메서드로 특정 속성에 대한 정보를 얻은 후 GeValue() 메서드로 속성의 값을 동적으로 호출할 수 있습니다. [b] 코드처럼 GetMethod() 메서드로 특성 메서드에 대한 정보를 얻은 후 Invoke() 메서드로 동적으로 메서드를 호출할 수 있습니다.

특정 속성에 적용된 특성 읽어오기

이번에는 리플렉션을 사용하여 특정 속성에 적용된 특성을 읽어오는 방법을 알아보겠습니다.

코드: ReflectionGetProperty.cs

using System;
using System.Reflection;

namespace ReflectionGetProperty
{
    class Person
    {
        [Obsolete] public string Name { get; set; }
    }

    class ReflectionGetProperty
    {
        static void Main()
        {
            // Name 속성의 정보 얻기
            PropertyInfo pi = typeof(Person).GetProperty("Name");

            // Name 속성에 적용된 특성 읽어오기
            object[] attributes = pi.GetCustomAttributes(false);
            foreach (var attr in attributes)
            {
                // 특성의 이름들 출력
                Console.WriteLine("{0}", attr.GetType().Name);
            }
        }
    }
}
ObsoleteAttribute

Type 개체의 GetProperty() 메서드를 통해서 특정 속성에 대한 정보를 얻고 다시 GetCustomAttributes() 메서드를 통해서 특성에 대한 정보를 얻어올 수 있습니다. 평상시 우리가 사용하던 코드보다는 조금 더 복잡해보이기에 .NET API 탐색기 등을 통해서 각각의 메서드에 대한 추가 내용을 학습하길 권장합니다.

Type 클래스와 Activator 클래스로 동적으로 개체의 인스턴스 생성하기

Type.GetType() 메서드로 특정 클래스의 Type 개체를 가져올 수 있고 이를 다시 Activator 클래스의 CreateInstance() 메서드에 전달하여 동적으로 문자열로 지정된 클래스의 인스턴스를 생성할 수 있습니다.

동적 인스턴스를 생성하는 다음 예제를 작성 후 실행해보세요.

코드: TypeAndActivator.cs

using System;

namespace TypeAndActivator
{
    //[1] 샘플 클래스 및 메서드 생성
    public class MyClass
    {
        public void Test() => 
            Console.WriteLine("MyClass의 Test() 메서드가 실행됩니다.");
    }

    class TypeAndActivator
    {
        static void Main()
        {
            //[2] Type.GetType() 메서드에 지정한 클래스 형식을 가져옴
            Type type = Type.GetType("TypeAndActivator.MyClass");

            //[3] Activatory.CreateInstance() 메서드로 지정된 형식의 인스턴스 생성
            dynamic objType = Activator.CreateInstance(type);

            //[4] dynamic 타입의 Test() 메서드를 직접 지정해서 호출 
            objType.Test();
        }
    }
}
MyClass의 Test() 메서드가 실행됩니다.

프로그래밍을 하다보면 동적으로 특정 클래스의 인스턴스를 생성해야할 일이 있습니다. 이럴때에는 위 예제처럼 Type.GetType() 메서드와 Activator.CreateInstance() 메서드를 함께 사용하면 됩니다. 물론, 학습을 시작하는 독자들이 보는 이 책의 범위를 벗어나는 내용이니 이런게 있구나 넘어가면 됩니다.

장 요약

C#을 사용하는 ASP.NET, Entity Framework 등에서는 내장 특성이 굉장히 많습니다. C# 기초 문법 파트에서는 이번 강의에서 다뤄본 ObsoleteConditional 특성 정도만 다루면 충분할 것 같습니다. 사용자 지정 특성은 자주 사용되지 않지만, 가장 간단한 모양으로 2개 정도 만들어 보았습니다. 특성에 대한 정보는 Type 개체를 사용하는 리플렉션에 의해서 얻어올 수 있습니다. 추후 사용자 지정 특성과 리플렉션에 대해서 더 자세한 정보가 필요하면 Microsoft Learn 사이트에서 검색한 자료를 참고하면 됩니다.

VisualAcademy Docs의 모든 콘텐츠, 이미지, 동영상의 저작권은 박용준에게 있습니다. 저작권법에 의해 보호를 받는 저작물이므로 무단 전재와 복제를 금합니다. 사이트의 콘텐츠를 복제하여 블로그, 웹사이트 등에 게시할 수 없습니다. 단, 링크와 SNS 공유, Youtube 동영상 공유는 허용합니다. www.VisualAcademy.com
박용준 강사의 모든 동영상 강의는 데브렉에서 독점으로 제공됩니다. www.devlec.com