콘텐츠로 이동

ASP.NET Core DI 내부 구조 — 서비스 컨테이너 심화

ASP.NET Core는 Microsoft.Extensions.DependencyInjection을 내장 IoC 컨테이너로 사용합니다. 단순한 등록과 해결(resolve)을 넘어 내부 동작을 이해하면 수명 충돌, 성능 문제, 고급 패턴을 올바르게 적용할 수 있습니다.


builder.Services.AddSingleton<ICache, MemoryCache>(); // 앱 생명주기 동안 1개
builder.Services.AddScoped<IDbContext, AppDbContext>(); // 요청당 1개
builder.Services.AddTransient<IEmailSender, SmtpSender>(); // 요청마다 새 인스턴스
// 위험: Singleton이 Scoped를 주입받으면 Scoped가 Singleton처럼 동작
public class MySingleton(IScopedService scoped) { } // 잘못된 패턴
// 개발 환경에서 ValidateScopes 활성화로 감지
builder.Host.UseDefaultServiceProvider(opt =>
{
opt.ValidateScopes = builder.Environment.IsDevelopment();
opt.ValidateOnBuild = true;
});

// 람다 팩토리
builder.Services.AddSingleton<IConnection>(sp =>
{
var config = sp.GetRequiredService<IConfiguration>();
return new SqlConnection(config["ConnectionString"]);
});
// ImplementationFactory로 조건 분기
builder.Services.AddScoped<IPaymentGateway>(sp =>
{
var env = sp.GetRequiredService<IHostEnvironment>();
return env.IsProduction()
? new StripeGateway(sp.GetRequiredService<StripeOptions>())
: new MockGateway();
});

// 등록
builder.Services.AddKeyedSingleton<IStorage, S3Storage>("cloud");
builder.Services.AddKeyedSingleton<IStorage, LocalStorage>("local");
// 해결
public class FileService(
[FromKeyedServices("cloud")] IStorage cloudStorage,
[FromKeyedServices("local")] IStorage localStorage)
{ }
// 직접 해결
var cloud = sp.GetRequiredKeyedService<IStorage>("cloud");

4. IServiceScopeFactory — Singleton에서 Scoped 사용

섹션 제목: “4. IServiceScopeFactory — Singleton에서 Scoped 사용”
public class BackgroundWorker(IServiceScopeFactory scopeFactory)
{
public async Task ProcessAsync()
{
// 새 스코프 생성 → Scoped 서비스 안전하게 사용
await using var scope = scopeFactory.CreateAsyncScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await db.SaveChangesAsync();
}
}

// IRepository<T> 를 한 번에 등록
builder.Services.AddScoped(typeof(IRepository<>), typeof(EfRepository<>));
// 사용
public class ProductService(IRepository<Product> repo) { }
public class OrderService(IRepository<Order> repo) { }

// appsettings.json의 "Email" 섹션을 EmailOptions에 바인딩
builder.Services.Configure<EmailOptions>(
builder.Configuration.GetSection("Email"));
// 주입
public class EmailService(IOptions<EmailOptions> options)
{
private readonly EmailOptions _opts = options.Value;
}
// 런타임 변경 감지
public class DynamicService(IOptionsMonitor<AppOptions> monitor)
{
public void OnChange() => monitor.OnChange(opts => Reload(opts));
}

// GetRequiredService vs GetService
// GetService는 null 반환 → null 체크 필요
// GetRequiredService는 없으면 예외 → 안전
// 빌드 시 유효성 검사
builder.Host.UseDefaultServiceProvider(opt =>
opt.ValidateOnBuild = true); // 등록 오류를 시작 시점에 발견
// ActivatorUtilities — 일부 매개변수만 DI로 해결
var obj = ActivatorUtilities.CreateInstance<MyService>(sp, "extra-param");

// 기존 등록을 래핑
builder.Services.AddSingleton<IOrderService, OrderService>();
builder.Services.Decorate<IOrderService, LoggingOrderService>();
// Scrutor 라이브러리 사용 시:
// services.Decorate<IOrderService>((inner, sp) =>
// new LoggingOrderService(inner, sp.GetRequiredService<ILogger>()));

ASP.NET Core DI는 단순 등록을 넘어 Keyed Services, 오픈 제네릭, IServiceScopeFactory 등 다양한 고급 패턴을 지원합니다. 수명 충돌은 ValidateScopesValidateOnBuild로 개발 단계에서 조기 발견하고, Singleton에서 Scoped 서비스가 필요하면 반드시 IServiceScopeFactory를 사용하세요.