수색…


비고

의존성 주입으로 해결 된 문제

의존성 주입을 사용하지 않으면 Greeter 클래스가 다음과 같이 보일 수 있습니다.

public class ControlFreakGreeter
{
    public void Greet()
    {
        var greetingProvider = new SqlGreetingProvider(
            ConfigurationManager.ConnectionStrings["myConnectionString"].ConnectionString);
        var greeting = greetingProvider.GetGreeting();
        Console.WriteLine(greeting);
    }
}

이것은 인사 장을 제공하는 클래스를 만들고, SQL 연결 문자열의 출처를 제어하며, 출력을 제어하는 ​​"컨트롤 괴물"입니다.

Greeter 클래스는 의존성 삽입을 사용하여 단일 책임에 찬성하여 책임을 포기하고 그에 대한 인사말을 작성합니다.

Dependency Inversion Principle 은 클래스가 다른 구체적인 클래스보다는 추상화 (인터페이스와 같은)에 의존해야한다고 제안합니다. 클래스 간의 직접적인 종속성 (연결)은 유지 보수를 점진적으로 어렵게 만들 수 있습니다. 추상화에 따라 그 결합을 줄일 수 있습니다.

의존성 삽입은 추상화에 의존하는 클래스를 작성하기 때문에 의존성 반전을 달성하는 데 도움이됩니다. Greeter 클래스는 IGreetingProviderIGreetingWriter 의 구현 세부 사항에 대해 "알지 IGreetingWriter ." 주입 된 종속성이 이러한 인터페이스를 구현한다는 것을 알고 있습니다. 즉, IGreetingProviderIGreetingWriter 를 구현하는 구체적인 클래스를 IGreetingWriterGreeter 영향을 미치지 않습니다. 어느 것도 완전히 다른 구현으로 대체하지 않을 것입니다. 인터페이스 변경 만 가능합니다. Greeter 는 분리됩니다.

ControlFreakGreeter 는 단위 테스트를 제대로 수행 할 수 없습니다. 작은 코드 단위를 테스트하려고하지만 대신 SQL에 연결하고 저장 프로 시저를 실행하는 테스트가 포함됩니다. 콘솔 출력 테스트도 포함됩니다. ControlFreakGreeter는 너무 많은 작업을 수행하므로 다른 클래스와는 별도로 테스트 할 수 없습니다.

Greeter 는 저장 프로 시저를 호출하거나 콘솔 출력을 읽는 것보다 실행 및 검증이 쉬운 조롱 된 구현을 삽입 할 수 있기 때문에 단위 테스트가 쉽습니다. app.config에 연결 문자열이 필요하지 않습니다.

IGreetingProviderIGreetingWriter 의 구체적인 구현은 더욱 복잡해 IGreetingWriter 수 있습니다. 그들은 그들 자신의 의존성을 가지고있을 수 있습니다. 예를 들어 SQL 연결 문자열을 SqlGreetingProvider 삽입합니다. 그러나 이러한 복잡성은 인터페이스에만 의존하는 다른 클래스에서 "숨겨집니다". 따라서 다른 클래스를 변경해야하는 "파급 효과"없이 하나의 클래스를 수정하는 것이 더 쉬워집니다.

의존성 주입 - 간단한 예

이 클래스를 Greeter 라고합니다. 그 책임은 인사말을 출력하는 것입니다. 두 가지 종속성이 있습니다. 출력하기 위해 인사말을 줄 수있는 무언가가 필요합니다. 그러면 인사말을 출력하는 방법이 필요합니다. 이러한 종속성은 모두 인터페이스, IGreetingProviderIGreetingWriter 로 설명됩니다. 이 예에서 두 가지 종속성은 Greeter "주입"됩니다. (예제 다음에 나오는 추가 설명)

public class Greeter
{
    private readonly IGreetingProvider _greetingProvider;
    private readonly IGreetingWriter _greetingWriter;

