🏗 C# DI 컨테이너 직접 구현하기 (Reflection 종합편)
ASP.NET Core를 사용하다 보면 너무 자연스럽게 DI를 사용한다.
services.AddSingleton<ILogger, FileLogger>();
하지만 이 한 줄 뒤에서 어떤 일이 벌어지는지 이해하지 못한다면, DI는 단순한 마법 상자에 불과하다.
이번 글에서는 그 마법을 직접 구현해본다.
1️⃣ DI(Dependency Injection)란 무엇인가?
DI는 객체가 직접 의존성을 생성하지 않고, 외부에서 주입받는 구조를 말한다.
❌ DI가 없는 코드
class OrderService
{
private readonly FileLogger _logger = new FileLogger();
}
✅ DI 적용 코드
class OrderService
{
private readonly ILogger _logger;
public OrderService(ILogger logger)
{
_logger = logger;
}
}
📘 Microsoft 공식 문서: .NET Dependency Injection 개요
종속성 주입 - .NET
.NET 앱 내에서 종속성 주입을 사용하는 방법을 알아봅니다. C#에서 서비스 수명을 정의하고 종속성을 표현하는 방법을 알아보세요.
learn.microsoft.com
2️⃣ 우리가 만들 DI 컨테이너의 목표
- Interface → 구현체 매핑
- 생성자 주입
- Reflection 기반 객체 생성
- 성능 최적화 (Expression Tree)
3️⃣ 기본 구조 설계
public interface IServiceContainer
{
void Register<TService, TImpl>();
TService Resolve<TService>();
}
4️⃣ 서비스 등록 (Register)
class ServiceContainer : IServiceContainer
{
private readonly Dictionary<Type, Type> _registrations = new();
public void Register<TService, TImpl>()
{
_registrations[typeof(TService)] = typeof(TImpl);
}
public TService Resolve<TService>()
{
return (TService)Resolve(typeof(TService));
}
private object Resolve(Type serviceType)
{
Type implType = _registrations[serviceType];
return CreateInstance(implType);
}
}
5️⃣ Reflection으로 생성자 분석
DI 컨테이너의 핵심은 생성자의 파라미터를 분석하는 것이다.
ConstructorInfo ctor = implType.GetConstructors().First();
ParameterInfo[] parameters = ctor.GetParameters();
📘 Microsoft 공식 문서: ConstructorInfo 클래스
ConstructorInfo Class (System.Reflection)
Discovers the attributes of a class constructor and provides access to constructor metadata.
learn.microsoft.com
6️⃣ 재귀적 의존성 해결
private object CreateInstance(Type type)
{
var ctor = type.GetConstructors().First();
var parameters = ctor.GetParameters();
object[] args = parameters
.Select(p => Resolve(p.ParameterType))
.ToArray();
return Activator.CreateInstance(type, args);
}
이 구조 덕분에 의존성이 여러 단계여도 자동으로 해결된다.
7️⃣ 문제점: Reflection 성능
이 방식은 작동은 하지만,
- Reflection 호출 반복
- Activator 사용
- 서버 환경에서 성능 저하
그래서 실무 DI 컨테이너는 Expression Tree를 사용한다.
8️⃣ Expression Tree로 팩토리 생성
static Func<IServiceContainer, object> CreateFactory(Type type)
{
var containerParam = Expression.Parameter(typeof(IServiceContainer), "c");
var ctor = type.GetConstructors().First();
var args = ctor.GetParameters()
.Select(p =>
Expression.Convert(
Expression.Call(
containerParam,
nameof(IServiceContainer.Resolve),
new[] { p.ParameterType }
),
p.ParameterType))
.ToArray();
var newExpr = Expression.New(ctor, args);
var lambda = Expression.Lambda<Func<IServiceContainer, object>>(
Expression.Convert(newExpr, typeof(object)),
containerParam);
return lambda.Compile();
}
9️⃣ Factory 캐싱
private readonly Dictionary<Type, Func<IServiceContainer, object>> _factories = new();
한 번만 Reflection + Compile 이후에는 일반 메서드 호출 수준의 성능
🔟 최종 Resolve 구현
private object Resolve(Type serviceType)
{
if (!_factories.TryGetValue(serviceType, out var factory))
{
Type implType = _registrations[serviceType];
factory = CreateFactory(implType);
_factories[serviceType] = factory;
}
return factory(this);
}
1️⃣1️⃣ DI 컨테이너 완성 사용 예
container.Register<ILogger, ConsoleLogger>();
container.Register<OrderService, OrderService>();
var service = container.Resolve<OrderService>();
이 순간:
- Reflection
- Delegate
- Expression Tree
가 모두 결합되어 동작한다.
1️⃣2️⃣ 실제 DI 컨테이너와의 연결
| 기능 | 우리가 만든 컨테이너 | ASP.NET Core |
|---|---|---|
| Reflection | O | O |
| Expression Tree | O | O |
| Scope | X | O |
| Lifetime | X | O |
1️⃣3️⃣ 정리
- DI는 마법이 아니다
- Reflection은 도구다
- Delegate는 연결 고리다
- Expression Tree는 성능 해법이다
반응형
'C#' 카테고리의 다른 글
| Event 메모리 누수와 WeakEvent 패턴 완전 분석 (0) | 2026.02.14 |
|---|---|
| Delegate vs Interface - 설계 관점에서의 선택 기준 (0) | 2026.02.12 |
| C# Reflection 성능 최적화 - Expression Tree와 IL Emit (0) | 2026.02.10 |
| C# 비동기 / 병렬 처리 + GC 최적화 실전 가이드 (0) | 2026.02.08 |
| C# 개발자를 위한 고급 메모리 관리: GC를 넘어서 Span과 Memory까지 (0) | 2025.12.14 |