의존성 주입 패턴은(는) 무엇을 하나요?
이 스킬을 사용해야 할 때
다음 상황에서 이 스킬을 사용하세요:
- ASP.NET Core 애플리케이션에서 서비스 등록을 체계화할 때
- 수백 개의 등록이 있는 Program.cs/Startup.cs 파일을 방지할 때
- 프로덕션과 테스트 간에 서비스 구성을 재사용할 때
- Microsoft.Extensions.DependencyInjection과 통합되는 라이브러리를 설계할 때
참고 파일
- advanced-patterns.md: DI 확장을 사용한 테스트, Akka.NET 액터 범위 관리, 조건부/팩토리/키 기반 등록 패턴
문제
체계화 없이 Program.cs는 관리 불가능해집니다:
// BAD: 200줄 이상의 비체계적 등록
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<IUserRepository, UserRepository>();
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
builder.Services.AddScoped<IProductRepository, ProductRepository>();
builder.Services.AddScoped<IUserService, UserService>();
// ... 150줄 더 ...
문제점: 관련 등록을 찾기 어렵고, 명확한 경계가 없으며, 테스트에서 재사용할 수 없고, 병합 충돌이 발생합니다.
해결책: 확장 메서드 구성
관련 등록을 확장 메서드로 그룹화하세요:
// GOOD: 깔끔하고 구성 가능한 Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddUserServices()
.AddOrderServices()
.AddEmailServices()
.AddPaymentServices()
.AddValidators();
var app = builder.Build();
확장 메서드 패턴
기본 구조
namespace MyApp.Users;
public static class UserServiceCollectionExtensions
{
public static IServiceCollection AddUserServices(this IServiceCollection services)
{
services.AddScoped<IUserRepository, UserRepository>();
services.AddScoped<IUserReadStore, UserReadStore>();
services.AddScoped<IUserWriteStore, UserWriteStore>();
services.AddScoped<IUserService, UserService>();
services.AddScoped<IUserValidationService, UserValidationService>();
return services;
}
}
구성 포함
namespace MyApp.Email;
public static class EmailServiceCollectionExtensions
{
public static IServiceCollection AddEmailServices(
this IServiceCollection services,
string configSectionName = "EmailSettings")
{
services.AddOptions<EmailOptions>()
.BindConfiguration(configSectionName)
.ValidateDataAnnotations()
.ValidateOnStart();
services.AddSingleton<IMjmlTemplateRenderer, MjmlTemplateRenderer>();
services.AddSingleton<IEmailLinkGenerator, EmailLinkGenerator>();
services.AddScoped<IUserEmailComposer, UserEmailComposer>();
services.AddScoped<IEmailSender, SmtpEmailSender>();
return services;
}
}
파일 구성
확장 메서드를 등록하는 서비스 근처에 배치하세요:
src/
MyApp.Api/
Program.cs # 모든 Add* 메서드 구성
MyApp.Users/
Services/
UserService.cs
UserServiceCollectionExtensions.cs # AddUserServices()
MyApp.Orders/
OrderServiceCollectionExtensions.cs # AddOrderServices()
MyApp.Email/
EmailServiceCollectionExtensions.cs # AddEmailServices()
규칙: {Feature}ServiceCollectionExtensions.cs를 해당 기능의 서비스 옆에 배치합니다.
명명 규칙
| 패턴 | 용도 |
|---|---|
Add{Feature}Services() | 일반 기능 등록 |
Add{Feature}() | 모호하지 않을 때 짧은 형태 |
Configure{Feature}() | 주로 옵션 설정 시 |
Use{Feature}() | 미들웨어 (IApplicationBuilder에서) |
테스트 이점
Add* 패턴을 사용하면 테스트에서 프로덕션 구성을 재사용하고 다른 부분만 재정의할 수 있습니다. WebApplicationFactory, Akka.Hosting.TestKit 및 독립형 ServiceCollection과 함께 작동합니다.
전체 테스트 예제는 advanced-patterns.md를 참조하세요.
계층적 확장
대규모 애플리케이션의 경우 확장을 계층적으로 구성하세요:
public static class AppServiceCollectionExtensions
{
public static IServiceCollection AddAppServices(this IServiceCollection services)
{
return services
.AddDomainServices()
.AddInfrastructureServices()
.AddApiServices();
}
}
public static class DomainServiceCollectionExtensions
{
public static IServiceCollection AddDomainServices(this IServiceCollection services)
{
return services
.AddUserServices()
.AddOrderServices()
.AddProductServices();
}
}
Akka.Hosting 통합
동일한 패턴이 Akka.NET 액터 구성에도 적용됩니다:
public static class OrderActorExtensions
{
public static AkkaConfigurationBuilder AddOrderActors(
this AkkaConfigurationBuilder builder)
{
return builder
.WithActors((system, registry, resolver) =>
{
var orderProps = resolver.Props<OrderActor>();
var orderRef = system.ActorOf(orderProps, "orders");
registry.Register<OrderActor>(orderRef);
});
}
}
// Program.cs에서 사용
builder.Services.AddAkka("MySystem", (builder, sp) =>
{
builder
.AddOrderActors()
.AddInventoryActors()
.AddNotificationActors();
});
전체 Akka.Hosting 패턴은 akka-hosting-actor-patterns 스킬을 참조하세요.
안티 패턴
하지 말아야 할 것: 모든 것을 Program.cs에 등록
// BAD: 200줄 이상의 등록이 있는 거대한 Program.cs
하지 말아야 할 것: 지나치게 일반적인 확장 생성
// BAD: 너무 모호하며, 무엇이 등록되는지 전달하지 못함
public static IServiceCollection AddServices(this IServiceCollection services) { ... }
하지 말아야 할 것: 중요한 구성 숨기기
// BAD: 설정이 묻혀 있음
public static IServiceCollection AddDatabase(this IServiceCollection services)
{
services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer("하드코딩된-연결-문자열")); // 숨겨짐!
}
// GOOD: 구성을 명시적으로 받음
public static IServiceCollection AddDatabase(
this IServiceCollection services,
string connectionString)
{
services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(connectionString));
}
모범 사례 요약
| 사례 | 이점 |
|---|---|
관련 서비스를 Add* 메서드로 그룹화 | 깔끔한 Program.cs, 명확한 경계 |
| 확장 메서드를 등록하는 서비스 근처에 배치 | 쉽게 찾고 유지 관리 가능 |
체이닝을 위해 IServiceCollection 반환 | 유창한 API |
| 구성 매개변수 허용 | 유연성 |
일관된 명명 사용 (Add{Feature}Services) | 검색 가능성 |
| 프로덕션 확장을 재사용하여 테스트 | 자신감, 중복 감소 |
수명 관리
| 수명 | 사용 시기 | 예시 |
|---|---|---|
| Singleton | 상태 비저장, 스레드 안전, 생성 비용 높음 | 구성, HttpClient 팩토리, 캐시 |
| Scoped | 요청별 상태 저장, 데이터베이스 컨텍스트 | DbContext, 리포지토리, 사용자 컨텍스트 |
| Transient | 가벼움, 상태 저장, 생성 비용 낮음 | 유효성 검사기, 단기 헬퍼 |
// SINGLETON: 상태 비저장 서비스, 안전하게 공유
services.AddSingleton<IMjmlTemplateRenderer, MjmlTemplateRenderer>();
// SCOPED: 데이터베이스 액세스, 요청별 상태
services.AddScoped<IUserRepository, UserRepository>();
// TRANSIENT: 저렴하고 단기적
services.AddTransient<CreateUserRequestValidator>();
Scoped 서비스에는 범위가 필요합니다. ASP.NET Core는 HTTP 요청당 하나의 범위를 생성합니다. 백그라운드 서비스와 액터에서는 수동으로 범위를 생성하세요.
액터 범위 관리 패턴은 advanced-patterns.md를 참조하세요.
일반적인 실수
Singleton에 Scoped 주입
// BAD: Singleton이 Scoped 서비스를 캡처 - 오래된 DbContext!
public class CacheService // Singleton으로 등록됨
{
private readonly IUserRepository _repo; // Scoped - 시작 시 캡처됨!
}
// GOOD: IServiceProvider를 주입하고, 작업마다 범위 생성
public class CacheService
{
private readonly IServiceProvider _serviceProvider;
public async Task<User> GetUserAsync(string id)
{
using var scope = _serviceProvider.CreateScope();
var repo = scope.ServiceProvider.GetRequiredService<IUserRepository>();
return await repo.GetByIdAsync(id);
}
}
백그라운드 작업에서 범위 누락
// BAD: Scoped 서비스에 대한 범위 없음
public class BadBackgroundService : BackgroundService
{
private readonly IOrderService _orderService; // Scoped - 예외 발생!
}
// GOOD: 각 작업 단위에 대해 범위 생성
public class GoodBackgroundService : BackgroundService
{
private readonly IServiceScopeFactory _scopeFactory;
protected override async Task ExecuteAsync(CancellationToken ct)
{
using var scope = _scopeFactory.CreateScope();
var orderService = scope.ServiceProvider.GetRequiredService<IOrderService>();
// ...
}
}
리소스
- Microsoft.Extensions.DependencyInjection: https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection
- Akka.Hosting: https://github.com/akkadotnet/Akka.Hosting
- Akka.DependencyInjection: https://getakka.net/articles/actors/dependency-injection.html
- Options 패턴:
microsoft-extensions-configuration스킬 참조