    public Greeter(IGreetingProvider greetingProvider, IGreetingWriter greetingWriter)
    {
        _greetingProvider = greetingProvider;
        _greetingWriter = greetingWriter;
    }

    public void Greet()
    {
        var greeting = _greetingProvider.GetGreeting();
        _greetingWriter.WriteGreeting(greeting);
    }
}

public interface IGreetingProvider
{
    string GetGreeting();
}

public interface IGreetingWriter
{
    void WriteGreeting(string greeting);
}

Greeting 클래스는 IGreetingProviderIGreetingWriter 에 의존하지만, 둘 중 하나의 인스턴스를 만들지는 않습니다. 대신 생성자에서이를 요구합니다. Greeting 의 인스턴스를 만드는 것은 무엇이든 그 두 가지 종속성을 제공해야합니다. 의존성을 "주입"한다고 부를 수 있습니다.

종속성은 생성자의 클래스에 제공되므로 "생성자 삽입"이라고도합니다.

몇 가지 일반적인 규칙 :

  • 생성자는 종속성을 private 필드로 저장합니다. 클래스가 인스턴스화 되 자마자 클래스의 다른 모든 비 정적 메소드에서 이러한 종속성을 사용할 수 있습니다.
  • private 필드는 readonly 입니다. 생성자에서 설정되면 변경할 수 없습니다. 이는 해당 필드가 생성자 외부에서 수정 될 수 없어야 함을 나타냅니다. 따라서 클래스의 수명 동안 해당 종속성을 사용할 수 있습니다.
  • 종속성은 인터페이스입니다. 이것은 꼭 필요한 것은 아니지만 종속성의 한 구현을 다른 구현으로 쉽게 대체 할 수 있기 때문에 일반적입니다. 또한 단위 테스트 목적으로 조롱 된 인터페이스 버전을 제공 할 수도 있습니다.

의존성 주입으로 단위 테스트를보다 쉽게 ​​만드는 방법

이는 IGreetingProviderIGreetingWriter 라는 두 가지 종속성이있는 Greeter 클래스의 이전 예제를 기반으로 작성되었습니다.

IGreetingProvider 의 실제 구현은 API 호출 또는 데이터베이스에서 문자열을 검색 할 수 있습니다. IGreetingWriter 구현하면 콘솔에 인사말이 표시 될 수 있습니다. 그러나 Greeter 가 생성자에 종속성을 주입 했으므로 이러한 인터페이스의 조롱 된 버전을 주입하는 단위 테스트를 작성하는 것은 쉽습니다. 실생활에서는 Moq 와 같은 프레임 워크를 사용할 수 있지만,이 경우 조롱 한 구현을 작성합니다.

public class TestGreetingProvider : IGreetingProvider
{
    public const string TestGreeting = "Hello!";

    public string GetGreeting()
    {
        return TestGreeting;
    }
}

public class TestGreetingWriter : List<string>, IGreetingWriter
{
    public void WriteGreeting(string greeting)
    {
        Add(greeting);
    }
}

[TestClass]
public class GreeterTests
{
    [TestMethod]
    public void Greeter_WritesGreeting()
    {
        var greetingProvider = new TestGreetingProvider();
        var greetingWriter = new TestGreetingWriter();
        var greeter = new Greeter(greetingProvider, greetingWriter);
        greeter.Greet();
        Assert.AreEqual(greetingWriter[0], TestGreetingProvider.TestGreeting);
    }
}

IGreetingProviderIGreetingWriter 의 동작은이 테스트와 관련이 없습니다. Google은 Greeter 가 인사말을 받고이를 작성 Greeter 테스트하려고합니다. Greeter 의 설계 (의존성 삽입 사용)는 복잡한 움직이는 부분없이 조롱 된 의존성을 주입 할 수있게합니다. 우리가 테스트하는 것은 Greeter 가 예상 한대로 종속 관계와 상호 작용한다는 것입니다.

의존성 주입 컨테이너 (IoC 컨테이너)를 사용하는 이유

