.NET 7의 IOptions, IOptionsMonitor 및 IOptionsSnapshot 이해하기 | Code4IT

.NET 7의 IOptions, IOptionsMonitor 및 IOptionsSnapshot 이해하기 | Code4IT

·

6 min read

Code4ITUnderstanding IOptions, IOptionsMonitor, and IOptionsSnapshot in .NET 7을 번역하였습니다.


.NET 애플리케이션에서 구성을 다룰 때는 다양한 전략을 선택할 수 있습니다. 예를 들어, 생성자에 IConfiguration 인스턴스를 삽입하고 이름으로 모든 값을 검색할 수 있습니다.

또는 강력한 형식 구성을 최대한 활용할 수 있습니다. 모든 것이 이미 .NET에서 자동으로 처리되므로 수동으로 값을 캐스팅하는 데 신경 쓸 필요가 없습니다.

이 글에서는 IOptions, IOptionsSnapshot, IOptionsMonitor에 대해 알아보겠습니다. 비슷해 보이지만 올바른 것을 선택하기 위해 이해해야 할 몇 가지 주요 차이점이 있습니다.

이 글에서는 하나의 엔드포인트만 노출하는 더미 .NET API를 만들었습니다.

내 appsettings.json 파일에 노드를 추가했습니다:

{
    "MyConfig": {
        "Name": "Davide"
    }
}

이것은 POCO 클래스와 매핑됩니다:

public class GeneralConfig
{
    public string Name { get; set; }
}

API 프로젝트에 추가하려면 Program.cs 파일에 이 줄을 추가하면 됩니다:

builder.Services.Configure<GeneralConfig>(builder.Configuration.GetSection("MyConfig"));

보시다시피, "MyConfig"라는 이름의 콘텐츠를 가져와서 GeneralConfig 유형의 객체에 매핑합니다.

이러한 유형을 테스트하기 위해 더미 API 컨트롤러 세트를 만들었습니다. 각 컨트롤러는 구성에서 값을 읽고 객체로 반환하는 하나의 단일 메서드인 Get() 메서드를 노출합니다.

다양한 접근 방식을 시도해 볼 수 있도록 관련 컨트롤러의 생성자에 IOptions, IOptionsSnapshot, IOptionsMonitor를 주입하겠습니다.

IOptions: 단순함, 싱글톤, 재로드를 지원하지 않음

IOptions<T>는 이러한 구성을 주입하는 가장 간단한 방법입니다. 생성자에 삽입하고 Value 속성을 사용하여 실제 값에 액세스할 수 있습니다:

private readonly GeneralConfig _config;

public TestingController(IOptions<GeneralConfig> config)
{
    _config = config.Value;
}

이제 appsettings.json 파일에 정의한 대로 값이 채워진 GeneralConfig 개체에 직접 액세스할 수 있습니다.

IOptions<T>를 사용할 때 고려해야 할 몇 가지 사항이 있습니다:

  • 이 서비스는 싱글톤 인스턴스로 주입됩니다. 전체 애플리케이션이 동일한 인스턴스를 사용하며 전체 애플리케이션 수명 기간 동안 유효합니다.

  • 모든 구성은 시작 시점에 읽습니다. 애플리케이션이 실행되는 동안 앱 설정 파일을 업데이트하더라도 업데이트된 값은 표시되지 않습니다.

어떤 사람들은 다음과 같이 IOptions<T> 인스턴스를 비공개 필드로 저장하고 필요할 때 Value 속성을 사용하여 값에 액세스하는 것을 선호합니다:

private readonly IOptions<GeneralConfig> _config;
private int updates = 0;

public TestingController(IOptions<GeneralConfig> config)
{
    _config = config;
}

[HttpGet]
public ActionResult<Result> Get()
{
    var name = _config.Value.Name;
    return new Result
    {
        MyName = name,
        Updates = updates
    };
}

작동은 하지만 쓸모가 없습니다. IOptions<T>는 싱글톤이므로 매번 Value 프로퍼티에 액세스할 필요가 없습니다. 시간이 지나도 변경되지 않으므로 매번 액세스하는 것은 쓸모없는 작업입니다. 전자의 접근 방식을 사용하면 작성하기가 더 쉽고 성능도 약간 더 좋습니다.

한 가지 더 있습니다. 단위 테스트를 작성할 때 정적 메서드인 Options.Create<T>를 사용하여 테스트 대상 시스템에 IOptions<T>를 삽입하고 필요한 유형의 인스턴스를 전달할 수 있습니다.

[SetUp]
public void Setup()
{
    var config = new GeneralConfig { Name = "Test" };

    var options = Options.Create(config);

    _sut = new TestingController(options);
}

