본문 바로가기

C#

DI 컨테이너 직접 구현하기 - Reflection 종합편

 

🏗 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는 성능 해법이다
반응형