다음을 통해 공유


.NET 종속성 주입

.NET은 클래스와 해당 종속성 간에 IoC(Inversion of Control) 를 달성하기 위한 기술인 DI(종속성 주입) 소프트웨어 디자인 패턴을 지원합니다. .NET의 종속성 주입은 구성, 로깅 및 옵션 패턴과 함께 프레임워크에 기본 제공된 부분입니다.

‘종속성’은 다른 개체가 종속된 개체입니다. 다음 MessageWriter 클래스에는 Write 다른 클래스가 의존할 수 있는 메서드가 있습니다.

public class MessageWriter : IMessageWriter
{
    public void Write(string message)
    {
        Console.WriteLine($"MessageWriter.Write(message: \"{message}\")");
    }
}

클래스는 해당 MessageWriter 메서드를 사용할 클래스의 Write 인스턴스를 만들 수 있습니다. 다음 예제에서, MessageWriter 클래스는 클래스의 Worker입니다.

public class Worker : BackgroundService
{
    private readonly MessageWriter _messageWriter = new();

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            _messageWriter.Write($"Worker running at: {DateTimeOffset.Now}");
            await Task.Delay(1_000, stoppingToken);
        }
    }
}

이 경우, Worker 클래스는 MessageWriter 클래스를 만들고 직접 의존합니다. 이와 같은 하드 코딩된 종속성은 문제가 되며 다음과 같은 이유로 피해야 합니다.

  • 다른 구현으로 MessageWriter을 바꾸기 위해, Worker 클래스를 수정해야 합니다.
  • 종속성을 포함하는 경우, MessageWriter 클래스도 그것들을 구성해야 합니다. 여러 클래스가 MessageWriter에 종속되어 있는 대형 프로젝트에서는 구성 코드가 앱 전체에 분산됩니다.
  • 이 구현은 단위 테스트하기가 어렵습니다. 앱에서 모의 또는 스텁 MessageWriter 클래스를 사용해야 하지만, 이 방법에서는 가능하지 않습니다.

개념

종속성 주입은 다음을 통해 하드 코딩된 종속성 문제를 해결합니다.

  • 인퍼테이스 또는 기본 클래스를 사용하여 종속성 구현을 추상화합니다.

  • 서비스 컨테이너의 종속성 등록

    .NET는 서비스 컨테이너인 IServiceProvider를 기본 제공합니다. 서비스는 일반적으로 앱 시작 시 등록되고 IServiceCollection에 추가됩니다. 모든 서비스가 추가되면 서비스 컨테이너를 만드는 데 사용합니다 BuildServiceProvider .

  • 서비스가 사용되는 클래스의 생성자에 서비스를 삽입합니다.

    프레임워크가 종속성의 인스턴스를 만들고 더 이상 필요하지 않으면 삭제하는 작업을 담당합니다.

팁 (조언)

종속성 주입 용어에서는 서비스가 일반적으로 IMessageWriter 같은 다른 개체에게 서비스를 제공하는 개체를 의미합니다. 이 서비스는 웹 서비스를 사용할 수 있지만 웹 서비스와는 관련이 없습니다.

예를 들어, IMessageWriter 인터페이스가 Write 메서드를 정의하고 있다고 가정합니다. 이 인터페이스는 이전에 표시된 구체적인 형식 MessageWriter으로 구현됩니다. 다음 샘플 코드는 구체적인 형식IMessageWriter으로 MessageWriter 서비스를 등록합니다. 이 메서드는 AddSingleton 싱글톤 수명 사용하여 서비스를 등록합니다. 즉, 앱이 종료될 때까지 처리되지 않습니다.

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddHostedService<Worker>();
builder.Services.AddSingleton<IMessageWriter, MessageWriter>();

using IHost host = builder.Build();

host.Run();

// <SnippetMW>
public class MessageWriter : IMessageWriter
{
    public void Write(string message)
    {
        Console.WriteLine($"MessageWriter.Write(message: \"{message}\")");
    }
}
// </SnippetMW>

// <SnippetIMW>
public interface IMessageWriter
{
    void Write(string message);
}
// </SnippetIMW>

