Menu

C# 리플렉션과 특성: typeof, GetType, 사용자 정의 특성

C#에서 리플렉션이 동작하는 방식: typeof와 GetType으로 얻는 Type 객체, 속성 읽기와 설정, 이름으로 메서드 호출, 인스턴스 만들기, 그리고 특성: Obsolete 같은 기본 제공 특성, 직접 선언하기, 실행 시점에 읽기. 리플렉션의 비용도 알아봅니다.

이 페이지에는 실행 가능한 에디터가 있습니다 - 편집하고 실행하면 결과를 바로 볼 수 있습니다.

리플렉션은 프로그램이 실행되는 동안 타입을 조사하게 해 줍니다. 클래스에 어떤 속성이 있는지, 그 값이 무엇인지, 어떤 메서드가 있는지, 어떤 특성이 붙어 있는지입니다. 특성은 나머지 절반입니다. [Obsolete]나 [JsonPropertyName("id")]처럼 대괄호로 쓰는 선언적 태그로, 컴파일러나 어떤 코드가 리플렉션으로 읽기 전까지는 그 자체로 아무 의미가 없습니다. 직렬 변환기, ORM, 검증 라이브러리, 테스트 프레임워크, ASP.NET 라우팅이 모두 이렇게 동작합니다.

대부분의 애플리케이션 코드는 리플렉션을 직접 쓸 일이 드물지만, 동작 방식을 알면 프레임워크의 많은 동작이 이해됩니다.

Type 객체: typeof와 GetType

모든 것은 System.Type에서 시작합니다. 얻는 방법은 두 가지입니다:

출력:

Employee
Manager
Employee
False
True
True
System.Int32
False
Name

기억할 차이: typeof(X)는 코드를 쓸 때 아는 타입을 가리키고, obj.GetType()은 객체에게 실제로 무엇인지 묻습니다. GetType() == typeof(Employee) 비교는 파생 클래스에서 실패하는 정확한 일치이며, 보통 원하는 것이 아닙니다. is와 IsAssignableFrom은 상속을 존중합니다. nameof는 비슷해 보이지만 리플렉션이 전혀 아닙니다. 컴파일러가 문자열 상수로 바꿉니다.

null 참조에 대한 GetType()은 물어볼 객체가 없으므로 NullReferenceException을 던집니다. 박싱된 nullable 값 형식에서는 기반 타입을 반환합니다: ((int?)5).GetType()은 System.Int32입니다.

속성 읽기와 설정

GetProperties()는 타입의 public 속성을 PropertyInfo 객체로 나열하며, 각각은 주어진 인스턴스에서 값을 읽고 쓸 수 있습니다:

출력:

Name   String   = Mug
Price  Decimal  = 8.50
Stock  Int32    = 12
7.90
True
SUP-77

이것이 보여 주는 세 가지:

  • GetValue는 object를 반환하므로 값 형식은 박싱되어 돌아오고, 쓰려면 캐스트해야 합니다.
  • 존재하지 않는 이름으로 GetProperty를 호출하면 null을 반환하고, 그 위의 다음 호출이 NullReferenceException을 던집니다. 쓰기 전에 확인하세요.
  • BindingFlags.NonPublic | BindingFlags.Instance는 private 멤버에 닿습니다. 도구와 테스트에서는 정당하지만, 캡슐화를 우회하고 클래스를 리팩터링하면 조용히 깨집니다.

이 루프가 본질적으로 CSV 내보내기나 객체를 JSON으로 바꾸는 직렬 변환기가 동작하는 방식입니다. 속성을 훑으며 각 값을 읽고 서식화합니다.

이름으로 메서드 호출하기와 객체 만들기

GetMethod는 메서드를 찾고, Invoke는 인수 배열로 그것을 호출합니다. Activator.CreateInstance는 Type으로부터 객체를 만들며, 플러그인 시스템과 DI 컨테이너가 실행 시점에 고른 타입을 만드는 방법입니다:

출력:

60.00
Decimal WithTax(1 parameters)
String Describe(0 parameters)
True

