컨트롤러: 의존성 주입과 컨트롤러

등록일시: 2016-11-07 08:00,  수정일시: 2016-11-25 14:24
조회수: 8,553
이 문서는 ASP.NET Core MVC 기술을 널리 알리고자 하는 개인적인 취지로 제공되는 번역문서입니다. 이 문서에 대한 모든 저작권은 마이크로소프트에 있으며 요청이 있을 경우 언제라도 게시가 중단될 수 있습니다. 번역 내용에 오역이 존재할 수 있고 주석은 번역자 개인의 의견일 뿐이며 마이크로소프트는 이에 관한 어떠한 보장도 하지 않습니다. 번역이 완료된 이후에도 대상 제품 및 기술이 개선되거나 변경됨에 따라 원문의 내용도 변경되거나 보완되었을 수 있으므로 주의하시기 바랍니다.
본문에서는 생성자 매개변수 또는 액션 매개변수를 통해서 의존성 주입으로 컨트롤러에 서비스를 제공하는 방법을 살펴봅니다.

ASP.NET Core MVC의 컨트롤러는 생성자를 통해서 자신의 의존성을 명시적으로 요청합니다. 그러나 때에 따라서는 개별 컨트롤러 액션에서만 서비스가 필요한 경우도 있으며, 그런 경우까지 컨트롤러 수준에서 서비스를 요청하는 것은 적절하지 않을 수도 있습니다. 대신, 액션 메서드의 매개변수로 서비스를 주입하는 방법을 선택할 수 있습니다.

GitHub에서 샘플 코드 확인 및 다운로드 받기

의존성 주입

의존성 주입(Dependency Injection)은 의존성 역전 원칙(Dependency Inversion Principle)을 기반으로 느슨하게 결합된 모듈들을 이용해서 응용 프로그램을 구성할 수 있는 기법입니다. ASP.NET Core는 응용 프로그램의 테스트와 유지보수를 보다 용이하게 만들어주는 의존성 주입 기능을 내장하고 있습니다.

생성자 주입

ASP.NET Core에 내장된 생성자 기반의 의존성 주입은 MVC 컨트롤러의 기능을 확장시켜줍니다. 간단히 컨트롤러의 생성자 매개변수로 서비스 형식을 추가하기만 하면, ASP.NET Core가 내장 서비스 컨테이너를 이용해서 해당 형식을 해결해줍니다. 일반적으로 서비스는 인터페이스를 이용해서 정의됩니다 (반드시 그런 것은 아닙니다). 가령, 현재 시간에 따라서 좌우되는 업무 로직이 응용 프로그램에 존재할 경우, 시간을 조회하는 서비스를 주입할 수 있으며 (하드코딩 하는 대신), 이를 활용하면 테스트 코드에서 시간을 설정하기 위해 사용되는 구현을 전달할 수 있습니다.

using System;

namespace ControllerDI.Interfaces
{
    public interface IDateTime
    {
        DateTime Now { get; }
    }
}

런타임에 시스템 시간을 이용해서 현재 시간을 가져오도록 인터페이스를 구현하는 작업은 다음처럼 간단합니다:

using System;
using ControllerDI.Interfaces;

namespace ControllerDI.Services
{
    public class SystemDateTime : IDateTime
    {
        public DateTime Now
        {
            get { return DateTime.Now; }
        }
    }
}

이제, 이 서비스를 컨트롤러에서 사용할 수 있습니다. 본문의 예제에서는 하루 중 시간에 따라서 사용자에게 인사말을 출력하는 약간의 로직을 HomeControllerIndex 메서드에 추가해보겠습니다.

using ControllerDI.Interfaces;
using Microsoft.AspNetCore.Mvc;

namespace ControllerDI.Controllers
{
    public class HomeController : Controller
    {
        private readonly IDateTime _dateTime;

        public HomeController(IDateTime dateTime)
        {
            _dateTime = dateTime;
        }

        public IActionResult Index()
        {
            var serverTime = _dateTime.Now;
            if (serverTime.Hour < 12)
            {
                ViewData["Message"] = "It's morning here - Good Morning!";
            }
            else if (serverTime.Hour < 17)
            {
                ViewData["Message"] = "It's afternoon here - Good Afternoon!";
            }
            else
            {
                ViewData["Message"] = "It's evening here - Good Evening!";
            }
            return View();
        }
    }
}

그러나 지금 상태로 응용 프로그램을 실행하면 다음과 같은 오류가 발생합니다:

An unhandled exception occurred while processing the request.

InvalidOperationException: Unable to resolve service for type 'ControllerDI.Interfaces.IDateTime' while attempting to activate 'ControllerDI.Controllers.HomeController'.
Microsoft.Extensions.DependencyInjection.ActivatorUtilities.GetService(IServiceProvider sp, Type type, Type requiredBy, Boolean isDefaultParameterRequired)

이 오류 메시지는 Startup 클래스의 ConfigureServices 메서드에서 서비스를 구성하지 않았기 때문에 발생하는 메시지입니다. IDateTime 형식에 대한 요청을 SystemDateTime 클래스의 인스턴스를 이용해서 해결하도록 지정하려면, 다음 코드에 강조된 줄을 ConfigureServices 메서드에 추가해야 합니다:

public void ConfigureServices(IServiceCollection services)
{
    // Add application services.
    services.AddTransient<IDateTime, SystemDateTime>();
}

노트

몇 가지 수명 옵션들 중 한 가지 옵션을 사용하도록 서비스를 구현할 수 있습니다 (Transient, Scoped, 또는 Singleton). 각각의 범위 옵션들이 서비스의 동작에 어떤 영향을 주는지에 관해서는 의존성 주입 문서를 참고하시기 바랍니다.

