-
18장. 사용자 정의 특성CLR Via C# 2022. 3. 16. 21:56
사용자 정의 특성은 코드에 표기를 해서 특별한 기능을 수행할 수 있도록 해주며, 모든 종류의 메타데이터 테이블 내의 항목에 추가적인 정보를 부가할 수 있다.
사용자 정의 특성의 사용
사용자 정의 특성에 대해서 가장 먼저 알아두어야 할 내용은 이 메커니즘이 단순히 특정 대상에 대해 추가적인 정보를 부가하는 방법이며, 구체적으로 살펴보더라도 컴파일러가 관리 모듈의 메타데이터상에 추가적인 정보를 부가하는 것에 불과하다는 것이다. 대부분의 특성은 컴파일러 자체에는 아무런 의미가 없으며, 컴파일러는 그저 소스 코드에서 특성을 찾아 메타데이터에 적절한 정보를 부가하는 것이 전부다.
C#에서는 특성을 적용할 때 접두사의 형태로 적용 대상을 지정할 수 있는데, 많은 경우에 있어 접두사를 명시하지 않으면 컴파일러가 특성이 어디에 적용되어야 하는지를 자동으로 유추한다. 그러나 몇몇 상황의 경우 컴파일러에게 정확한 의도를 전달하기 위하여 반드시 접두사를 사용해야 하는 경우도 있다.
using System; [assembly : SomeAttr] // 어셈블리에 적용된다. [module : SomeAttr] // 모듈에 적용된다. [type : SomeAttr] // 타입에 적용된다. internal sealed class SomeType<[typevar : SomeAttr] T>{ // 제네릭 타입 변수에 적용된 ... }사용자 정의 특성이란 단순히 특정 타입의 인스턴스다. CLS의 규약에 따르면, 사용자 정의 특성 타입은 직접적이든 간접적이든 모두 public 추상 클래스인 System.Attribute 클래스로부터 상속을 받아야 한다고 되어 있다.
앞에서 언급한 것처럼, 사용자 정의 특성은 임의의 클래스의 인스턴스이므로, 객체 생성을 위해서 반드시 public 생성자가 있어야만 한다. 이로 인해 특성을 사용하는 문법은 마치 클래스의 인스턴스 생성자를 호출하는 것과 비슷한 모양을 띤다. 또한 일부 언어에서는 특성 클래스와 연결된 public 필드나 속성 값을 대입할 수 있는 특별한 문법을 지원하기도 한다.
[ DllImport("Kernel32", CharSet = CharSet.Auto, SetLastError = true)] public static extern Boolean GetVersionEx([In, Out] OSVERSIONINFO ver);이 문법은 일반적인 생성자 호출 문법과는 많이 다르다. DllImportAttribute 클래스에 대한 문서를 살펴보면, 생성자는 단일 String 매개변수 하나를 필요로 한다는 것을 알 수 있다. 즉 앞의 예제에서 "Kernel32"는 생성자의 매개변수로서 전달된 것이다. 생성자의 매개변수들은 보통 위치 매개변수(Positional Parameter) 라고 불리며 누락할 수 없는 필수 매개변수들이므로 특성을 적용하려 할 때 반드시 지정해야만 한다.
그렇다면 나머지 두 매개변수는 무엇일까? C#은 이 같은 구조로 DllImportAttribute의 생성자를 호출한 후, public 필드나 속성에 임의의 값을 대입할 수 있도록 해준다. 즉 이 예제는 "Kernel32"라는 문자열을 DllImportAttribute 객체의 생성자에 매개변수로 전달하여 해당 객체를 생성한 후, public 인스턴스 필드인 CharSet과 SetLastError에 각각 CharSet.Auto와 true를 지정한다. 필드나 속성 등에 값을 대입하기 위해서 지정하는 매개변수를 위치 매개변수와 구분하여 명명된 매개변수(Named Parameter) 라고 부르며 선택적으로 사용할 수 있다.
사용자 정의 특성 클래스 정의하기
사용자 정의 특성 클래스를 정의하려면 앞에서 살펴봤듯이 System.Attribute를 상속받은 클래스이며 비추상 특성일 시 public 생성자가 있어야한다.
여기에 추가적으로 AttributeUsageAttribute 사용자 정의 특성을 사용하여 적용 대상을 제한할 수 있다. 또한 부수적으로 AllowMultiple과 Inherited의 두개의 public 속성을 지원하고 있다.
AllowMultiple 속성이 True면 단일 대상에 대하여 단일 특성을 여러 번 적용할 수 있고, False면 단일 대상에 대해 단일 특성은 단 한번 만 적용이 가능하다.
Inherited 속성의 적용대상이 클래스인 경우에는 그 클래스를 상속한 클래스에도 동일 특성이 적영됨을 의미한다. 대상이 메서드일 경우에는 이 메서드를 재정의하는 경우에도 동일 특성이 적용됨을 의미한다. 단, 클래스, 메서드, 속성, 이벤트, 필드, 메서드 반환 값, 매개변수에 대해서만 상속을 고려한다. 따라서 적용 타입이 이들 중 하나일 경우에만 Inherited 속성을 true로 설정할 수 있다. 참고할 것은 관리되는 모듈에 생성될 상속된 타입에 부수적인 메타데이터가 새로 생기지는 않는다는 점이다.
[AttributeUsage(AttributeTargets.Class, Inherited = true)] public class SomeAttribute : System.Attribute{ public SomeAttribute(){} }사용자 정의 특성의 생성자와 필드/속성의 데이터 타입
사용자 정의 특성 클래스를 정의할 때 반드시 지정되어야 하는 값은 생성자의 매개변수를 통해서 전달받도록 할 수 있다. 이 생성자는 사용자 정의 특성 클래스의 인스턴스를 생성할 때 호출된다. 그 외에도 비정적 public 필드와 속성을 타입 안에 정의하여 개발자가 사용자 정의 특성 클래스의 인스턴스에 대해 선택적으로 값을 설정할 수 있도록 할 수 있다.
사용자 정의 특성을 적용할 때에는 반드시 컴파일 시점에서 확정 가능한 상수 표현식을 적용하려는 클래스에서 정의한 것과 일치하는 타입으로 지정해야 한다. 다음은 사용자 정의 특성의 사용 예이다.
using System; internal enum Color { Red } [AttributeUsage(AttributeTargets.All)] internal sealed class SomeAttribute : Attribute { public SomeAttribute(String name, Object o, Type[] types) { ... } } [Some("Lim", Color.Red, new Type[] { typeof(Math), typeof(Console) })] internal sealed class SomeType { }사용자 정의 특성의 인스턴스는 바이트 스트림으로 Serialization되어 메타데이터 내에 포함되며 실행 시점에서 바이트 스트림을 deserialize하여 객체를 복원한다. 실제로는 컴파일러가 사용자 정의 특성 클래스를 다시 복원하기 위하여 필요한 추가 정보들도 메타데이터에 같이 기록되는데, 사용자 정의 특성의 생성자가 취하는 매개변수들은 1바이트의 타입 ID 플래그 값을 저장하고, 연이어 그 값을 저장한다. 이후 필드와 속성 값을 저장하는데, 1바이트의 타입 ID 플래그를 저장하고 필드와 속성의 이름을 저장한 후 값을 저장한다. 배열의 경우 배열 요소의 개수를 먼저 저장하고, 연이어 개별 요소를 저장한다.
사용자 정의 특성을 검출하기
사용자 정의 특성 클래스를 직접 정의하는 경우에는 이 특성이 적용된 대상에 대해서 사용자 정의 특성 클래스가 적용되었는지를 확인하여 그에 준하는 동작을 수행하도록 해야 한다.
FCL은 사용자 정의 특성 클래스의 적용 여부를 확인할 수 있는 다양한 방법들을 제공한다. 만약 System.Type 객체를 이용하는 경우, IsDefined 메서드를 통해 확인하면 된다. 하지만 가끔은 타입이 아닌 어셈블리나 모듈 혹은 메서드와 같은 대상에 대해서도 사용자 정의 특성 클래스의 적용 여부를 확인해야 할 때가 있다. 이 경우 System.Reflection.CustomAttributeExtensions 클래스에 정의된 확장 메서드를 사용하면 된다. 이 클래스에서는 IsDefined, GetCustomAttributes, GetCustomAttribute라는 세 가지의 정적 메서드가 있어서 사용자 정의 특성의 적용 여부를 확인할 수 있도록 해준다. 각각의 메서드는 여러 형태의 오버로드 버전을 제공한다. 또한 상속 계통을 따라서 사용자 정의 특성의 적용 여부를 추적하는 메서드들도 제공된다.
이 메서드들을 호출하면, 내부적으로 관리 모듈의 메타데이터를 대상으로 적용된 사용자 정의 특성 클래스를 찾기 위해서 문자열 비교를 수행한다. 당연히 이러한 비교 작업에는 시간이 걸린다. 성능이 중요한 요소라면, 이러한 동일한 정보를 확인하기 위해서 이러한 메서드를 반복적으로 호출하지 않도록, 그 결과를 캐싱하는 것에 신경을 써야 할 것이다.
사용자 정의 특성의 사용 여부를 객체 생성 없이 파악하기
보안을 중시하는 몇몇 상황에서 이 기법을 사용하면 Attribute 클래스나 이를 상속한 클래스들을 사용하지 않아도 된다. Attribute의 GetCustomAttribute(s) 메서드를 사용하면, 내부적으로 사용자 정의 특성 클래스의 생성자를 호출하고, 이 객체의 속성을 설정하기 위해서 set 접근자 메서드를 호출하게 된다. 또한 해당 타입을 처음으로 사용하는 경우라면 CLR이 해당 타입의 타입 생성자를 호출하게 된다(존재한다면). 사용자 정의 특성의 내용을 살펴보기 위해서라도 생성자, Set 접근자, 그리고 타입 생성자를 거치면서 그 안에 담긴 코드를 실행하게 된다. 이로 인해 앱 도메인 안에서 검증되지 않은 코드를 실행할 수 있는 여지를 남기게 되며, 잠재적인 보안 문제가 될 수 있다.
사용자 정의 특성을 정의하고 있는 클래스의 코드를 수행하지 않고, 사용자 정의 특성의 적용 여부를 확인하기 위해서는, System.Reflection.CustomAttributeData 클래스를 사용하면 된다. 이 클래스는 사용자 정의 특성을 가져올 수 있는 GetCustomAttributes라는 이름의 정적 메서드를 정의하고 있다. 총 네 가지의 오버로드된 메서드를 제공하는데, 각각 Assembly, Module, ParameterInfo, MemberInfo 타입의 매개변수를 받을 수 있으며 System.Reflection 네임스페이스 안에 정의되어 있다.
CustomAttributeData의 GetCustomAttribute 메서드는 팩토리 패턴처럼 동작하는데, 이 메서드를 호출하면 CustomAttributeData 객체들을 담고 있는 컬렉션이 IList 타입으로 반환된다. 이 컬렉션 내의 각 요소는 하나의 사용자 정의 특성을 나타내는데, 각각의 CustomAttributeData 객체를 이용하면 사용자 정의 특성 객체들이 어떻게 생성되고 초기화되는지에 대한 정보를 읽기 전용의 속성으로 조회할 수 있다. 주목할 만한 부분은 여기서 나오는 모든 정보들이 "설정될 값들에 대한 내용"을 조회하는 것으로, 실제로는 아무런 코드도 실행되지 않기 때문에 보안에 관련된 이점을 잃어버리지 않고도 원하는 목적을 달성할 수 있다.
Conditional 특성 클래스
컴파일러가 특정 사용자 정의 특성이 적영된 대상이 있는지를 확인하는 코드를 작성하려는 경우에 Conditional 특성 클래스를 이용하면 사용자 정의 특성을 적용한 대상을 컴파일할 때 특정 기호가 정의된 경우에만 메타데이터에 관련 특성을 부가하게 된다. 하지만 사용자 정의 특성 클래스를 정의한 메타데이터와 그 구현부는 여전히 어셈블리에 남게 된다.
//#define TEST #define VERIFY using System; using System.Diagnostics; [Conditional(TEST)] // TEST가 define 안돼서 코드 실행 안됨! public sealed class CondAttribute : Attribute { } [Cond] // CondAttribute 는 적용되지 않는다! public sealed class Program{ ... }'CLR Via C#' 카테고리의 다른 글
20장. 예외와 상태 관리 (0) 2022.06.30 19장. Null 값 타입 (0) 2022.06.21 17장. 델리게이트 (0) 2022.03.09 16장. 배열 (0) 2022.02.28 15장. 열거 타입과 비트 플래그 (0) 2022.02.23