.NET에서 IOptions를 잘못 사용하는 것을 멈추세요!

발행: (2025년 12월 27일 오전 03:41 GMT+9)
12 min read
원문: Dev.to

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시에 디버깅할 때 스스로에게 감사하게 될 거예요!

Back to Blog

관련 글

더 보기 »