Skip to main content

Command Palette

Search for a command to run...

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

Updated
6 min read
.NET 7의 IOptions, IOptionsMonitor 및 IOptionsSnapshot 이해하기 | Code4IT

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

65 views

More from this blog

개발, 테스트, 운영에서의 도커 활용

핵심 원칙: "한 번 빌드하고, 어디서든 실행한다 (Build once, run anywhere)" 도커의 가장 큰 장점은 환경 일관성입니다. 동일한 도커 이미지를 사용하여 개발, 테스트, 운영 환경을 구성함으로써 "제 PC에서는 됐는데..." 하는 문제를 최소화할 수 있습니다. 1. 개발 단계 (Development) 목표: 빠른 코드 변경 반영, 쉬운 디버깅, 실제 운영 환경과 유사한 환경 구성. Docker 사용 방안: Dockerf...

May 9, 20256 min read15

[EF Core] 데이터 삭제 시 소프트 삭제 적용

DB에서 데이터를 삭제하면 일반적으로 복구할 수 없습니다. 또한 관계에 따라 영구 삭제 자체가 어려울 수도 있습니다. 그래서 데이터를 영구 삭제하는 대신 IsDeleted 속성을 true로 주고 IsDeleted 속성을 필터링해서 조회하는 방법을 사용하기도 합니다. 이를 소프트 삭제라고 합니다. 그런데 EF에서 알아서 데이터 삭제 시 소프트 삭제를 하고 쿼리시 IsDeleted 속성을 체크해서 삭제한 데이터를 제외한 데이터만 쿼리하게 하는 ...

Mar 18, 20243 min read19

[EF Core] ValueConverter를 이용해서 엔터티 속성의 도메인 관리

EF Core를 사용하면서 문자열 길이 등의 특성을 일일이 지정하는 것은 번거롭습니다. ... [MaxLength(32)] public string? 제목 { get; set; } 엔터티가 한 개일 때는 상관이 없으나 제목 유형이 여러 엔터티에 사용될 경우 유형을 지정하기 번거롭습니다. 속성 유형을 도메인으로 관리하면 참 편할텐데요, ValueConverter를 이용할 수 있습니다. 그런데 이것을 인터페이스 정적 추상를 사용해서 다음처럼 ...

Mar 16, 20242 min read8

디모이 블로그

154 posts

.NET 관련 기술을 선호하고 새로운 언어를 배우는데 관심이 있습니다.