Dependency Injection 라이프타임 이해: Singleton, Scoped, 및 Transient

발행: (2025년 12월 3일 오후 07:55 GMT+9)
6 min read
원문: Dev.to

Source: Dev.to

싱글톤

주요 특징

  • 전체 애플리케이션 수명 동안 하나의 인스턴스
  • 모든 요청 간에 공유
  • 최초 요청 시 한 번 생성
  • 애플리케이션 종료 시 폐기

사용 시점

  • 상태 비저장 서비스(인스턴스별 데이터 없음)
  • 생성 비용이 큰 서비스(캐시, HTTP 클라이언트)
  • 공유 자원(구성, 로깅, 캐싱)
// Register as singleton
services.AddSingleton();
services.AddSingleton();

예시: 캐시 서비스

public class CacheService : ICacheService
{
    private readonly Dictionary _cache = new();
    private readonly object _lock = new();

    public void Set(string key, T value)
    {
        lock (_lock) { _cache[key] = value; }
    }

    public T Get(string key)
    {
        lock (_lock)
        {
            return _cache.TryGetValue(key, out var value) ? (T)value : default;
        }
    }
}

주의 사항

  • 공유된 가변 상태
  • 스레드 안전성 문제(적절한 동기화 필요)
  • 비관리 리소스를 보유할 때 IDisposable 구현
// BAD: Singleton with mutable state
public class BadService
{
    public string CurrentUserId { get; set; } // Shared across all requests!
}

스코프드

주요 특징

  • 스코프당 하나의 인스턴스(보통 HTTP 요청당)
  • 같은 스코프 내에서는 공유, 스코프 간에는 다름
  • 스코프 시작 시 생성
  • 스코프 종료 시 폐기

사용 시점

  • 데이터베이스 컨텍스트(예: Entity Framework DbContext)
  • 요청별로 상태가 필요한 서비스
  • Unit‑of‑Work 패턴
// Register as scoped
services.AddScoped();
services.AddScoped();

예시: 데이터베이스 컨텍스트 및 주문 서비스

public class ApplicationDbContext : DbContext
{
    public DbSet Orders { get; set; }
}

public class OrderService
{
    private readonly ApplicationDbContext _context;

    public OrderService(ApplicationDbContext context) => _context = context;

    public async Task CreateOrder(Order order)
    {
        _context.Orders.Add(order);
        await _context.SaveChangesAsync();
        return order;
    }
}

주의 사항

  • 스코프드 서비스를 싱글톤에 주입하면 수명 불일치 발생
// BAD: Singleton depending on scoped service
public class BadSingleton
{
    private readonly IScopedService _scoped; // Error!
}

// GOOD: Resolve scoped service via IServiceProvider
public class GoodSingleton
{
    private readonly IServiceProvider _serviceProvider;

    public GoodSingleton(IServiceProvider serviceProvider) => _serviceProvider = serviceProvider;

    public void DoWork()
    {
        using var scope = _serviceProvider.CreateScope();
        var scoped = scope.ServiceProvider.GetRequiredService();
        scoped.DoWork();
    }
}
  • 적절한 스코프를 만들지 않고 백그라운드 작업에서 스코프드 서비스를 캡처하지 말 것.

트랜지언트

주요 특징

  • 요청될 때마다 새로운 인스턴스
  • 소비자 간에 공유되지 않음
  • 필요 시 생성, 스코프를 벗어나면 폐기(가비지 컬렉션)
  • 세 옵션 중 가장 짧은 수명

사용 시점

  • 가볍고 생성 비용이 낮은 서비스
  • 상태 비저장 작업
  • 공유하면 안 되는 서비스(공유 시 버그 발생)
// Register as transient
services.AddTransient();
services.AddTransient, OrderValidator>();

예시: 주문 검증기

public class OrderValidator : IValidator
{
    public ValidationResult Validate(Order order)
    {
        var result = new ValidationResult();

        if (order.Items == null || order.Items.Count == 0)
            result.AddError("Order must have at least one item");

        return result;
    }
}

주의 사항

  • 비용이 많이 드는 서비스를 트랜지언트로 사용하면 성능에 영향
  • 트랜지언트 인스턴스를 static 변수에 보관하면 메모리 누수
  • 싱글톤으로 충분한 경우 불필요한 생성

비교 표

항목싱글톤스코프드트랜지언트
인스턴스 수전체 앱당 하나스코프당 하나요청 시마다 새로 생성
수명애플리케이션 수명스코프 수명(요청당)가장 짧음 – 스코프 종료 시 폐기
메모리 사용량낮음(공유)중간높음(많은 인스턴스)
스레드 안전성스레드 안전 필요보통 필요 없음보통 필요 없음
폐기 시점앱 종료 시스코프 종료 시가비지 컬렉션 시
일반적인 사용상태 비저장, 비용 많이 드는 서비스요청별 서비스가볍고 상태 비저장 서비스

서비스 등록 예시

public void ConfigureServices(IServiceCollection services)
{
    // Singleton: Shared across all requests
    services.AddSingleton();

    // Scoped: One per HTTP request
    services.AddScoped();

    // Transient: New instance each time
    services.AddTransient();
}

백그라운드 워커 예시 (호스티드 서비스에서 스코프드 서비스)

public class BackgroundWorker : BackgroundService
{
    private readonly IServiceProvider _serviceProvider;

    public BackgroundWorker(IServiceProvider serviceProvider) =>
        _serviceProvider = serviceProvider;

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            // Create a scope for each iteration
            using var scope = _serviceProvider.CreateScope();
            var scopedService = scope.ServiceProvider.GetRequiredService();
            await scopedService.DoWorkAsync();

            await Task.Delay(1000, stoppingToken);
        }
    }
}

수명 의존성 규칙

  • 서비스는 동일하거나 더 긴 수명의 서비스에만 의존할 수 있다.
서비스 수명허용되는 의존성
싱글톤싱글톤
스코프드싱글톤, 스코프드
트랜지언트싱글톤, 스코프드, 트랜지언트

흔한 실수

// BAD: Singleton depending on scoped service
public class SingletonService
{
    private readonly IScopedService _scoped; // Error!
}

// GOOD: Resolve scoped service via IServiceProvider when needed
public class SingletonService
{
    private readonly IServiceProvider _serviceProvider;

    public SingletonService(IServiceProvider serviceProvider) =>
        _serviceProvider = serviceProvider;

    public void DoWork()
    {
        using var scope = _serviceProvider.CreateScope();
        var scoped = scope.ServiceProvider.GetRequiredService();
        // Use scoped service...
    }
}
Back to Blog

관련 글

더 보기 »

Powershell은 은근히 멋지다

PowerShell는 더 인기 있는 쉘에 비해 종종 간과되지만, 로컬 개발 워크플로를 간소화할 수 있는 강력한 기능을 제공합니다. 아래는 ...