// <SnippetWorker>
public sealed class Worker(IMessageWriter messageWriter) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            messageWriter.Write($"Worker running at: {DateTimeOffset.Now}");
            await Task.Delay(1_000, stoppingToken);
        }
    }
}

// </SnippetWorker>

앞의 코드 예제에서 강조 표시된 줄은 다음과 같습니다.

  • 호스트 앱 작성기 인스턴스를 만듭니다.
  • Worker로 등록하고 인터페이스를 IMessageWriter 클래스의 해당 구현을 사용하여 싱글톤 서비스로 등록하여 서비스를 구성합니다MessageWriter.
  • 호스트를 빌드하고 실행합니다.

호스트에는 종속성 주입 서비스 공급자가 포함되어 있습니다. 또한 Worker를 자동으로 인스턴스화하고 해당 IMessageWriter 구현을 인수로 제공하는 데 필요한 기타 모든 관련 서비스도 포함되어 있습니다.

DI 패턴을 사용하면 작업자 서비스는 구체적인 형식 MessageWriter을 사용하지 않고 구현하는 인터페이스만 IMessageWriter 사용합니다. 이 설계를 사용하면 작업자 서비스를 수정하지 않고도 작업자 서비스에서 사용하는 구현을 쉽게 변경할 수 있습니다. 작업자 서비스 또한 인스턴스를 생성하지MessageWriter. DI 컨테이너는 인스턴스를 만듭니다.

이제 MessageWriter프레임워크 제공 로깅 서비스를 사용하는 형식으로 교체하려고 합니다. LoggingMessageWriter 클래스는 생성자에서 ILogger<TCategoryName>를 요청하여 의존하도록 만드세요.

public class LoggingMessageWriter(
    ILogger<LoggingMessageWriter> logger) : IMessageWriter
{
    public void Write(string message) =>
        logger.LogInformation("Info: {Msg}", message);
}

MessageWriter에서 LoggingMessageWriter로 전환하려면, 이 새 AddSingleton 구현을 등록하기 위해 호출을 IMessageWriter로 업데이트합니다.

builder.Services.AddSingleton<IMessageWriter, LoggingMessageWriter>();

팁 (조언)

컨테이너는 ILogger<TCategoryName>을 활용하여 해결 하므로 생성된 모든(제네릭) 형식을 등록할 필요가 없습니다.

생성자 주입 동작

IServiceProvider 또는 기본 제공 서비스 컨테이너 ActivatorUtilities를 사용하여 서비스를 해결할 수 있습니다. ActivatorUtilities 는 컨테이너에 등록되지 않고 일부 프레임워크 기능과 함께 사용되는 개체를 만듭니다.

생성자에는 종속성 주입으로 제공되지 않는 인수를 사용할 수 있지만, 인수에 기본값을 할당해야 합니다.

IServiceProvider 또는 ActivatorUtilities이(가) 서비스를 해결할 때, 생성자 주입에는 공용 생성자가 필요합니다.

서비스를 해결할 때 ActivatorUtilities에서 생성자 주입은 적용 가능한 생성자가 하나만 존재해야 합니다. 생성자 오버로드가 지원되지만, 해당 인수가 모두 종속성 주입으로 처리될 수 있는 하나의 오버로드만 존재할 수 있습니다.

생성자 선택 규칙

형식에서 둘 이상의 생성자를 정의하는 경우 서비스 공급자는 사용할 생성자를 결정하는 논리를 포함합니다. 형식의 DI 확인이 가능한 대부분의 매개 변수가 있는 생성자가 선택됩니다. 다음 예제 서비스를 고려합니다.

public class ExampleService
{
    public ExampleService()
    {
    }

    public ExampleService(ILogger<ExampleService> logger)
    {
        // ...
    }

    public ExampleService(ServiceA serviceA, ServiceB serviceB)
    {
        // ...
    }
}

이전 코드에서는 로깅이 추가되었으며 서비스 공급자에서 확인할 수 있지만 ServiceA 형식 및 ServiceB 형식은 그렇지 않다고 가정합니다. 매개변수를 가진 ILogger<ExampleService> 생성자가 ExampleService 인스턴스를 해결합니다. 더 많은 매개 변수를 정의하는 생성자가 있더라도 ServiceAServiceB 형식은 DI에서 해결할 수 없습니다.