데모: 런타임에 구성이 변경되지 않음

아래에서 애플리케이션이 실행 중일 때 구성이 변경되지 않음을 보여주는 GIF를 확인할 수 있습니다.

With IOptions the configuration does not change at runtime

보시다시피 다음 단계를 수행했습니다:

  1. 애플리케이션을 시작합니다.

  2. /TestingOptions 엔드포인트를 호출하면 Davide라는 이름이 반환됩니다.

  3. 이제 appsettings.json 파일의 내용을 업데이트하여 이름을 Davide Bellone으로 설정합니다.

  4. 동일한 엔드포인트를 다시 호출하면 업데이트된 값이 표시되지 않습니다.

IOptionSnapshot: 범위지정, 낮은 성능, 재로드 지원

IOptions<T>와 유사하게 IOptionsSnapshot<T>이 있습니다. 비슷하게 작동하지만 큰 차이가 있습니다: IOptionsSnapshot<T>는 요청할 때마다 다시 계산됩니다.

public TestingController(IOptionsSnapshot<GeneralConfig> config)
{
    _config = config.Value;
}

IOptionsSnapshot<T>를 사용하면 항상 가장 최신의 값을 사용할 수 있습니다. 이 서비스에는 범위가 지정된 수명이 주입되어 모든 HTTP 요청 시 설정에서 값을 읽습니다. 즉, 애플리케이션이 실행되는 동안 설정 값을 업데이트할 수 있으며 업데이트된 결과를 확인할 수 있습니다.

.NET은 HTTP를 호출할 때마다 구성을 다시 빌드하므로 약간의 성능 오버헤드가 있습니다. 따라서 꼭 필요한 경우가 아니라면 항상 IOptions<T>를 사용하세요.

IOptions<T>에서 했던 것처럼 IOptionsSnapshot<T>를 테스트할 방법이 없으므로 스텁이나 모의(Moq 또는 NSubstitute 🔗 사용 가능)를 사용해야 합니다.

데모: 애플리케이션이 실행되는 동안 구성이 변경됩니다.

아래 GIF를 보세요. 여기서는 애플리케이션을 실행하고 /TestingIOptionsSnapshot 엔드포인트를 호출합니다.

With IOptionsSnapshot the configuration changes while the application is running

다음 단계를 수행했습니다:

  1. 애플리케이션을 실행합니다.

  2. /TestingIOptionsSnapshot 엔드포인트를 호출합니다. 반환된 값은 appsettings.json 파일에서 동일합니다: Davide Bellone

  3. 그런 다음 구성 파일에서 값을 업데이트합니다.

  4. /TestingIOptionsSnapshot를 다시 호출하면 반환된 값이 앱 설정 파일의 새 값을 반영하는 것을 확인할 수 있습니다.

IOptionsMonitor: 복합함, 싱글톤, 재로드 지원

마지막으로, 세 가지 중 마지막인 IOptionsMonitor<T>입니다.

IOptionsMonitor<T>를 사용하면 appsettings.json 파일에서 가장 최신의 값을 확인할 수 있습니다.

또한 구성 파일이 업데이트될 때마다 트리거되는 콜백 이벤트도 있습니다.

싱글톤 서비스로 주입되므로 전체 애플리케이션 수명 동안 동일한 인스턴스가 공유됩니다.

