Reflection은 런타임에 타입 정보를 읽고 조작할 수 있게 해주는 강력한 기능입니다. 테스트나 마이그레이션 상황에서 Private 멤버에 접근해야 할 때 유용하지만, 남용은 유지보수성과 성능을 해치므로 신중히 사용해야 합니다.
1. 예제 클래스
using System;
using System.Reflection;
public class User
{
private int _age = 10;
private string Secret { get; set; } = "token";
private string Combine(string prefix) => $"{prefix}-{_age}-{Secret}";
}
2. Private 필드/속성 읽기·쓰기
var user = new User();
var type = typeof(User);
// 필드 접근
var field = type.GetField("_age", BindingFlags.Instance | BindingFlags.NonPublic);
int age = (int)field!.GetValue(user)!; // 10
field.SetValue(user, 20);
// 속성 접근
var prop = type.GetProperty("Secret", BindingFlags.Instance | BindingFlags.NonPublic);
string secret = (string)prop!.GetValue(user)!; // "token"
prop.SetValue(user, "api");
3. Private 메서드 호출
var method = type.GetMethod("Combine", BindingFlags.Instance | BindingFlags.NonPublic);
string result = (string)method!.Invoke(user, new object[] { "P" })!; // "P-20-api"
Invoke는 박싱/언박싱과 매개변수 배열 생성이 발생하여 느립니다. 자주 호출한다면 델리게이트를 만들어 캐시하는 것이 좋습니다.
4. 성능 최적화: CreateDelegate 캐싱
// 오픈 인스턴스 델리게이트: 첫 매개변수로 대상 인스턴스를 받습니다.
var mi = typeof(User).GetMethod("Combine", BindingFlags.Instance | BindingFlags.NonPublic)!;
var combine = (Func<User, string, string>)mi.CreateDelegate(typeof(Func<User, string, string>));
var r1 = combine(user, "P"); // "P-20-api"
// 클로즈드 인스턴스 델리게이트: 대상 인스턴스를 고정합니다.
var combineForUser = (Func<string, string>)mi.CreateDelegate(typeof(Func<string, string>), user);
var r2 = combineForUser("Q"); // "Q-20-api"
5. 테스트에서의 활용
최선은 퍼블릭 API를 통해 테스트하는 것입니다. 다만, 레거시 코드나 리팩터링 전 과도기에 Private 로직 검증이 필요할 수 있습니다.
// xUnit 예제
using System;
using System.Reflection;
using Xunit;
public class UserTests
{
[Fact]
public void Combine_내부로직_검증()
{
var user = new User();
var t = typeof(User);
// 준비: 내부 상태 수정
t.GetField("_age", BindingFlags.Instance | BindingFlags.NonPublic)!
.SetValue(user, 42);
t.GetProperty("Secret", BindingFlags.Instance | BindingFlags.NonPublic)!
.SetValue(user, "key");
// 빠른 호출을 위해 델리게이트 사용
var mi = t.GetMethod("Combine", BindingFlags.Instance | BindingFlags.NonPublic)!;
var combine = (Func<User, string, string>)mi.CreateDelegate(typeof(Func<User, string, string>));
var result = combine(user, "T");
Assert.Equal("T-42-key", result);
}
}
MSTest를 사용한다면 PrivateObject로 간단히 호출할 수도 있습니다.
// MSTest 예제
using Microsoft.VisualStudio.TestTools.UnitTesting;
[TestClass]
public class UserMsTests
{
[TestMethod]
public void Combine_PrivateObject_호출()
{
var user = new User();
var po = new PrivateObject(user);
var result = (string)po.Invoke("Combine", new object[] { "P" });
Assert.IsTrue(result.StartsWith("P-"));
}
}
6. 대안: InternalsVisibleTo로 테스트 용이성 확보
Private을 그대로 두고 반사로 접근하는 대신, 테스트 전용으로 internal로 낮추고 어셈블리 친구 관계를 맺는 방법이 더 안정적입니다.
// 프로젝트의 AssemblyInfo.cs (또는 소스 상단)
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("MyProject.Tests")]
// 또는 해당 멤버를 internal로 바꾸고 테스트에서 직접 참조합니다.
7. 실패 원인과 디버그 팁
자주 겪는 문제와 해결 방법입니다.
- BindingFlags 누락: Instance/Static, NonPublic/Public 조합이 정확해야 합니다.
- 멤버명 오타: nameof를 활용해 리팩터링에 안전하게 유지합니다.
- null 반환: 계층 상위 타입의 멤버일 수 있습니다. type.BaseType을 순회하거나 FlattenHierarchy 옵션을 검토합니다(Static에만 영향).
- 접근 제한: 트리밍/AOT 환경에서 반사가 제거될 수 있습니다. 필요한 멤버에 보존 특성이나 DynamicDependency를 지정합니다.
- 성능: Invoke는 느립니다. CreateDelegate로 캐시하거나 한번만 반사로 찾아 델리게이트를 재사용합니다.
8. 보안과 운영 주의사항
- 캡슐화 위반: 테스트 외 운영 코드에서 Private 반사는 유지보수 비용을 키웁니다.
- 런타임 정책: .NET Core/5+는 기본적으로 Private 반사를 허용하지만, 샌드박스나 일부 호스팅 환경에 따라 제한될 수 있습니다.
- 트리밍/AOT: PublishTrimmed나 NativeAOT 사용 시 반사 대상 멤버가 제거될 수 있습니다.
9. 간단 유틸리티 헬퍼
public static class Reflect
{
public static FieldInfo Field<T>(string name) =>
typeof(T).GetField(name, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)
?? throw new MissingFieldException(typeof(T).FullName, name);
public static PropertyInfo Property<T>(string name) =>
typeof(T).GetProperty(name, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)
?? throw new MissingMemberException(typeof(T).FullName, name);
public static MethodInfo Method<T>(string name, params Type[] types) =>
typeof(T).GetMethod(name, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, null, types, null)
?? throw new MissingMethodException(typeof(T).FullName, name);
}
// 사용 예
var fAge = Reflect.Field<User>("_age");
fAge.SetValue(user, 33);
요약: 테스트에서는 일시적인 검증을 위해 Reflection으로 Private 멤버에 접근할 수 있습니다. 단, 장기적으로는 퍼블릭 계약을 통해 테스트하거나, internal + InternalsVisibleTo로 전환하는 것이 유지보수에 유리합니다.
'C#' 카테고리의 다른 글
| C# ConcurrentDictionary를 이용한 안전한 데이터 공유 (1) | 2026.07.01 |
|---|---|
| C# 인터페이스 다중 상속 시 충돌 해결 전략 (0) | 2026.06.30 |
| C# 이벤트 vs 콜백(CallBack) 비교와 선택 기준 (0) | 2026.06.26 |
| C# sealed 키워드로 상속 제한하기 (0) | 2026.06.26 |
| C# 인스턴스 멤버와 정적 멤버의 메모리 차이 (0) | 2026.06.25 |