DeclaredOnly는 목록을 클래스 자체에 선언된 멤버로 제한합니다. 없으면 GetMethods가 object의 ToString, Equals, GetHashCode, GetType도 반환합니다. Type.GetType("Name")에는 네임스페이스로 한정된 이름이 필요하고, 다른 어셈블리의 타입이라면 어셈블리 이름도 필요합니다("MyApp.Plugins.Csv, MyApp.Plugins").

호출한 메서드가 예외를 던지면 Invoke는 그 예외를 TargetInvocationException으로 감싸며, 원래 예외는 InnerException에 있습니다.

특성: 컴파일러와 프레임워크가 읽는 태그

특성은 설명하는 대상 앞에 대괄호로 씁니다. 프레임워크가 많은 특성을 정의하며, 그중 몇 가지는 컴파일러가 하는 일을 바꿉니다:

public class OrderService
{
    [Obsolete("Use PlaceOrderAsync instead.")]
    public void PlaceOrder(Order order) { }
    // Every call site: warning CS0618: 'OrderService.PlaceOrder(Order)' is obsolete: 'Use PlaceOrderAsync instead.'
    // [Obsolete("...", true)] makes it error CS0619 instead.

    [Conditional("DEBUG")]
    public void Trace(string message) => Console.WriteLine(message);
    // Calls to Trace are removed entirely from builds without the DEBUG symbol.
}

[Flags] enum Channels { None = 0, Email = 1, Sms = 2 }   // changes how ToString formats combinations
[Serializable] class Snapshot { }                     // marks a type for legacy binary serialization

다른 특성들은 라이브러리가 실행 시점에 읽습니다. [JsonPropertyName]과 [JsonIgnore]는 System.Text.Json이, [Required]와 [MaxLength]는 ASP.NET Core 모델 검증과 Entity Framework가, [HttpGet("orders/{id}")]는 ASP.NET 라우팅이, [Fact]와 [Test]는 테스트 실행기가 읽습니다. 특성 자체는 아무것도 하지 않고, 그것을 찾는 코드가 일합니다.

ObsoleteAttribute라는 이름은 적용할 때 [Obsolete]로 줄어듭니다. 관례상 모든 특성 클래스 이름은 Attribute로 끝나며, C#은 접미사를 생략하게 해 줍니다.

사용자 정의 특성 선언하고 읽기

사용자 정의 특성은 Attribute에서 파생된 클래스입니다. [AttributeUsage]는 무엇에 적용할 수 있는지 말합니다. 생성자 매개변수는 위치 인수가 되고, public 설정 가능 속성은 명명된 인수가 됩니다:

출력:

Username must be at most 20 characters
Email is required
Keep the city code short
0

이것이 ASP.NET Core 모델 검증이 System.ComponentModel.DataAnnotations로 하는 일의 축소판입니다. 특성 인수는 어셈블리의 메타데이터에 저장되므로 컴파일 시점 상수(숫자, 문자열, typeof(...), 열거형 값, 또는 이들의 배열)여야 합니다. GetCustomAttribute<T>()는 System.Reflection의 확장 메서드이며, 특성이 있는지만 알면 될 때는 IsDefined(typeof(T))도 있습니다.

리플렉션의 비용

리플렉션은 유연성을 위해 속도와 안전성을 내줍니다:

  • 속도. 이름으로 멤버를 찾아 Invoke나 GetValue로 호출하는 것은 직접 호출보다 훨씬 느리고 값 형식을 박싱합니다. 반복해서 쓴다면 PropertyInfo나 MethodInfo를 한 번 찾아 보관하거나, Delegate.CreateDelegate나 MethodInfo.CreateDelegate로 델리게이트로 바꿔서 호출하세요.
  • 안전성. 철자가 틀린 이름이나 바뀐 시그니처는 잘 컴파일되고 실행 시점에 실패합니다. 이름 변경이 잡히도록 가능한 곳에서는 문자열 "Price" 대신 nameof(Product.Price)를 쓰세요.
  • 트리밍과 AOT. 트리밍된 앱과 Native AOT 앱은 아무것도 쓰지 않는 것처럼 보이는 멤버를 제거하는데, 리플렉션은 그 분석으로부터 사용을 숨깁니다. 최신 라이브러리(System.Text.Json, GeneratedRegex, 로깅)는 같은 일을 컴파일 시점에 하는 소스 생성기로 옮겨 가고 있습니다.