올바르게 서비스를 구성한 다음 응용 프로그램을 실행하고 Home 페이지로 이동해보면 기대한 것처럼 시간에 따른 메시지가 출력됩니다:

컨트롤러에서 명시적으로 의존성을 요청해서(Explicit Dependencies Principle) 코드를 테스트하기 쉽게 만드는 방법에 대해서는 컨트롤러 로직 테스트하기 문서를 참고하시기 바랍니다.

ASP.NET Core의 내장 의존성 주입 기능은 하나의 생성자가 존재하는 클래스에 대해서만 서비스 요청을 지원합니다. 만약 생성자가 여러개 존재할 경우에는 다음과 같은 예외가 발생하게 됩니다:

An unhandled exception occurred while processing the request.

InvalidOperationException: Multiple constructors accepting all given argument types have been found in type 'ControllerDI.Controllers.HomeController'. There should only be one applicable constructor.
Microsoft.Extensions.DependencyInjection.ActivatorUtilities.FindApplicableConstructor(Type instanceType, Type[] argumentTypes, ConstructorInfo& matchingConstructor, Nullable`1[]& parameterMap)

이 문제는 오류 메시지의 내용처럼 생성자가 하나만 존재하도록 클래스를 수정하면 해결할 수 있습니다. 또는 여러 개의 생성자를 지원하는 다른 서드-파티 구현으로 기본 의존성 주입 기능을 대신할 수도 있습니다.

FromServices 어트리뷰트를 이용한 액션 주입

때로는 컨트롤러에서 단 하나의 액션에서만 서비스가 필요한 경우도 있습니다. 그런 경우라면 서비스를 액션 메서드 자체에 매개변수로 주입하는 것이 보다 합리적일 것입니다. 이 작업은 다음과 같이 매개변수에 [FromServices] 어트리뷰트를 적용하여 처리할 수 있습니다:

public IActionResult About([FromServices] IDateTime dateTime)
{
    ViewData["Message"] = "Currently on the server the time is " + dateTime.Now;

    return View();
}

컨트롤러에서 설정 접근하기

컨트롤러에서 응용 프로그램이나 구성 설정에 접근하는 것은 매우 보편적인 패턴입니다. 이때 Configuration 문서에 소개된 Options 패턴을 이용해서 설정에 접근하게 됩니다. 일반적으로는 의존성 주입을 이용해서 컨트롤러에서 직접 설정을 요청하지 않습니다. 더 나은 접근방식은 IOptions<T> 인스턴스를 요청하는 것으로, 여기서 T는 필요한 구성 클래스를 뜻합니다.

Options 패턴을 이용해서 작업하기 위해서는, 먼저 다음과 같이 해당 옵션들을 나타내는 클래스를 생성해야 합니다:

namespace ControllerDI.Model
{
    public class SampleWebSettings
    {
        public string Title { get; set; }
        public int Updates { get; set; }
    }
}

그런 다음, ConfigureServices 메서드에서 옵션 모델을 사용하고, 서비스 컬렉션에 구성 클래스를 추가하도록 응용 프로그램을 구성합니다:

public Startup(IHostingEnvironment env)
{
    var builder = new ConfigurationBuilder()
        .SetBasePath(env.ContentRootPath)
        .AddJsonFile("samplewebsettings.json");
    Configuration = builder.Build();
}

public IConfigurationRoot Configuration { get; set; }

// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
    // Required to use the Options<T> pattern
    services.AddOptions();

    // Add settings from configuration
    services.Configure<SampleWebSettings>(Configuration);

    // Uncomment to add settings from code
    //services.Configure<SampleWebSettings>(settings =>
    //{
    //    settings.Updates = 17;
    //});

    services.AddMvc();

    // Add application services.
    services.AddTransient<IDateTime, SystemDateTime>();
}

노트

이 예제 코드에서는 응용 프로그램이 JSON으로 서식화된 파일에서 설정을 읽어오도록 구성하고 있습니다. 반면 주석으로 처리되어 있는 부분처럼 설정 전체를 코드로 구성할 수도 있습니다. 더 자세한 구성 옵션들에 대해서는 Configuration 문서를 참고하시기 바랍니다.

강력한 형식의 구성 개체를 지정하고 (여기에서는 SampleWebSettings) 이를 서비스 컬렉션에 추가하고 나면, IOptions<T>의 인스턴스를 요청해서 컨트롤러나 액션 메서드에서 이를 요청할 수 있습니다 (여기에서는 IOptions<SampleWebSettings>). 다음 코드는 컨트롤러에서 설정을 요청하는 방법을 보여줍니다:

public class SettingsController : Controller
{
    private readonly SampleWebSettings _settings;

    public SettingsController(IOptions<SampleWebSettings> settingsOptions)
    {
        _settings = settingsOptions.Value;
    }

    public IActionResult Index()
    {
        ViewData["Title"] = _settings.Title;
        ViewData["Updates"] = _settings.Updates;
        return View();
    }
}

Options 패턴을 따르면 설정 정보를 어디에서 어떻게 찾을지 알 필요가 없기 때문에, 설정과 구성을 서로 분리함과 동시에 컨트롤러에서 관심사의 분리를 보장할 수 있습니다. 또한 컨트롤러 클래스 내부에 Static Cling이나 설정 클래스에 대한 직접적인 인스턴스 생성이 사라지므로 컨트롤러의 단위 테스트(컨트롤러 로직 테스트하기 문서 참고)도 손쉬워집니다.