의존성 주입은 의존성을 제어하지 않도록 클래스를 작성하는 대신 종속성을 제공합니다 ( "주입").

Castle Windsor, Autofac, SimpleInjector, Ninject, Unity 등의 의존성 주입 프레임 워크 (종종 "DI 컨테이너", "IoC 컨테이너"또는 "컨테이너"라고 함)를 사용하는 것과는 다릅니다.

컨테이너는 의존성 주입을 더 쉽게 만듭니다. 예를 들어, 의존성 주입에 의존하는 여러 클래스를 작성한다고 가정합니다. 하나의 클래스는 여러 인터페이스에 의존하며, 해당 인터페이스를 구현하는 클래스는 다른 인터페이스에 의존합니다. 일부는 특정 값에 의존합니다. 그리고 재미로, 그 클래스의 일부는 IDisposable 구현하고 처리해야합니다.

각각의 개별 수업은 잘 작성되어 있으며 쉽게 테스트 할 수 있습니다. 그러나 이제는 다른 문제가 있습니다 : 클래스의 인스턴스를 만드는 것은 훨씬 더 복잡해졌습니다. CustomerService 클래스의 인스턴스를 생성한다고 가정합니다. 종속성을 가지며 종속성에는 종속성이 있습니다. 인스턴스를 생성하는 것은 다음과 같이 보일 수 있습니다 :

public CustomerData GetCustomerData(string customerNumber)
{
    var customerApiEndpoint = ConfigurationManager.AppSettings["customerApi:customerApiEndpoint"];
    var logFilePath = ConfigurationManager.AppSettings["logwriter:logFilePath"];
    var authConnectionString = ConfigurationManager.ConnectionStrings["authorization"].ConnectionString;
    using(var logWriter = new LogWriter(logFilePath ))
    {
        using(var customerApiClient = new CustomerApiClient(customerApiEndpoint))
        {
            var customerService = new CustomerService(
                new SqlAuthorizationRepository(authorizationConnectionString, logWriter),
                new CustomerDataRepository(customerApiClient, logWriter),
                logWriter
            );   
            
            // All this just to create an instance of CustomerService!         
            return customerService.GetCustomerData(string customerNumber);
        }
    }
}

전체 거대한 구성을 CustomerService 반환하는 별도의 함수에 넣는 것이 어떻습니까? 한 가지 이유는 각 클래스에 대한 종속성이이 클래스에 주입되기 때문에 클래스는 해당 종속성이 IDisposable 인지 아니면이를 처리하는지에 대한 책임이 없다는 것입니다. 그냥 사용합니다. 따라서 완전하게 구축 된 CustomerService 를 반환하는 GetCustomerService() 함수가있는 경우이 클래스에는 많은 일회용 리소스가 포함될 수 있으며 액세스하거나 처리 할 방법이 없을 수 있습니다.

그리고 IDisposable 을 폐기하는 것 외에도 누가 그런 일련의 중첩 생성자를 호출하려고합니까? 그것은 간단한 예입니다. 그것은 훨씬 나빠질 수 있습니다. 다시 말하지만, 우리가 수업을 잘못 작성한 것은 아닙니다. 수업은 개별적으로 완벽 할 수도 있습니다. 도전은 그것들을 함께 구성하는 것입니다.

의존성 주입 컨테이너는이를 단순화합니다. 이를 통해 각 종속성을 충족시키는 데 사용할 클래스 또는 값을 지정할 수 있습니다. 이 약간 간소화 된 예제는 Castle Windsor를 사용합니다.

var container = new WindsorContainer()
container.Register(
    Component.For<CustomerService>(),
    Component.For<ILogWriter, LogWriter>()
        .DependsOn(Dependency.OnAppSettingsValue("logFilePath", "logWriter:logFilePath")),
    Component.For<IAuthorizationRepository, SqlAuthorizationRepository>()
        .DependsOn(Dependency.OnValue(connectionString, ConfigurationManager.ConnectionStrings["authorization"].ConnectionString)),
    Component.For<ICustomerDataProvider, CustomerApiClient>()
         .DependsOn(Dependency.OnAppSettingsValue("apiEndpoint", "customerApi:customerApiEndpoint"))   
);