리플렉션은 프로그램에서 진짜로 타입을 미리 알 수 없는 부분에 쓰세요. 플러그인, 범용 도구, 직렬 변환기, 테스트 헬퍼입니다. 타입을 안다면 일반 코드, 제네릭, 인터페이스가 더 빠르고 컴파일러가 검사해 줍니다.

흔한 실수

  • 기본 클래스를 검사하려고 GetType() == typeof(Base) 쓰기. 파생 타입에서 실패합니다. is나 IsAssignableFrom을 쓰세요.
  • null을 확인하지 않기. GetProperty, GetMethod, Type.GetType은 일치하는 것이 없으면 null을 반환합니다.
  • 캐시 없이 성능이 중요한 루프에서 리플렉션 쓰기. MemberInfo를 캐시하거나 델리게이트를 컴파일하세요.
  • Invoke에서 엉뚱한 예외 잡기. 진짜 예외는 TargetInvocationException의 InnerException입니다.
  • 멤버 이름에 마법 문자열 쓰기. nameof를 쓰세요.

자주 묻는 질문

C#에서 리플렉션이란 무엇인가요?

리플렉션은 프로그램이 실행 중에 타입을 조사하는 능력입니다. 클래스의 속성과 메서드를 나열하고, 이름으로 값을 읽고 설정하고, 메서드를 호출하고, 인스턴스를 만들고, 특성을 읽습니다. System.Reflection에 있으며 Type 객체에서 시작합니다. 직렬 변환기, ORM, 의존성 주입 컨테이너, 테스트 프레임워크가 그 위에 만들어져 있습니다.

C#에서 typeof와 GetType의 차이는 무엇인가요?

typeof(Customer)는 타입 이름으로부터 컴파일 시점에 결정되며 객체가 필요 없습니다. obj.GetType()은 실행 시점에 인스턴스에서 호출되며, 변수의 선언된 타입보다 더 파생된 타입일 수 있는 객체의 실제 타입을 반환합니다. Animal a = new Dog();에서 a.GetType()은 Dog입니다. null 참조에 대한 GetType()은 NullReferenceException을 던집니다.

C#에서 이름으로 속성 값을 얻으려면 어떻게 하나요?

obj.GetType().GetProperty("Price")는 PropertyInfo(그런 public 속성이 없으면 null)를 반환하고, .GetValue(obj)는 그 값을 object로 읽습니다. .SetValue(obj, value)는 값을 씁니다. 조회가 비싼 부분이므로 루프에서 이렇게 한다면 PropertyInfo를 캐시하세요.

C#에서 사용자 정의 특성은 어떻게 만드나요?

System.Attribute에서 파생된 클래스를 선언하고, 이름에 Attribute 접미사를 붙이고, [AttributeUsage]로 쓸 수 있는 곳을 표시하세요: [AttributeUsage(AttributeTargets.Property)] class MaxLengthAttribute : Attribute { public int Length { get; } public MaxLengthAttribute(int length) { Length = length; } }. [MaxLength(50)]로 적용하고 property.GetCustomAttribute<MaxLengthAttribute>()로 읽습니다.

C#에서 Obsolete 특성은 무엇을 하나요?

멤버에 붙인 [Obsolete("Use PlaceOrderAsync instead")]는 컴파일러가 모든 호출 지점에서 여러분의 메시지와 함께 경고 CS0618을 내게 합니다. [Obsolete("...", true)]는 경고를 오류 CS0619로 바꿉니다. 라이브러리가 호출하는 쪽을 하루아침에 깨뜨리지 않고 API를 은퇴시키는 방법입니다.

C#에서 리플렉션은 느린가요?

직접 호출과 비교하면 그렇습니다. 이름으로 멤버를 찾아 MethodInfo.Invoke나 PropertyInfo.GetValue로 호출하는 것은 보통 수십에서 수백 배 느리고, 값 형식을 박싱합니다. 시작, 설정, 가끔 쓰는 용도에는 괜찮습니다. 성능이 중요한 경로에는 MemberInfo를 캐시하거나, 델리게이트를 한 번 만들거나, 대신 제네릭이나 소스 생성기를 쓰세요.

Coddy programming languages illustration

Coddy로 코딩 배우기

시작하기