ASP.NET Core에서의 의존성 주입 기법과 서비스 라이프사이클 관리

의존성 주입의 핵심 원리

의존성 주입(Dependency Injection, DI)은 객체 간 결합도를 낮추기 위한 설계 패턴으로, 클래스가 필요한 종속 항목을 외부에서 제공받는 방식이다. 이는 직접 인스턴스화하거나 정적 참조를 사용하는 대신, 생성자나 메서드를 통해 의존성을 전달함으로써 유연성과 테스트 용이성을 확보한다.

대표적인 구현 방식인 생성자 주입은 클래스가 요구하는 의존성을 명시적으로 선언하여, '의존성 역전 원칙'(Dependency Inversion Principle)을 따르며, 고수준 모듈이 저수준 모듈에 의존하지 않고 모두 추상화에 의존하도록 한다.

ASP.NET Core 내장 의존성 컨테이너 활용

ASP.NET Core는 기본적으로 IServiceCollection를 통해 서비스를 등록하고 관리하는 내장 컨테이너를 제공한다. 이 컨테이너는 애플리케이션 시작 시 Startup.ConfigureServices 메서드에서 구성되며, 플랫폼 기능(예: Entity Framework Core, MVC)뿐만 아니라 사용자 정의 서비스도 포함할 수 있다.

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<AppDbContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

    services.AddTransient<IUserService, UserService>();
}

사용자 정의 서비스 등록 및 주입

서비스를 등록할 때는 일반적으로 인터페이스를 첫 번째 매개변수로, 구현 클래스를 두 번째 매개변수로 지정한다. 예를 들어:

public interface IUserService
{
    Task<List<UserDto>> GetAllUsers();
}

public class UserService : IUserService
{
    private readonly AppDbContext _context;

    public UserService(AppDbContext context)
    {
        _context = context;
    }

    public async Task<List<UserDto>> GetAllUsers()
    {
        return await _context.Users.Select(u => new UserDto { Name = u.Name }).ToListAsync();
    }
}

이후 컨테이너에 등록하고, 컨트롤러에서 생성자 주입을 통해 사용 가능하다:

public class UserController : Controller
{
    private readonly IUserService _userService;

    public UserController(IUserService userService)
    {
        _userService = userService;
    }

    public async Task<ActionResult> Index()
    {
        var users = await _userService.GetAllUsers();
        return View(users);
    }
}

서비스 생명주기 유형

서비스 등록 시 각각의 생명주기를 선택해야 하며, 다음과 같은 세 가지 유형이 존재한다:

  • Transient (순환): 요청 시마다 새 인스턴스 생성. 상태 없는 가벼운 서비스에 적합.
  • Scoped (범위): HTTP 요청 단위로 한 번만 생성. 요청 내에서 동일한 인스턴스 재사용.
  • Singleton (단일체): 처음 요청 시 생성 후 이후 모든 요청에서 동일 인스턴스 공유. 전역 상태 저장에 사용.

실제 동작 확인 예제

다음은 각 생명주기의 차이를 확인하기 위한 예제 코드다:

public interface IOperation
{
    Guid Id { get; }
}

public class Operation : IOperation
{
    public Operation() => Id = Guid.NewGuid();

    public Guid Id { get; }
}

// Startup.cs
services.AddTransient<IOperation, Operation>();
services.AddScoped<IOperation, Operation>();
services.AddSingleton<IOperation, Operation>();

컨트롤러에서 각 유형의 인스턴스를 주입하면, 뷰에서 Id 값을 비교해 동작을 확인할 수 있다. 특히 Singleton은 동일한 값이 반복되고, Scoped는 요청 내에서 일관된 값이 유지된다.

요청 범위 내 서비스 접근

HTTP 요청 내에서 특정 서비스를 얻고자 할 경우, HttpContext.RequestServices를 통해 컨텍스트 내 서비스를 검색할 수 있다. 이는 ApplicationServices와 달리 요청에 맞춰 동적으로 해석되는 서비스 집합이다.

좋은 디자인 팁

의존성 주입을 사용할 때는 다음 원칙을 준수하자:

  • 정적 메서드나 new 연산자를 통한 직접 인스턴스화를 피하라. (즉, New is Glue)
  • 의존성이 너무 많다면 클래스가 너무 많은 책임을 지고 있다는 신호. 단일 책임 원칙을 고려해 분리 필요.
  • Controller는 UI 처리에 집중하고, 비즈니스 로직이나 데이터 접근은 별도의 서비스 계층에서 처리.

외부 컨테이너 사용 (Autofac 예시)

내장 컨테이너 외에도, Autofac 같은 강력한 컨테이너를 선택할 수 있다. 이를 위해 Autofac.Extensions.DependencyInjection 패키지를 설치하고, Startup 클래스에서 컨테이너를 교체할 수 있다.

public class Startup
{
    public IContainer ApplicationContainer { get; private set; }

    public void ConfigureContainer(ContainerBuilder builder)
    {
        builder.RegisterType<UserService>().As<IUserService>();
        builder.RegisterType<AppDbContext>().InstancePerLifetimeScope();
    }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc();
    }
}

태그: ASP.NET Core Dependency Injection Service Lifetime Autofac IoC Container

7월 1일 01:29에 게시됨