C#에서의 옵션 패턴
[[academy-video-youtube({"vid": "ko1Ie9gDydY", "start_time": "0", "title": "The Options Pattern in C#", "creator": "Tim Corey", "length": "10m 15s"})]]
.NET 애플리케이션의 구성은 일반적으로 appsettings.json, 환경 변수 또는 사용자 비밀에 존재합니다. 파일에서 클래스에 설정 데이터를 깨끗하고 테스트 가능한 방식으로 가져오는 것이 옵션 패턴이 해결하는 문제입니다. JSON 키를 수동으로 읽거나 원시 문자열을 여기저기 전달하는 대신, 설정 섹션을 강력하게 형식화된 C# 클래스에 바인딩하고 의존성 주입이 필요한 곳에 전달되도록 설정합니다.
그의 영상 "The Options Pattern in C#"에서 Tim Corey는 옵션 패턴의 세 가지 변형 (IOptions, IOptionsSnapshot, IOptionsMonitor)을 소개하고, 각각을 Blazor 서버 앱에서 데모로 보여주며, 구성 누락 또는 잘못된 경우에 애플리케이션이 빠르게 실패할 수 있도록 검증을 추가하는 방법을 설명합니다. 구성 파일을 읽고 의존성 주입을 사용하는 모든 .NET 프로젝트를 구축하는 경우 이 패턴은 기본입니다.
설정: POCO 모델과 appsettings.json
[0:34 - 1:46] Tim은 Blazor 웹 앱 (서버 측 렌더링, 클라이언트 측 상호작용 없음)으로 시작하며, 이미 두 가지 요소가 준비되어 있습니다. 첫 번째는 appsettings.json에서 CloudInfo 섹션 아래에 세 개의 키-값 쌍이 있는 섹션입니다.
// appsettings.json
{
"CloudInfo": {
"Storage": "https://storage.example.com",
"Website": "https://www.example.com",
"API": "https://api.example.com"
}
}// appsettings.json
{
"CloudInfo": {
"Storage": "https://storage.example.com",
"Website": "https://www.example.com",
"API": "https://api.example.com"
}
}두 번째 요소는 JSON 키와 일치하는 속성을 가진 plain C# 클래스 (POCO)입니다.
public class CloudInfoOptions
{
public string Storage { get; set; }
public string Website { get; set; }
public string API { get; set; }
}public class CloudInfoOptions
{
public string Storage { get; set; }
public string Website { get; set; }
public string API { get; set; }
}Tim은 구성 소스가 반드시 appsettings.json일 필요는 없다고 언급합니다. 그것은 appsettings.Development.json, secrets.json 또는 그 조합이 될 수 있습니다. 옵션 패턴은 애플리케이션에 등록된 설정 공급자에서 읽으며, POCO의 속성 이름은 관례상 JSON 키와 일치합니다.
Options 등록하기: Program.cs
[2:25 - 3:15] 의존성 주입에 구성을 연결하는 데는 Program.cs에서 두 번의 메소드 호출이 필요합니다.
// Register CloudInfoOptions bound to the "CloudInfo" section
builder.Services.AddOptions<CloudInfoOptions>()
.BindConfiguration("CloudInfo");// Register CloudInfoOptions bound to the "CloudInfo" section
builder.Services.AddOptions<CloudInfoOptions>()
.BindConfiguration("CloudInfo");AddOptions<t>()는 DI에 유형을 등록합니다. BindConfiguration("CloudInfo")는 프레임워크에 구성의 어느 섹션을 매핑할지 알려줍니다. 문자열 "CloudInfo"는 appsettings.json의 JSON 키에 해당합니다. 섹션 이름과 클래스 이름이 일치하지 않으면, 문자열 인수가 프레임워크가 올바른 데이터를 찾는 데 사용합니다.
IOptions: 싱글턴 접근법
[3:15 - 5:18] 등록이 완료된 후, Tim은 IOptions<CloudInfoOptions>를 사용하여 구성을 Blazor 페이지에 주입합니다.
@inject IOptions<CloudInfoOptions> CloudConfig
@code {
protected override void OnInitialized()
{
var options = CloudConfig.Value;
// options.Storage, options.Website, options.API are available
}
}@inject IOptions<CloudInfoOptions> CloudConfig
@code {
protected override void OnInitialized()
{
var options = CloudConfig.Value;
// options.Storage, options.Website, options.API are available
}
}핵심적인 세부 사항은 IOptions<t>가 싱글톤으로 등록된다는 것입니다. 설정 값은 애플리케이션이 시작될 때 한 번 읽히고 프로세스의 수명 동안 캐시됩니다. 앱이 실행 중일 때 appsettings.json을 변경하면, IOptions이 변경 사항을 반영하지 않을 것입니다.
대부분의 애플리케이션에서는 이 방법이 적합합니다. 설정은 런타임에 거의 변경되지 않으며, 싱글턴 수명은 요청당 추가 비용이 없습니다.
IOptionsSnapshot: 범위 재로딩
[5:18 - 6:55] Tim은 IOptions을 IOptionsSnapshot으로 교체하여 범위 버전을 보여줍니다:
@inject IOptionsSnapshot<CloudInfoOptions> CloudConfig@inject IOptionsSnapshot<CloudInfoOptions> CloudConfigIOptionsSnapshot<t>은 각 요청에 대해 새로 구성을 읽습니다 (범위 수명). 앱이 실행 중일 때 appsettings.json을 수정하면, 다음 웹 요청이 새로운 값을 가져올 것입니다. 이전 요청은 자체 스냅샷을 유지하므로 요청 중 불일치가 없습니다.
이는 기능 플래그 전환, API 엔드포인트 업데이트 또는 스토리지 연결 문자열 회전처럼 애플리케이션을 재시작하지 않고 설정 변경이 필요할 때 유용합니다. 대가로는 설정을 다시 읽는 데 소요되는 사소한 요청당 비용이 있으며, 대부분의 작업량에서는 무시할 수 있습니다.
IOptionsMonitor: 실시간 변경 알림
[6:55 - 8:34] 세 번째 버전 IOptionsMonitor<t>는 스냅샷 리로딩보다 더 나아갑니다. 설정 변경을 적극적으로 감시하고 값이 업데이트될 때 콜백을 트리거할 수 있습니다.
@inject IOptionsMonitor<CloudInfoOptions> CloudConfig
@code {
protected override void OnInitialized()
{
// Current values
var current = CloudConfig.CurrentValue;
// Register a callback for live changes
CloudConfig.OnChange(updatedOptions =>
{
// React to configuration changes in real time
});
}
}@inject IOptionsMonitor<CloudInfoOptions> CloudConfig
@code {
protected override void OnInitialized()
{
// Current values
var current = CloudConfig.CurrentValue;
// Register a callback for live changes
CloudConfig.OnChange(updatedOptions =>
{
// React to configuration changes in real time
});
}
}IOptionsSnapshot와 달리, 이는 요청 간에만 새로 고침되는 반면, IOptionsMonitor는 장기 실행 작업 중 변화 감지가 가능합니다. Tim은 이것이 백그라운드 서비스, SignalR 허브 또는 단일 HTTP 요청보다 수명이 긴 구성요소에 가장 관련이 있음을 지적합니다.
유효성 검증 추가하기
[8:34 - 10:05] Tim이 다루는 마지막 부분은 유효성 검증입니다. 옵션 패턴은 설정에 처음 액세스할 때 실행되는 인라인 유효성 검사 규칙을 지원합니다.
builder.Services.AddOptions<CloudInfoOptions>()
.BindConfiguration("CloudInfo")
.Validate(opts =>
!string.IsNullOrEmpty(opts.Storage) &&
!string.IsNullOrEmpty(opts.Website) &&
!string.IsNullOrEmpty(opts.API));builder.Services.AddOptions<CloudInfoOptions>()
.BindConfiguration("CloudInfo")
.Validate(opts =>
!string.IsNullOrEmpty(opts.Storage) &&
!string.IsNullOrEmpty(opts.Website) &&
!string.IsNullOrEmpty(opts.API));Tim은 Storage 값을 appsettings.json에서 제거하고 애플리케이션을 시작하여 실패 사례를 보여줍니다. 결과는 즉각적인 처리되지 않은 예외: "CloudInfoOptions failed validation."입니다. 불완전한 설정으로 애플리케이션이 시작을 거부하는 것은 바로 원하는 행동입니다. 시작 시 값을 찾지 못하는 것보다 제작 환경에서 오전 2시에 null 참조를 만나는 것이 훨씬 낫습니다.
더 복잡한 검증 시나리오 (속성 간의 상호 검증, 조건부 규칙)에서는 여러 .Validate() 호출을 연결하거나 IValidateOptions<t>을 전용 검증 클래스로 구현할 수 있습니다.
정리하기: 세 가지 인터페이스, 하나의 패턴
[10:05 - 10:10] 옵션 패턴은 설정 저장과 소비 사이의 깔끔한 분리를 제공합니다. IOptions은 구성이 고정적인 대부분의 사용 사례를 다루고 있습니다. IOptionsSnapshot은 요청 간의 변경 사항을 반영해야 하는 애플리케이션을 처리합니다. IOptionsMonitor은 구성 업데이트에 대한 실시간 인식이 필요한 장기 실행 구성 요소에 활용됩니다. 유효성 검증은 필수 값이 누락되었을 때 애플리케이션이 크게 실패하도록 합니다.
결론
[10:10 - 10:15] 종합적으로: Program.cs에 AddOptions<t>().BindConfiguration("SectionName")로 구성을 등록하고, 재로딩 요구 사항에 따라 세 가지 인터페이스 중 하나 (IOptions, IOptionsSnapshot, 또는 IOptionsMonitor)를 주입하며, 시작 시 누락된 값을 잡기 위해 .Validate()를 추가하십시오.
이 패턴은 Blazor, ASP.NET Core, 콘솔 앱, 워커 서비스와 같은 모든 .NET 프로젝트 유형에서 작동합니다. 어디서나 등록은 동일합니다.
예제 팁: 어떤 인터페이스를 사용할지 잘 모를 경우, IOptions<t>로 시작하십시오. 가장 간단하며 요청당 오버헤드가 전혀 없고, 설정이 시작 시에 설정되고 변경되지 않는 경우 대부분의 사례를 다룹니다. 런타임 리로딩에 대한 구체적인 필요가 있을 때만 IOptionsSnapshot 또는 IOptionsMonitor으로 이동하십시오.

