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...
}
}