애트리뷰트는 타입, 멤버, 매개변수 등에 메타데이터를 부여하는 기능입니다. 컴파일러와 런타임, 프레임워크(예: ASP.NET Core, 데이터 주석)가 이 메타데이터를 해석해 동작을 확장합니다. 직접 애트리뷰트를 정의하고 리플렉션으로 읽어 활용하는 방법을 정리합니다.
1. 기본 개념과 문법
애트리뷰트는 대괄호로 선언 대상 위에 표시합니다. 클래스명이 Attribute로 끝나더라도 사용 시 접미사는 생략 가능하며, 생성자 인수는 위치 인수, set 가능한 프로퍼티는 명명 인수로 전달합니다.
[Serializable] // 접미사 'Attribute' 생략 가능
[System.Obsolete("Use NewMethod")] // 전체 이름 사용 가능
public class OldType {}
2. 사용자 정의 애트리뷰트 만들기
AttributeUsage로 적용 대상, 중복 허용, 상속 여부를 제어합니다. 생성자의 매개변수는 위치 인수, set 가능한 프로퍼티는 명명 인수로 설정합니다.
using System;
// 클래스/메서드에만 적용, 중복 허용, 상속 비적용
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = false)]
public sealed class AuditAttribute : Attribute
{
public string Action { get; } // 위치 인수로 받는 필수 값
public int Level { get; set; } = 1; // 명명 인수 (선택)
public string? Note { get; set; } // 명명 인수 (선택)
public AuditAttribute(string action)
{
Action = action;
}
}
// 속성/매개변수에 적용, 범위 검증 메타데이터
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
public sealed class BetweenAttribute : Attribute
{
public int Min { get; }
public int Max { get; }
public BetweenAttribute(int min, int max)
{
Min = min;
Max = max;
}
}
3. 애트리뷰트 적용 예시
클래스, 메서드, 속성, 매개변수, 어셈블리 수준에 적용하는 예시입니다.
using System;
// 어셈블리 수준 애트리뷰트 (파일 최상단에 위치)
[assembly: CLSCompliant(true)]
[Audit("Create", Level = 2, Note = "서비스 생성 작업")]
public class OrderService
{
[Between(1, 100)]
public int Quantity { get; set; }
[Audit("PlaceOrder")] // 메서드 수준 메타데이터
public void Place(
[Between(1, 5)] int priority // 매개변수 수준 메타데이터
)
{
// 구현 생략
Console.WriteLine($"Placing order with Quantity={Quantity}, Priority={priority}");
}
}
4. 런타임에서 애트리뷰트 읽기 (Reflection)
리플렉션으로 메타데이터를 읽어 동작을 결정합니다. 빈번한 호출에서는 결과 캐싱을 권장합니다.
using System;
using System.Linq;
using System.Reflection;
public static class AttributeReader
{
public static void Dump()
{
var type = typeof(OrderService);
// 클래스에 선언된 AuditAttribute들
var audits = type.GetCustomAttributes<AuditAttribute>(inherit: false);
foreach (var a in audits)
{
Console.WriteLine($"[Audit] Action={a.Action}, Level={a.Level}, Note={a.Note}");
}
// 메서드 및 매개변수 애트리뷰트
var method = type.GetMethod("Place");
if (method is not null)
{
bool hasAudit = method.IsDefined(typeof(AuditAttribute), inherit: false);
Console.WriteLine($"Method has [Audit]: {hasAudit}");
var param = method.GetParameters().First();
var between = param.GetCustomAttribute<BetweenAttribute>();
if (between is not null)
{
Console.WriteLine($"Priority range: {between.Min}..{between.Max}");
}
}
}
}
5. 자주 쓰는 내장 애트리뷰트
Obsolete로 사용 중단 경고를 내고, Conditional로 빌드 구성에 따라 호출 제거, Caller 정보로 호출 위치를 자동 주입할 수 있습니다.
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
public static class Samples
{
[Obsolete("Use NewApi instead", error: false)]
public static void OldApi() { }
[Conditional("DEBUG")] // DEBUG에서만 호출이 컴파일에 포함됨
public static void DebugLog(string message) => Console.WriteLine(message);
public static void Trace(
[CallerMemberName] string member = "",
[CallerFilePath] string file = "",
[CallerLineNumber] int line = 0)
{
Console.WriteLine($"Trace from {member} ({System.IO.Path.GetFileName(file)}:{line})");
}
}
// 사용 예시
class Demo
{
static void Main()
{
Samples.DebugLog("디버그 전용 로그"); // Release 빌드에서는 제거됨
Samples.Trace(); // 호출자 정보 자동 채워짐
var svc = new OrderService { Quantity = 10 };
svc.Place(3);
AttributeReader.Dump();
}
}
6. 실전 팁과 주의 사항
애트리뷰트는 메타데이터일 뿐이며, 실제 동작은 프레임워크나 리플렉션 코드가 해석할 때만 발생합니다. 따라서 무거운 로직을 애트리뷰트 생성자에 넣지 않는 것이 좋습니다.
AttributeUsage의 AllowMultiple, Inherited 설정은 검색 결과에 직접 영향을 줍니다. 상속된 멤버/타입에서 애트리뷰트를 읽을 때 inherit 인자를 적절히 지정하세요.
애트리뷰트 인수는 컴파일 타임 상수, typeof, nameof만 허용됩니다. 런타임 값을 전달할 수 없습니다.
리플렉션 비용이 민감한 경로에서는 Attribute.IsDefined, GetCustomAttribute(s) 결과를 캐싱하거나 소스 생성기/코드 생성으로 대체하세요.
트리밍(AOT/NativeAOT) 환경에서는 리플렉션으로 접근할 멤버가 제거되지 않도록 DynamicallyAccessedMembers 등의 보존 힌트를 고려하세요.
네이밍은 접미사 Attribute를 붙여 정의하고, 사용 시에는 생략 가능한 관례를 따릅니다. 마커 용도의 얇은 애트리뷰트를 만들어 파이프라인(예: 미들웨어, 소스 제너레이터)의 훅으로 활용하면 유지보수성이 좋아집니다.
'C#' 카테고리의 다른 글
| C# 동적 타입(dynamic)과 DLR 이해하기 (0) | 2026.04.17 |
|---|---|
| C# 인터롭(Interop)으로 네이티브 코드 호출하기 (0) | 2026.04.16 |
| C# 접근 제한자 (Access Modifiers) (0) | 2026.04.15 |
| C# 생성자와 소멸자 (0) | 2026.04.15 |
| C# ref, out, in 매개변수 한 번에 정리 (0) | 2026.04.14 |