이를 "의존성 등록"또는 "컨테이너 구성"이라고합니다. 번역 된 내용은 WindsorContainer 에게 다음과 같이 알려줍니다.

  • 클래스에 ILogWriter 가 필요한 경우 LogWriter 의 인스턴스를 만듭니다. LogWriter 에는 파일 경로가 필요합니다. AppSettings 에서이 값을 사용하십시오.
  • 클래스에 IAuthorizationRepository 가 필요한 경우 SqlAuthorizationRepository 의 인스턴스를 SqlAuthorizationRepository . 연결 문자열이 필요합니다. ConnectionStrings 섹션에서이 값을 사용하십시오.
  • 클래스에 ICustomerDataProvider 가 필요한 경우 CustomerApiClient 만들고 AppSettings 에서 필요한 문자열을 제공하십시오.

컨테이너에서 의존성을 요청할 때 의존성을 "해결"한다고합니다. 컨테이너를 사용하여 직접 수행하는 것은 좋지 않지만 실제로는 다릅니다. 데모 목적으로 이제 다음 작업을 수행 할 수 있습니다.

var customerService = container.Resolve<CustomerService>();
var data = customerService.GetCustomerData(customerNumber);
container.Release(customerService);

컨테이너는 CustomerServiceIAuthorizationRepositoryICustomerDataProviderIAuthorizationRepository 되어 있음을 알고 있습니다. 이러한 요구 사항을 충족시키기 위해 어떤 클래스를 만들어야하는지 알고 있습니다. 이러한 클래스는 차례로 더 많은 의존성을 가지며 컨테이너는이를 충족시키는 방법을 알고 있습니다. CustomerService 의 인스턴스를 반환 할 수있을 때까지 필요한 모든 클래스를 만듭니다.

클래스에 IDoesSomethingElse 와 같이 등록하지 않은 종속성이 필요한 지점에 도달하면 CustomerService 하려고 할 때 해당 요구 사항을 충족시킬 항목이 등록되지 않았 음을 알리는 명확한 예외가 발생합니다.

각 DI 프레임 워크는 조금 다르게 동작하지만 일반적으로 특정 클래스가 인스턴스화되는 방식을 제어합니다. 예를 들어, LogWriter 인스턴스를 하나 만들고 ILogWriter 의존하는 모든 클래스에 제공하거나, 매번 새로운 클래스를 생성하기를 원합니까? 대부분의 컨테이너에는이를 지정하는 방법이 있습니다.

IDisposable 을 구현하는 클래스는 무엇입니까? 이것이 우리가 container.Release(customerService); 라고 부르는 이유입니다 container.Release(customerService); 결국. 대부분의 컨테이너 (Windsor 포함)는 생성 된 모든 종속성을 철회하고 Dispose 해야하는 컨테이너를 Dispose 합니다. CustomerServiceIDisposable 경우에도 처리됩니다.

위와 같이 종속성을 등록하면 더 많은 코드를 작성할 수 있습니다. 그러나 우리가 많은 의존성을 가진 많은 클래스를 가지고 있으면 정말 효과적입니다. 그리고 만약 우리가 의존성 주입을 사용 하지 않고 동일한 클래스를 작성해야한다면 많은 클래스를 가진 동일한 어플리케이션이 유지 및 테스트하기가 어려워 질 것입니다.

이것은 의존성 주입 컨테이너를 사용하는 이유를 표면에 흠집 낸다. 애플리케이션을 구성하고 올바르게 사용하는 구성 방법 은 단지 하나의 주제가 아닙니다. 지시 사항과 예제가 컨테이너마다 다르므로 많은 주제가 있습니다.



Modified text is an extract of the original Stack Overflow Documentation
아래 라이선스 CC BY-SA 3.0
와 제휴하지 않음 Stack Overflow