자습서: .NET에서 종속성 주입 사용

이 자습서에서는 .NET에서 DI(종속성 주입)를 사용하는 방법을 보여 줍니다. Microsoft 확장을 사용하면 DI는 서비스를 추가하고 IServiceCollection에서 구성하여 관리됩니다. IHost 인터페이스는 등록된 모든 서비스의 컨테이너 역할을 하는 IServiceProvider 인스턴스를 공개합니다.

이 자습서에서는 다음 작업 방법을 알아봅니다.

  • 종속성 주입을 사용하는 .NET 콘솔 앱 만들기
  • 제네릭 호스트 빌드 및 구성
  • 여러 인터페이스 및 해당 구현 작성
  • DI를 위해 서비스 수명 및 범위 지정 사용

사전 요구 사항

  • .NET Core 3.1 SDK 이상
  • 새 .NET 애플리케이션 만들기 및 NuGet 패키지 설치에 관한 지식

새 콘솔 애플리케이션 만들기

dotnet new 명령 또는 IDE 새 프로젝트 마법사를 사용하여 ConsoleDI.Example 라는 새 .NET 콘솔 애플리케이션을 만듭니다. Microsoft.Extensions.Hosting NuGet 패키지를 프로젝트에 추가합니다.

새 콘솔 앱 프로젝트 파일은 다음과 유사합니다.

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>true</ImplicitUsings>
    <RootNamespace>ConsoleDI.Example</RootNamespace>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
  </ItemGroup>

</Project>

중요

이 예제에서는 앱을 빌드하고 실행하기 위해 Microsoft.Extensions.Hosting NuGet 패키지가 필요합니다. 일부 메타패키지에는 Microsoft.Extensions.Hosting 패키지가 포함될 수 있습니다. 이 경우 명시적 패키지 참조가 필요하지 않습니다.

인터페이스 추가

이 샘플 앱에서는 종속성 주입이 서비스 수명을 처리하는 방법을 알아봅니다. 다양한 서비스 수명을 나타내는 여러 인터페이스를 만듭니다. 프로젝트 루트 디렉터리에 다음 인터페이스를 추가합니다.

IReportServiceLifetime.cs

using Microsoft.Extensions.DependencyInjection;

namespace ConsoleDI.Example;

public interface IReportServiceLifetime
{
    Guid Id { get; }

    ServiceLifetime Lifetime { get; }
}

IReportServiceLifetime 인터페이스는 다음을 정의합니다.

  • 서비스의 고유 식별자를 나타내는 Guid Id 속성
  • 서비스 수명을 나타내는 ServiceLifetime 속성

IExampleTransientService.cs

using Microsoft.Extensions.DependencyInjection;

namespace ConsoleDI.Example;

public interface IExampleTransientService : IReportServiceLifetime
{
    ServiceLifetime IReportServiceLifetime.Lifetime => ServiceLifetime.Transient;
}

IExampleScopedService.cs

using Microsoft.Extensions.DependencyInjection;

namespace ConsoleDI.Example;

public interface IExampleScopedService : IReportServiceLifetime
{
    ServiceLifetime IReportServiceLifetime.Lifetime => ServiceLifetime.Scoped;
}

IExampleSingletonService.cs

using Microsoft.Extensions.DependencyInjection;

namespace ConsoleDI.Example;

public interface IExampleSingletonService : IReportServiceLifetime
{
    ServiceLifetime IReportServiceLifetime.Lifetime => ServiceLifetime.Singleton;
}

IReportServiceLifetime의 모든 하위 인터페이스는 기본값으로 IReportServiceLifetime.Lifetime을 명시적으로 구현합니다. 예를 들어 IExampleTransientServiceServiceLifetime.Transient 값을 사용하여 IReportServiceLifetime.Lifetime을 명시적으로 구현합니다.

기본 구현 추가

예제 구현은 모두 Guid.NewGuid() 결과를 사용하여 Id 속성을 초기화합니다. 다양한 서비스에 대한 다음 기본 구현 클래스를 프로젝트 루트 디렉터리에 추가합니다.

ExampleTransientService.cs

namespace ConsoleDI.Example;

internal sealed class ExampleTransientService : IExampleTransientService
{
    Guid IReportServiceLifetime.Id { get; } = Guid.NewGuid();
}

ExampleScopedService.cs

namespace ConsoleDI.Example;

internal sealed class ExampleScopedService : IExampleScopedService
{
    Guid IReportServiceLifetime.Id { get; } = Guid.NewGuid();
}

ExampleSingletonService.cs

namespace ConsoleDI.Example;

internal sealed class ExampleSingletonService : IExampleSingletonService
{
    Guid IReportServiceLifetime.Id { get; } = Guid.NewGuid();
}

각 구현은 internal sealed로 정의되고 해당 인터페이스를 구현합니다. 예를 들어 ExampleSingletonServiceIExampleSingletonService를 구현합니다.

DI가 필요한 서비스 추가

콘솔 앱에 서비스 역할을 하는 다음 서비스 수명 보고자 클래스를 추가합니다.

ServiceLifetimeReporter.cs

namespace ConsoleDI.Example;

internal sealed class ServiceLifetimeReporter(
    IExampleTransientService transientService,
    IExampleScopedService scopedService,
    IExampleSingletonService singletonService)
{
    public void ReportServiceLifetimeDetails(string lifetimeDetails)
    {
        Console.WriteLine(lifetimeDetails);

        LogService(transientService, "Always different");
        LogService(scopedService, "Changes only with lifetime");
        LogService(singletonService, "Always the same");
    }

    private static void LogService<T>(T service, string message)
        where T : IReportServiceLifetime =>
        Console.WriteLine(
            $"    {typeof(T).Name}: {service.Id} ({message})");
}

