Microsoft.Extensions 구성 패턴
이 기술을 사용해야 할 때
다음과 같은 경우에 이 기술을 사용하세요:
- appsettings.json의 구성 데이터를 강력한 타입의 클래스에 바인딩할 때
- 애플리케이션 시작 시 구성을 검증하여 빠르게 실패(fail fast)하고자 할 때
- 설정에 대한 복잡한 유효성 검사 로직을 구현할 때
- 테스트 가능하고 유지보수하기 쉬운 구성 클래스를 설계할 때
- IOptions, IOptionsSnapshot, IOptionsMonitor의 차이를 이해하고 사용할 때
참조 파일
- advanced-patterns.md: 의존성이 있는 유효성 검사기, 명명된 옵션, 완전한 프로덕션 예제(AkkaSettings), 유효성 검사기 테스트
구성 유효성 검사가 중요한 이유
문제점: 애플리케이션은 잘못된 구성(연결 문자열 누락, 잘못된 URL, 범위를 벗어난 값)으로 인해 런타임에 자주 실패합니다. 이러한 실패는 구성이 로드된 위치와는 전혀 다른 비즈니스 로직 깊숙한 곳에서 발생합니다.
해결책: 시작 시 구성을 검증하세요. 잘못된 경우 명확한 오류 메시지와 함께 즉시 실패합니다.
// 나쁨: 누군가 서비스를 사용하려고 할 때 런타임에 실패
public class EmailService
{
public EmailService(IOptions<SmtpSettings> options)
{
var settings = options.Value;
// 프로덕션에서 10분 후에 NullReferenceException 발생
_client = new SmtpClient(settings.Host, settings.Port);
}
}
// 좋음: 시작 시 명확한 오류와 함께 실패
// "SmtpSettings 유효성 검사 실패: Host가 필요합니다."
패턴 1: 기본 옵션 바인딩
설정 클래스 정의
public class SmtpSettings
{
public const string SectionName = "Smtp";
public string Host { get; set; } = string.Empty;
public int Port { get; set; } = 587;
public string? Username { get; set; }
public string? Password { get; set; }
public bool UseSsl { get; set; } = true;
}
구성에서 바인딩
builder.Services.AddOptions<SmtpSettings>()
.BindConfiguration(SmtpSettings.SectionName);
// appsettings.json
{
"Smtp": {
"Host": "smtp.example.com",
"Port": 587,
"Username": "[email protected]",
"Password": "secret",
"UseSsl": true
}
}
서비스에서 소비
public class EmailService
{
private readonly SmtpSettings _settings;
// IOptions<T> - 싱글톤, 시작 시 한 번만 읽음
public EmailService(IOptions<SmtpSettings> options)
{
_settings = options.Value;
}
}
패턴 2: Data Annotations 유효성 검사
간단한 유효성 검사 규칙에는 Data Annotations을 사용하세요:
using System.ComponentModel.DataAnnotations;
public class SmtpSettings
{
public const string SectionName = "Smtp";
[Required(ErrorMessage = "SMTP 호스트가 필요합니다")]
public string Host { get; set; } = string.Empty;
[Range(1, 65535, ErrorMessage = "포트는 1에서 65535 사이여야 합니다")]
public int Port { get; set; } = 587;
[EmailAddress(ErrorMessage = "사용자 이름은 유효한 이메일 주소여야 합니다")]
public string? Username { get; set; }
public string? Password { get; set; }
public bool UseSsl { get; set; } = true;
}
Data Annotations 유효성 검사 활성화
builder.Services.AddOptions<SmtpSettings>()
.BindConfiguration(SmtpSettings.SectionName)
.ValidateDataAnnotations() // 속성 기반 유효성 검사 활성화
.ValidateOnStart(); // 시작 시 즉시 검증
핵심 사항: .ValidateOnStart()는 중요합니다. 이것이 없으면 유효성 검사는 옵션이 처음 액세스될 때만 실행됩니다.
패턴 3: 복잡한 유효성 검사를 위한 IValidateOptions
Data Annotations은 간단한 규칙에 적합하지만, 복잡한 유효성 검사는 IValidateOptions<T>가 필요합니다:
| 시나리오 | Data Annotations | IValidateOptions |
|---|---|---|
| 필수 필드 | 예 | 예 |
| 범위 확인 | 예 | 예 |
| 속성 간 유효성 검사 | 아니요 | 예 |
| 조건부 유효성 검사 | 아니요 | 예 |
| 외부 서비스 확인 | 아니요 | 예 |
| 유효성 검사기의 의존성 주입 | 아니요 | 예 |
IValidateOptions 구현
using Microsoft.Extensions.Options;
public class SmtpSettingsValidator : IValidateOptions<SmtpSettings>
{
public ValidateOptionsResult Validate(string? name, SmtpSettings options)
{
var failures = new List<string>();
if (string.IsNullOrWhiteSpace(options.Host))
failures.Add("Host가 필요합니다");
if (options.Port is < 1 or > 65535)
failures.Add($"Port {options.Port}가 잘못되었습니다. 1에서 65535 사이여야 합니다");
// 속성 간 유효성 검사
if (!string.IsNullOrEmpty(options.Username) && string.IsNullOrEmpty(options.Password))
failures.Add("Username이 지정된 경우 Password가 필요합니다");
// 조건부 유효성 검사
if (options.UseSsl && options.Port == 25)
failures.Add("포트 25는 일반적으로 SSL과 함께 사용되지 않습니다. 포트 465 또는 587을 고려하세요");
return failures.Count > 0
? ValidateOptionsResult.Fail(failures)
: ValidateOptionsResult.Success;
}
}
유효성 검사기 등록
builder.Services.AddOptions<SmtpSettings>()
.BindConfiguration(SmtpSettings.SectionName)
.ValidateDataAnnotations()
.ValidateOnStart();
builder.Services.AddSingleton<IValidateOptions<SmtpSettings>, SmtpSettingsValidator>();
순서가 중요합니다: Data Annotations이 먼저 실행되고, 그 다음 IValidateOptions 유효성 검사기가 실행됩니다. 모든 실패는 함께 수집됩니다.
의존성이 있는 유효성 검사기, 명명된 옵션 및 완전한 프로덕션 예제는 advanced-patterns.md를 참조하세요.
패턴 4: 옵션 수명
| 인터페이스 | 수명 | 변경 시 다시 로드 | 사용 사례 |
|---|---|---|---|
IOptions<T> | 싱글톤 | 아니요 | 정적 구성, 한 번만 읽기 |
IOptionsSnapshot<T> | 범위 | 예 (요청별) | 최신 구성이 필요한 웹 앱 |
IOptionsMonitor<T> | 싱글톤 | 예 (콜백 포함) | 백그라운드 서비스, 실시간 업데이트 |
백그라운드 서비스를 위한 IOptionsMonitor
public class BackgroundWorker : BackgroundService
{
private readonly IOptionsMonitor<WorkerSettings> _optionsMonitor;
private WorkerSettings _currentSettings;
public BackgroundWorker(IOptionsMonitor<WorkerSettings> optionsMonitor)
{
_optionsMonitor = optionsMonitor;
_currentSettings = optionsMonitor.CurrentValue;
_optionsMonitor.OnChange(settings =>
{
_currentSettings = settings;
});
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await DoWorkAsync();
await Task.Delay(_currentSettings.PollingInterval, stoppingToken);
}
}
}
패턴 5: 사후 구성(Post-Configuration)
바인딩 후 유효성 검사 전에 옵션을 수정합니다:
builder.Services.AddOptions<ApiSettings>()
.BindConfiguration("Api")
.PostConfigure(options =>
{
if (!string.IsNullOrEmpty(options.BaseUrl) && !options.BaseUrl.EndsWith('/'))
options.BaseUrl += '/';
options.Timeout ??= TimeSpan.FromSeconds(30);
})
.ValidateDataAnnotations()
.ValidateOnStart();
피해야 할 안티 패턴
1. 수동 구성 액세스
// 나쁨: 유효성 검사를 우회하고 테스트하기 어려움
public class MyService
{
public MyService(IConfiguration configuration)
{
var host = configuration["Smtp:Host"]; // 유효성 검사 없음!
}
}
// 좋음: 강력한 타입, 유효성 검사됨
public class MyService
{
public MyService(IOptions<SmtpSettings> options)
{
var host = options.Value.Host; // 시작 시 유효성 검사됨
}
}
2. 생성자에서 유효성 검사
// 나쁨: 유효성 검사가 시작 시가 아니라 런타임에 발생
public class MyService
{
public MyService(IOptions<Settings> options)
{
if (string.IsNullOrEmpty(options.Value.Required))
throw new ArgumentException("Required가 누락되었습니다"); // 너무 늦음!
}
}
// 좋음: IValidateOptions + ValidateOnStart()를 통해 시작 시 유효성 검사
3. ValidateOnStart()를 잊는 경우
// 나쁨: 유효성 검사가 처음 액세스될 때만 실행됨
builder.Services.AddOptions<Settings>()
.ValidateDataAnnotations(); // ValidateOnStart() 누락!
// 좋음: 잘못된 경우 즉시 실패
builder.Services.AddOptions<Settings>()
.ValidateDataAnnotations()
.ValidateOnStart();
4. IValidateOptions에서 예외를 던지는 경우
// 나쁨: 예외를 던져 유효성 검사 체인을 깨뜨림
public ValidateOptionsResult Validate(string? name, Settings options)
{
if (options.Value < 0)
throw new ArgumentException("값은 음수가 될 수 없습니다"); // 잘못됨!
return ValidateOptionsResult.Success;
}
// 좋음: 실패 결과를 반환
public ValidateOptionsResult Validate(string? name, Settings options)
{
if (options.Value < 0)
return ValidateOptionsResult.Fail("값은 음수가 될 수 없습니다");
return ValidateOptionsResult.Success;
}
요약
| 원칙 | 구현 |
|---|---|
| 빠른 실패 | .ValidateOnStart() |
| 강력한 타입 | POCO 클래스에 바인딩 |
| 간단한 유효성 검사 | Data Annotations |
| 복잡한 유효성 검사 | IValidateOptions<T> |
| 속성 간 규칙 | IValidateOptions<T> |
| 환경 인식 | IHostEnvironment 주입 |
| 테스트 가능 | 유효성 검사기는 일반 클래스 |