.NET에서 IOptions를 잘못 사용하는 것을 멈추세요!
Source: Dev.to
.NET에서 IOptions를 잘못 사용하는 것을 멈추세요
IOptions, IOptionsSnapshot, IOptionsMonitor는 .NET 설정 시스템에서 매우 유용하지만, 잘못 사용하면 예상치 못한 동작이나 성능 문제를 일으킬 수 있습니다. 이 글에서는 흔히 저지르는 실수들을 짚어보고, 올바른 사용 방법을 제시합니다.
1️⃣ IOptions<T>는 읽기 전용 설정을 위한 것입니다
public class MySettings
{
public string ConnectionString { get; set; }
}
services.Configure<MySettings>(Configuration.GetSection("MySettings"));
- 잘못된 사용:
IOptions<MySettings>를 주입받아 런타임에 값을 수정하려 시도한다. - 왜 문제인가?
IOptions<T>는 애플리케이션 시작 시 한 번만 바인딩됩니다. 이후 변경 사항은 반영되지 않으며, 직접 값을 바꾸면 DI 컨테이너와 설정 소스 간에 불일치가 발생합니다.
올바른 접근법: 설정을 변경해야 한다면 IConfiguration을 직접 사용하거나, IOptionsMonitor<T>를 활용해 변경을 감지합니다.
2️⃣ IOptionsSnapshot<T>는 스코프당 한 번만 새 값을 제공합니다
public class MyService
{
private readonly IOptionsSnapshot<MySettings> _options;
public MyService(IOptionsSnapshot<MySettings> options)
{
_options = options;
}
public void DoWork()
{
var current = _options.Value; // 스코프가 같은 동안은 같은 인스턴스
}
}
- 잘못된 사용:
IOptionsSnapshot<T>를 싱글톤에 주입한다. - 왜 문제인가?
IOptionsSnapshot<T>는 스코프(예: HTTP 요청)마다 새 인스턴스를 생성하도록 설계되었습니다. 싱글톤에 주입하면 스코프가 없으므로 매번 같은 인스턴스를 반환하게 되고, 기대한 “스냅샷” 동작이 사라집니다.
해결책: IOptionsSnapshot<T>는 스코프가 있는 서비스(예: 컨트롤러, Scoped 서비스)에서만 사용하고, 싱글톤에서는 IOptionsMonitor<T> 혹은 IConfiguration을 사용합니다.
3️⃣ IOptionsMonitor<T>는 변경 감지를 위한 최선의 선택
public class MyBackgroundService : BackgroundService
{
private readonly IOptionsMonitor<MySettings> _monitor;
public MyBackgroundService(IOptionsMonitor<MySettings> monitor)
{
_monitor = monitor;
_monitor.OnChange(settings =>
{
// 설정이 바뀔 때마다 실행되는 로직
ReloadResources(settings);
});
}
// ...
}
-
특징
- 애플리케이션 실행 중 설정 파일이 바뀌면 자동으로 새 값을 제공한다.
OnChange콜백을 통해 실시간으로 반응할 수 있다.- 싱글톤에서도 안전하게 사용할 수 있다.
-
주의점
IOptionsMonitor<T>는 내부적으로IChangeToken을 사용해 파일 시스템을 감시합니다. 파일이 자주 바뀌는 경우 성능에 영향을 줄 수 있으니, 필요에 따라 감시 빈도를 조절하거나 별도 캐시 전략을 도입하세요.
4️⃣ 코드 예시: 올바른 DI 등록과 사용법
// Program.cs / Startup.cs
builder.Services.Configure<MySettings>(builder.Configuration.GetSection("MySettings"));
builder.Services.AddSingleton<MySingletonService>(); // 내부에서 IOptionsMonitor 사용
builder.Services.AddScoped<MyScopedService>(); // 내부에서 IOptionsSnapshot 사용
public class MySingletonService
{
private readonly IOptionsMonitor<MySettings> _monitor;
public MySingletonService(IOptionsMonitor<MySettings> monitor)
{
_monitor = monitor;
}
public void Print()
{
Console.WriteLine(_monitor.CurrentValue.ConnectionString);
}
}
public class MyScopedService
{
private readonly IOptionsSnapshot<MySettings> _snapshot;
public MyScopedService(IOptionsSnapshot<MySettings> snapshot)
{
_snapshot = snapshot;
}
public void Print()
{
Console.WriteLine(_snapshot.Value.ConnectionString);
}
}
5️⃣ 흔히 하는 실수 체크리스트
| 실수 | 올바른 대안 |
|---|---|
IOptions<T>를 주입받아 런타임에 수정 | IConfiguration 또는 IOptionsMonitor<T> 사용 |
IOptionsSnapshot<T>를 싱글톤에 주입 | 싱글톤 → IOptionsMonitor<T>스코프 서비스 → IOptionsSnapshot<T> |
설정 파일을 자주 변경하면서 IOptionsMonitor<T>만 남용 | 변경 빈도에 따라 캐시 레이어 도입 또는 IConfigurationReloadToken 직접 사용 |
IOptions<T>를 여러 번 Configure<T>로 중복 바인딩 | 하나의 섹션에 대해 한 번만 Configure<T> 호출 |
6️⃣ 마무리
- 읽기 전용 설정 →
IOptions<T> - 스코프당 스냅샷이 필요할 때 →
IOptionsSnapshot<T>(스코프가 있는 서비스에만) - 실시간 변경 감지가 필요할 때 →
IOptionsMonitor<T>(싱글톤 포함 모든 스코프에서 사용 가능)
올바른 옵션 인터페이스를 선택하면 코드 가독성이 높아지고, 불필요한 버그와 성능 저하를 방지할 수 있습니다. 이제 IOptions를 잘못 사용하는 습관을 버리고, 상황에 맞는 인터페이스를 활용해 보세요!
📌 IOptions – 간단한 버전
Think of it as: 한 번 적어두고 절대 바꾸지 않는 포스트잇.
What it does: 앱이 시작될 때 설정을 로드하고, 그게 전부.
Example
public class NotificationSettings
{
public string SmtpServer { get; set; }
public int Port { get; set; }
public string DefaultSender { get; set; }
}
public class NotificationService
{
private readonly NotificationSettings _settings;
public NotificationService(IOptions<NotificationSettings> options)
{
_settings = options.Value;
}
}
Use it for: 절대 변하지 않는 설정 – 예: SMTP 서버, 데이터베이스 URL, 기본 발신자 이메일.
The catch: 설정 파일을 변경하면 변경 사항을 보려면 앱을 재시작해야 합니다.
📌 IOptionsSnapshot – 요청‑친화적인 버전
이것을 … 라고 생각해 보세요: 누군가 물어볼 때마다 메시지를 확인하는 것과 같습니다.
동작 방식: 들어오는 각 HTTP 요청마다 최신 설정을 가져옵니다.
예시
public class NotificationController : ControllerBase
{
private readonly IOptionsSnapshot<NotificationSettings> _prefs;
public NotificationController(IOptionsSnapshot<NotificationSettings> prefs)
{
_prefs = prefs;
}
[HttpPost("send")]
public IActionResult SendNotification()
{
// Gets fresh preferences for each request
var enableEmail = _prefs.Value.EnableEmailNotifications;
var enableSms = _prefs.Value.EnableSmsNotifications;
// Send based on current preferences
// …
return Ok();
}
}
다음과 같은 경우에 사용하세요: 사용자 선호도, 알림 토글, A/B 테스트 등—각 요청마다 다른 설정이 필요할 수 있는 상황.
주의점: 웹 요청에서만 동작하며, 백그라운드 작업에서는 사용할 수 없고, 매 요청마다 설정을 읽습니다.
📌 IOptionsMonitor – 실시간 업데이트 버전
Think of it as: 실시간 알림이 켜진 것과 같습니다 – 무언가 변경되면 즉시 알 수 있습니다.
What it does: 구성 파일을 감시하고 변경 시 자동으로 업데이트합니다 (재시작 필요 없음).
Example
public class NotificationBackgroundService : BackgroundService
{
private readonly IOptionsMonitor<NotificationLimits> _monitor;
public NotificationBackgroundService(IOptionsMonitor<NotificationLimits> monitor)
{
_monitor = monitor;
// Runs automatically when config changes!
_monitor.OnChange(thresholds =>
{
Console.WriteLine("Notification thresholds updated!");
UpdateNotificationRules(thresholds);
});
}
protected override async Task ExecuteAsync(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
// Use current threshold values
var maxPerHour = _monitor.CurrentValue.MaxNotificationsPerHour;
await CheckAndSendNotifications(maxPerHour);
await Task.Delay(TimeSpan.FromMinutes(5), token);
}
}
}
Use it for: 백그라운드 작업, 예약 작업, 또는 재시작 없이 실시간 업데이트가 필요한 모든 시나리오에 사용합니다.
The catch: 설정이 약간 복잡하고 메모리를 약간 더 사용합니다.
🎯 어떻게 선택할까?
스스로에게 간단한 질문을 해보세요: “앱이 실행되는 동안 내 설정이 변경될까요?”
| 답변 | 권장 옵션 |
|---|---|
| 아니오 | IOptions – 간단하고 빠름 |
| 예 | 웹 요청 또는 백그라운드 작업에 해당하나요? |
| 웹 요청 | IOptionsSnapshot |
| 백그라운드 작업 | IOptionsMonitor |
⚠️ 내가 저지른 실수 (당신은 피할 수 있도록)
| 실수 | 왜 문제가 되는가 |
|---|---|
백그라운드 서비스에서 IOptionsSnapshot 사용 | 스코프가 있는 웹 요청에만 동작합니다 |
IOptionsSnapshot.Value를 변수에 저장 | “요청마다 새로 고침” 이점을 잃게 됩니다 |
IOptionsMonitor와 함께 OnChange를 듣지 않음 | 설정이 변경될 때 알 수 없습니다 |
| 설정 검증을 하지 않음 | 설정이 잘못되면 앱이 임의의 시점에 충돌할 수 있습니다 |
💡 미리 알았으면 좋았을 멋진 트릭들
트릭 #1 – 같은 유형의 여러 설정
services.Configure<EmailSettings>("Email", config.GetSection("Email"));
services.Configure<SmsSettings>("SMS", config.GetSection("SMS"));
services.Configure<PushSettings>("Push", config.GetSection("Push"));
// Later in your code:
var emailSettings = _options.Get<EmailSettings>("Email");
var smsSettings = _options.Get<SmsSettings>("SMS");
트릭 #2 – 시작 시 설정 검증
services.AddOptions<NotificationSettings>()
.Validate(settings => !string.IsNullOrEmpty(settings.SmtpServer),
"Hey! You forgot the SMTP Server!")
.Validate(settings => settings.Port > 0 && settings.Port (Configuration.GetSection("NotificationLimits"));
사용 (예: 백그라운드 서비스에서)
public class NotificationWorker : BackgroundService
{
private readonly IOptionsMonitor<NotificationLimits> _limits;
public NotificationWorker(IOptionsMonitor<NotificationLimits> limits)
{
_limits = limits;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
var limits = _limits.CurrentValue;
// Use limits.MaxPerUser and limits.TimeWindowHours …
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
}
}
}
{
"NotificationLimits": {
"MaxPerUser": 5,
"EnableEmailNotifications": true,
"EnableSmsNotifications": false
}
}
// When starting your app
builder.Services.Configure<NotificationLimits>(
builder.Configuration.GetSection("NotificationLimits"));
백그라운드 서비스 – 실시간 업데이트 필요!
public class NotificationQueueService : BackgroundService
{
private readonly IOptionsMonitor<NotificationLimits> _monitor;
public NotificationQueueService(IOptionsMonitor<NotificationLimits> monitor)
{
_monitor = monitor;
}
protected override async Task ExecuteAsync(CancellationToken token)
{
// Update limits immediately when config changes
_monitor.OnChange(limits =>
{
Console.WriteLine($"Notification limits updated to {limits.MaxPerUser} per user");
UpdateQueueProcessor(limits);
});
while (!token.IsCancellationRequested)
{
await ProcessNotificationQueue(_monitor.CurrentValue);
await Task.Delay(TimeSpan.FromSeconds(10), token);
}
}
}
API 엔드포인트 – 요청마다 최신 데이터 필요
public class NotificationController : ControllerBase
{
private readonly IOptionsSnapshot<NotificationLimits> _limits;
public NotificationController(IOptionsSnapshot<NotificationLimits> limits)
{
_limits = limits;
}
[HttpPost("send")]
public async Task<IActionResult> SendNotification([FromBody] NotificationRequest request)
{
var maxPerUser = _limits.Value.MaxPerUser;
if (await CheckUserNotificationCount(request.UserId) >= maxPerUser)
return BadRequest("Notification limit reached");
// Each request gets current limits
await SendNotificationAsync(request);
return Ok();
}
}
🎓 이것을 기억하세요
| 옵션 | 사용 시점 | 비유 |
|---|---|---|
| IOptions | 한 번 작성하고 영원히 읽음 | 문신 – 한 번 새기면 영구적 |
| IOptionsSnapshot | 매 요청마다 최신 데이터 | 매일 아침 날씨 확인 |
| IOptionsMonitor | 설정이 변경될 때 실시간 업데이트 | 실시간으로 업데이트되는 소셜 미디어 피드 |
이것들을 사용해 본 적이 있나요?
가장 헷갈렸던 것은 무엇인가요? 아래에 댓글을 남겨 주세요! 👇
이 내용이 도움이 되었으면 나중에 저장해 두세요 – 새벽 2시에 디버깅할 때 스스로에게 감사하게 될 거예요!