콘텐츠로 이동

IConfiguration & IOptions 패턴 심화

ASP.NET Core의 구성(Configuration) 시스템은 appsettings.json, 환경 변수, 커맨드라인 인수 등을 계층적으로 병합합니다. IOptions<T> 패턴으로 강타입 설정 객체를 주입하고, IOptionsMonitor<T>로 런타임 변경을 감지할 수 있습니다.


appsettings.json
↓ (덮어씀)
appsettings.{Environment}.json
User Secrets (개발 환경)
환경 변수
커맨드라인 인수 (최고 우선순위)
// Program.cs — 커스텀 소스 추가
builder.Configuration
.AddJsonFile("custom.json", optional: true, reloadOnChange: true)
.AddEnvironmentVariables(prefix: "MYAPP_")
.AddCommandLine(args);

appsettings.json
{
"Email": {
"SmtpHost": "smtp.example.com",
"Port": 587,
"UseTls": true
}
}
public class EmailOptions
{
public const string SectionName = "Email";
public string SmtpHost { get; set; } = "";
public int Port { get; set; } = 25;
public bool UseTls { get; set; }
}
// 등록
builder.Services.Configure<EmailOptions>(
builder.Configuration.GetSection(EmailOptions.SectionName));
// 주입
public class EmailService(IOptions<EmailOptions> options)
{
private readonly EmailOptions _opts = options.Value; // 싱글톤으로 캐시됨
}

// IOptions<T> — Singleton, 앱 시작 시점 값 고정
public class Service1(IOptions<MyOptions> opts)
{
void Use() => _ = opts.Value; // 항상 동일
}
// IOptionsSnapshot<T> — Scoped, 요청마다 갱신
public class Service2(IOptionsSnapshot<MyOptions> opts)
{
void Use() => _ = opts.Value; // 요청 시점 값
}
// IOptionsMonitor<T> — Singleton, 파일 변경 즉시 반영
public class Service3(IOptionsMonitor<MyOptions> monitor)
{
void Use() => _ = monitor.CurrentValue; // 최신 값
// 변경 이벤트 구독
IDisposable? _sub;
void Subscribe() =>
_sub = monitor.OnChange(opts => Reload(opts));
}
IOptionsIOptionsSnapshotIOptionsMonitor
수명SingletonScopedSingleton
변경 반영요청 단위즉시
Singleton에서 사용

using System.ComponentModel.DataAnnotations;
public class DatabaseOptions
{
[Required]
public string ConnectionString { get; set; } = "";
[Range(1, 100)]
public int MaxPoolSize { get; set; } = 10;
[Url]
public string? HealthCheckUrl { get; set; }
}
// 등록 + 유효성 검사
builder.Services
.AddOptions<DatabaseOptions>()
.BindConfiguration("Database")
.ValidateDataAnnotations()
.ValidateOnStart(); // 시작 시점에 즉시 검증
// 커스텀 유효성 검사
builder.Services
.AddOptions<DatabaseOptions>()
.BindConfiguration("Database")
.Validate(opts =>
{
return opts.MaxPoolSize <= 50 || opts.ConnectionString.Contains("pooling=true");
}, "풀 크기가 50 초과면 연결 문자열에 pooling=true 필요");

// 같은 타입을 다른 설정으로 여러 개 등록
builder.Services.Configure<SmtpOptions>("Primary",
builder.Configuration.GetSection("Email:Primary"));
builder.Services.Configure<SmtpOptions>("Backup",
builder.Configuration.GetSection("Email:Backup"));
// Named Options 주입
public class EmailRouter(IOptionsMonitor<SmtpOptions> monitor)
{
private SmtpOptions Primary => monitor.Get("Primary");
private SmtpOptions Backup => monitor.Get("Backup");
}

// 데이터베이스에서 설정 로드
public class DbConfigSource(string connectionString) : IConfigurationSource
{
public IConfigurationProvider Build(IConfigurationBuilder builder)
=> new DbConfigProvider(connectionString);
}
public class DbConfigProvider(string connectionString)
: ConfigurationProvider
{
public override void Load()
{
using var conn = new SqlConnection(connectionString);
var rows = conn.Query<(string Key, string Value)>(
"SELECT [Key], [Value] FROM AppConfig");
Data = rows.ToDictionary(r => r.Key, r => r.Value);
}
}
// 등록
builder.Configuration.Add(new DbConfigSource(connectionString));

Terminal window
# 계층형 키는 __ (더블 언더스코어)로 구분
export Email__SmtpHost=smtp.gmail.com
export Email__Port=587
# 배열
export Urls__0=https://localhost:5001
export Urls__1=https://localhost:5002

// 다른 설정을 기반으로 추가 설정 적용
builder.Services
.AddOptions<SmtpOptions>()
.BindConfiguration("Email")
.PostConfigure<IHostEnvironment>((opts, env) =>
{
// 개발 환경에서만 TLS 비활성화
if (env.IsDevelopment())
opts.UseTls = false;
});
// 모든 Named Options에 후처리 적용
builder.Services.PostConfigureAll<MyOptions>(opts =>
{
opts.Timeout = TimeSpan.FromSeconds(
Math.Max(opts.Timeout.TotalSeconds, 5));
});

9. IConfigureOptions — 의존성 주입이 필요한 설정

섹션 제목: “9. IConfigureOptions — 의존성 주입이 필요한 설정”
// 다른 서비스에 의존하는 설정 구성
public class MyOptionsConfigurator(IHttpClientFactory factory)
: IConfigureOptions<MyOptions>
{
public void Configure(MyOptions options)
{
// 서비스를 이용해 설정 동적 구성
var client = factory.CreateClient("Config");
options.RemoteEndpoint = FetchEndpoint(client);
}
}
builder.Services.AddSingleton<IConfigureOptions<MyOptions>, MyOptionsConfigurator>();

public class FeatureOptions
{
public bool EnableNewCheckout { get; set; }
public bool EnableBetaDashboard { get; set; }
public Dictionary<string, bool> Features { get; set; } = new();
}
// JSON
// { "Features": { "EnableNewCheckout": true, "EnableBetaDashboard": false } }
public class FeatureService(IOptionsMonitor<FeatureOptions> monitor)
{
public bool IsEnabled(string feature) =>
monitor.CurrentValue.Features.TryGetValue(feature, out bool on) && on;
}
// 사용
if (featureService.IsEnabled("EnableNewCheckout"))
return RedirectToAction("NewCheckout");

구성 소스 우선순위를 이해하면 환경별 설정 오버라이드가 자연스럽습니다. 런타임 변경이 필요하면 IOptionsMonitor, 요청 범위 변경이면 IOptionsSnapshot, 고정 설정이면 IOptions를 선택하세요. ValidateOnStart()로 잘못된 설정이 있으면 앱 시작 즉시 오류를 발견하도록 하고, PostConfigureIConfigureOptions로 환경 의존적인 설정 후처리를 깔끔하게 분리하세요.