생성자를 발견할 때 모호성이 있으면 예외가 throw됩니다. 다음 C# 서비스 예를 살펴보세요.

경고

모호한 DI에서 해석 가능한 형식 매개 변수가 포함된 이 ExampleService 코드는 예외를 발생시킵니다. 이렇게 하지 마세요. "모호한 DI 확인 가능 형식"의 의미를 표시하기 위한 것입니다.

public class ExampleService
{
    public ExampleService()
    {
    }

    public ExampleService(ILogger<ExampleService> logger)
    {
        // ...
    }

    public ExampleService(IOptions<ExampleOptions> options)
    {
        // ...
    }
}

위 예제에는 세 가지 생성자가 있습니다. 첫 번째 생성자는 매개 변수가 없으며 서비스 공급자의 서비스가 필요하지 않습니다. 로깅 및 옵션이 모두 DI 컨테이너에 추가되었고 DI 확인 가능 서비스라고 가정합니다. DI 컨테이너가 ExampleService 유형을 해석하려고 시도할 때, 두 생성자가 모호하기 때문에 예외가 발생합니다.

대신 두 DI 확인 가능한 형식을 모두 허용하는 생성자를 정의하여 모호성을 방지합니다.

public class ExampleService
{
    public ExampleService()
    {
    }

    public ExampleService(
        ILogger<ExampleService> logger,
        IOptions<ExampleOptions> options)
    {
        // ...
    }
}

범위 유효성 검사

범위가 지정된 서비스는 해당 서비스를 만든 컨테이너에 의해 삭제됩니다. 범위가 지정된 서비스가 루트 컨테이너에 만들어지면 앱이 종료될 때 루트 컨테이너에서만 삭제되므로 서비스의 수명이 효과적으로 싱글톤 으로 승격됩니다. 서비스 범위의 유효성 검사는 BuildServiceProvider가 호출될 경우 이러한 상황을 감지합니다.

앱이 개발 환경에서 실행되고 CreateApplicationBuilder 를 호출하여 호스트를 빌드하는 경우 기본 서비스 공급자는 다음을 확인하는 검사를 수행합니다.

  • 범위가 지정된 서비스는 루트 서비스 공급자에서 확인되지 않습니다.
  • 범위가 지정된 서비스는 싱글톤에 삽입되지 않습니다.

범위 시나리오

IServiceScopeFactory는 항상 singleton으로 등록되지만, IServiceProvider는 포함하는 클래스의 수명에 따라 달라질 수 있습니다. 예를 들어 범위에서 서비스를 해결하고 해당 서비스가 IServiceProvider를 사용하는 경우, 그것은 범위 내 인스턴스입니다.

와 같은 IHostedService구현 BackgroundService내에서 범위 지정 서비스를 달성하려면 생성자 주입을 통해 서비스 종속성을 삽입하지 마세요. 대신 IServiceScopeFactory를 주입하고 범위를 만든 후 범위의 종속성을 확인하여 적절한 서비스 수명을 사용하도록 합니다.

namespace WorkerScope.Example;

public sealed class Worker(
    ILogger<Worker> logger,
    IServiceScopeFactory serviceScopeFactory)
    : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            using (IServiceScope scope = serviceScopeFactory.CreateScope())
            {
                try
                {
                    logger.LogInformation(
                        "Starting scoped work, provider hash: {hash}.",
                        scope.ServiceProvider.GetHashCode());

                    var store = scope.ServiceProvider.GetRequiredService<IObjectStore>();
                    var next = await store.GetNextAsync();
                    logger.LogInformation("{next}", next);

                    var processor = scope.ServiceProvider.GetRequiredService<IObjectProcessor>();
                    await processor.ProcessAsync(next);
                    logger.LogInformation("Processing {name}.", next.Name);

                    var relay = scope.ServiceProvider.GetRequiredService<IObjectRelay>();
                    await relay.RelayAsync(next);
                    logger.LogInformation("Processed results have been relayed.");

                    var marked = await store.MarkAsync(next);
                    logger.LogInformation("Marked as processed: {next}", marked);
                }
                finally
                {
                    logger.LogInformation(
                        "Finished scoped work, provider hash: {hash}.{nl}",
                        scope.ServiceProvider.GetHashCode(), Environment.NewLine);
                }
            }
        }
    }
}