IOptions<T>와 두 가지 주요 차이점이 있습니다:

  1. 구성 값을 저장하는 속성 이름은 Value가 아닌 CurrentValue입니다

  2. 설정 파일을 업데이트할 때마다 호출되는 콜백이 있습니다: `OnChange(Action<TOptions, string?> listener). 이를 사용하여 구성이 변경될 때마다 트리거되어야 하는 작업을 수행할 수 있습니다.

노트: OnChange는 폐기해야 하는 IDisposable을 구현하는 객체를 반환합니다. 그렇지 않으면 크리스 엘버트가 알아차렸듯이(참고로 트위터에서 그를 팔로우하세요!) IOptionsMonitor<T>를 사용하는 클래스의 인스턴스는 절대로 처분되지 않습니다.

다시 말하지만, IOptions<T>에서 했던 것처럼 IOptionsMonitor<T>를 테스트할 수 있는 방법은 없습니다. 따라서 스텁과 모의 테스트에 의존해야 합니다(다시 말하지만, Moq 또는 NSubstitute 🔗를 사용해야 할 수도 있습니다).

데모: 구성이 변경되고 콜백이 호출됩니다.

아래 GIF에서는 IOptionsMonitor의 사용법을 보여드립니다.

구성의 변경 사항을 수신하고 정적 카운터를 업데이트한 다음 API에서 최종 결과를 반환하는 API 컨트롤러를 만들었습니다:

public class TestingIOptionsMonitorController : ControllerBase
{
    private static int updates = 0;
    private readonly GeneralConfig _config;

    public TestingIOptionsMonitorController(IOptionsMonitor<GeneralConfig> config)
    {
        _config = config.CurrentValue;
        config.OnChange((_, _) => updates++);
    }

    [HttpGet]
    public ActionResult<Result> Get() => new Result
    {
        MyName = _config.Name,
        Updates = updates
    };
}

애플리케이션이 실행되는 동안 애플리케이션을 실행하고 구성 콘텐츠를 수정하면 IOptionsMonitor<T>의 전체 사용 현황을 확인할 수 있습니다:

With IOptionsMonitor the configuration changes, the callback is called

보시다시피, 저는 이 단계를 수행했습니다:

  1. 애플리케이션을 실행합니다.

  2. /TestionIOptionsMonitor 엔드포인트를 호출합니다. 구성에서 내 이름 필드를 읽고 업데이트는 0입니다.

  3. 그런 다음 구성 파일을 업데이트하고 저장합니다. 백그라운드에서 OnChange 콜백이 실행되고 Updates 값이 업데이트됩니다.

이상하게도 콜백이 예상보다 더 많이 호출됩니다. 파일을 두 번만 업데이트했지만 카운터는 6으로 설정되어 있습니다. 이상한 동작입니다. 왜 이런 일이 발생하는지 아신다면 아래에 메시지를 남겨주세요 📩.

.NET의 IOptions 대 IOptionsSnapshot 대 IOptionsMonitor

지금까지 IOptions, IOptionsSnapshotIOptionsMonitor에 대해 간략하게 소개해 드렸습니다.

물론 몇 가지 차이점이 있습니다. 다음은 이 글에서 배운 내용을 요약한 표입니다.

유형DI 수명단위 테스트에 삽입하는 가장 좋은 방법실시간 재로드 허용콜백 기능 유무
IOptions<T>SingletonOptions.Create<T>
IOptionsSnapshot<T>ScopedStub / Mock🟢
IOptionsMonitor<T>SingletonStub / Mock🟢🟢

예를 들어, IOptionsSnapshotIOptionsMonitor를 사용하면 명명된 옵션을 사용할 수 있으므로 JSON 파일에서 다른 노드를 참조하는 동일한 유형의 인스턴스를 더 많이 삽입할 수 있습니다.

하지만 이는 다음 글에서 다룰 주제이니 기대해 주세요 😎.

추가 읽기

구성을 주입하는 방법에 대한 자세한 내용은 다음과 같습니다.

가장 좋은 리소스 중 하나는 공식 문서입니다:

🔗 Options pattern in ASP.NET Core | Microsoft Learn

저는 IOptionsIOptionsMonitor는 싱글톤이고, IOptionsSnapshot은 범위가 지정되어 있다고 계속 설명했습니다. 이 두 가지가 무엇을 의미하는지 잘 모르시는 분들을 위해 짧지만 자세히 설명해드리겠습니다:

🔗 Dependency Injection lifetimes in .NET - Scoped vs Transient vs Singleton | Code4IT

특히, 싱글톤 서비스에 일시적 또는 범위 지정 서비스를 삽입할 때의 문제점을 설명하는 보너스 팁에 집중해 주시기 바랍니다:

🔗 Dependency Injection lifetimes in .NET - Scoped vs Transient vs Singleton | Code4IT

이 기사는 Code4IT 🐧에 처음 게재되었습니다.

이 글에서는 appsettings.json 파일에 구성을 저장했습니다. 환경 변수 및 launchSettings.json 등 더 많은 방법으로 구성 값을 설정할 수 있습니다.

🔗 3 (and more) ways to set configuration values in .NET | Code4IT

마무리

이 문서에서는 .NET 7 응용 프로그램에서 IOptions<T>, IOptionsSnapshot<T>IOptionsMonitor<T>를 사용하는 방법에 대해 알아봤습니다.

예를 들어, 전체 객체를 싱글톤으로 주입하거나 IConfiguration을 사용하여 단일 값을 가져오는 등 다양한 방법으로 구성을 처리할 수 있습니다.

언제 다른 접근 방식 대신 어떤 접근 방식을 선택하시겠습니까? 아래에 댓글을 남겨주세요! 📩

이 기사가 도움이 되셨기를 바랍니다! 트위터링크드인에서 계속 연락하세요! 🤜🤛

행복한 코딩!

🐧


Understanding IOptions, IOptionsMonitor, and IOptionsSnapshot in .NET 7 | Code4IT