ServiceLifetimeReporter는 앞에서 언급한 각 서비스 인터페이스인 IExampleTransientService, IExampleScopedService, IExampleSingletonService가 필요한 생성자를 정의합니다. 개체는 소비자가 지정된 lifetimeDetails 매개 변수를 사용하여 서비스에 대해 보고할 수 있는 단일 메서드를 공개합니다. 호출될 때 ReportServiceLifetimeDetails 메서드는 각 서비스의 고유 식별자를 서비스 수명 메시지와 함께 기록합니다. 로그 메시지는 서비스 수명을 시각화하는 데 도움이 됩니다.

DI를 위해 서비스 등록

다음 코드로 Program.cs를 업데이트합니다.

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using ConsoleDI.Example;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddTransient<IExampleTransientService, ExampleTransientService>();
builder.Services.AddScoped<IExampleScopedService, ExampleScopedService>();
builder.Services.AddSingleton<IExampleSingletonService, ExampleSingletonService>();
builder.Services.AddTransient<ServiceLifetimeReporter>();

using IHost host = builder.Build();

ExemplifyServiceLifetime(host.Services, "Lifetime 1");
ExemplifyServiceLifetime(host.Services, "Lifetime 2");

await host.RunAsync();

static void ExemplifyServiceLifetime(IServiceProvider hostProvider, string lifetime)
{
    using IServiceScope serviceScope = hostProvider.CreateScope();
    IServiceProvider provider = serviceScope.ServiceProvider;
    ServiceLifetimeReporter logger = provider.GetRequiredService<ServiceLifetimeReporter>();
    logger.ReportServiceLifetimeDetails(
        $"{lifetime}: Call 1 to provider.GetRequiredService<ServiceLifetimeReporter>()");

    Console.WriteLine("...");

    logger = provider.GetRequiredService<ServiceLifetimeReporter>();
    logger.ReportServiceLifetimeDetails(
        $"{lifetime}: Call 2 to provider.GetRequiredService<ServiceLifetimeReporter>()");

    Console.WriteLine();
}

services.Add{LIFETIME}<{SERVICE}> 확장 메서드는 서비스를 추가(및 잠재적으로 구성)합니다. 앱에서 이 규칙을 따르는 것이 좋습니다. 공식 Microsoft 패키지를 작성하지 않는 한 Microsoft.Extensions.DependencyInjection 네임스페이스에 확장 메서드를 배치하지 마세요. Microsoft.Extensions.DependencyInjection 네임스페이스 내에 정의된 확장 메서드:

  • 추가 using 블록 없이 IntelliSense에 표시됩니다.
  • 일반적으로 해당 확장 메서드가 호출되는 Program 또는 Startup 클래스에서 필요한 using 문의 수를 줄입니다.

앱은 다음을 수행합니다.

결론

이 샘플 앱에서는 여러 인터페이스와 해당 구현을 만들었습니다. 이러한 각 서비스는 고유하게 식별되고 ServiceLifetime과 페어링됩니다. 샘플 앱은 인터페이스에 대해 서비스 구현을 등록하는 방법과 지원 인터페이스 없이 순수 클래스를 등록하는 방법을 보여 줍니다. 그런 다음, 샘플 앱은 생성자 매개 변수로 정의된 종속성을 런타임에 확인하는 방법을 보여 줍니다.

이 앱을 실행하면 다음과 같은 출력이 표시됩니다.

// Sample output:
// Lifetime 1: Call 1 to provider.GetRequiredService<ServiceLifetimeReporter>()
//     IExampleTransientService: d08a27fa-87d2-4a06-98d7-2773af886125 (Always different)
//     IExampleScopedService: 402c83c9-b4ed-4be1-b78c-86be1b1d908d (Changes only with lifetime)
//     IExampleSingletonService: a61f1ff4-0b14-4508-bd41-21d852484a7b (Always the same)
// ...
// Lifetime 1: Call 2 to provider.GetRequiredService<ServiceLifetimeReporter>()
//     IExampleTransientService: b43d68fb-2c7b-4a9b-8f02-fc507c164326 (Always different)
//     IExampleScopedService: 402c83c9-b4ed-4be1-b78c-86be1b1d908d (Changes only with lifetime)
//     IExampleSingletonService: a61f1ff4-0b14-4508-bd41-21d852484a7b (Always the same)
// 
// Lifetime 2: Call 1 to provider.GetRequiredService<ServiceLifetimeReporter>()
//     IExampleTransientService: f3856b59-ab3f-4bbd-876f-7bab0013d392 (Always different)
//     IExampleScopedService: bba80089-1157-4041-936d-e96d81dd9d1c (Changes only with lifetime)
//     IExampleSingletonService: a61f1ff4-0b14-4508-bd41-21d852484a7b (Always the same)
// ...
// Lifetime 2: Call 2 to provider.GetRequiredService<ServiceLifetimeReporter>()
//     IExampleTransientService: a8015c6a-08cd-4799-9ec3-2f2af9cbbfd2 (Always different)
//     IExampleScopedService: bba80089-1157-4041-936d-e96d81dd9d1c (Changes only with lifetime)
//     IExampleSingletonService: a61f1ff4-0b14-4508-bd41-21d852484a7b (Always the same)

앱 출력에서 다음을 확인할 수 있습니다.

  • Transient 서비스는 항상 다르지만 서비스를 검색할 때마다 새 인스턴스가 만들어집니다.
  • Scoped 서비스는 새 범위에서 변경될 뿐 아니라 한 범위 내에서는 동일한 인스턴스입니다.
  • Singleton 서비스는 항상 동일하며 새 인스턴스는 한 번만 만들어집니다.

참고 항목