위의 코드에서 앱이 실행되는 동안 백그라운드 서비스는 다음과 같습니다.

  • IServiceScopeFactory에 종속됩니다.
  • IServiceScope 다른 서비스를 해결하기 위한 항목을 만듭니다.
  • 사용할 범위가 지정된 서비스를 확인합니다.
  • 개체 처리에 대한 작업을 수행한 후 해당 개체를 릴레이하고 마지막으로 해당 개체를 처리된 것으로 표시합니다.

샘플 소스 코드에서 IHostedService 구현이 범위가 지정된 서비스 수명의 이점을 활용하는 방법을 확인할 수 있습니다.

키 지정된 서비스

서비스를 등록하고 키에 따라 조회를 수행할 수 있습니다. 즉, 여러 서비스를 다른 키로 등록하고 조회에 이 키를 사용할 수 있습니다.

예를 들어, 인터페이스 IMessageWriter(MemoryMessageWriterQueueMessageWriter)의 서로 다른 구현이 있는 경우를 생각해 보세요.

키를 매개 변수로 지원하는 서비스 등록 메서드(앞서 본)의 오버로드를 사용하여 이러한 서비스를 등록할 수 있습니다.

services.AddKeyedSingleton<IMessageWriter, MemoryMessageWriter>("memory");
services.AddKeyedSingleton<IMessageWriter, QueueMessageWriter>("queue");

keystring에만 제한되지 않습니다. 원하는 key을 사용할 수 있으며, 단 objectEquals를 올바르게 구현해야 합니다.

IMessageWriter를 사용하는 클래스의 생성자에서 FromKeyedServicesAttribute를 추가하여 확인할 서비스의 키를 지정합니다.

public class ExampleService
{
    public ExampleService(
        [FromKeyedServices("queue")] IMessageWriter writer)
    {
        // Omitted for brevity...
    }
}

KeyedService.AnyKey 속성

이 속성은 KeyedService.AnyKey 키 지정된 서비스를 사용하기 위한 특수 키를 제공합니다. 모든 키와 일치하는 대체(fallback)로 사용하여 KeyedService.AnyKey 서비스를 등록할 수 있습니다. 명시적 등록이 없는 키에 대한 기본 구현을 제공하려는 경우에 유용합니다.

var services = new ServiceCollection();

// Register a fallback cache for any key.
services.AddKeyedSingleton<ICache>(KeyedService.AnyKey, (sp, key) =>
{
    // Create a cache instance based on the key.
    return new DefaultCache(key?.ToString() ?? "unknown");
});

// Register a specific cache for the "premium" key.
services.AddKeyedSingleton<ICache>("premium", new PremiumCache());

var provider = services.BuildServiceProvider();

// Requesting with "premium" key returns PremiumCache.
var premiumCache = provider.GetKeyedService<ICache>("premium");
Console.WriteLine($"Premium key: {premiumCache}");

// Requesting with any other key uses the AnyKey fallback.
var basicCache = provider.GetKeyedService<ICache>("basic");
Console.WriteLine($"Basic key: {basicCache}");

var standardCache = provider.GetKeyedService<ICache>("standard");
Console.WriteLine($"Standard key: {standardCache}");

앞의 예에서:

  • ICache 로 요청하면 "premium" 인스턴스가 반환됩니다PremiumCache.
  • ICache 및 다른 키(예: "basic" 또는 "standard")로 요청하면 DefaultCache 대체를 사용하여 AnyKey 새로 생성됩니다.

중요합니다

.NET 10부터 GetKeyedService()KeyedService.AnyKey와 함께 호출하면 InvalidOperationException 예외가 발생합니다. 왜냐하면 AnyKey는 등록 대체(fallback)로 의도된 것이지 쿼리 키가 아니기 때문입니다. 자세한 내용은 AnyKey를 사용하여 GetKeyedService() 및 GetKeyedServices()의 문제 해결을 참조하세요.

참고하십시오