[dependency-injection] 의존성 주입을 사용하는 이유는 무엇입니까?

의존성 주입 (DI) 을 이해하려고하는데 다시 실패했습니다. 어리석은 것 같습니다. 내 코드는 결코 엉망이 아닙니다. 나는 가상 함수와 인터페이스를 거의 작성하지 않으며 (푸른 달에 한 번만 사용하더라도) 모든 구성은 json.net (때로는 XML 직렬 변환기 사용)을 사용하여 마술처럼 클래스로 직렬화됩니다.

어떤 문제가 해결되는지 잘 모르겠습니다. “hi.이 함수를 실행하면이 유형의 객체를 반환하고 이러한 매개 변수 / 데이터를 사용합니다.”
하지만 … 왜 내가 그걸 사용 하겠어? 참고로 사용할 필요는 없지만 object그 목적이 무엇인지 이해합니다.

DI를 사용하는 웹 사이트 또는 데스크톱 응용 프로그램을 구축 할 때 실제 상황은 무엇입니까? 누군가가 게임에서 인터페이스 / 가상 기능을 사용하려는 이유에 대한 사례를 쉽게 생각 해낼 수 있지만, 게임 이외의 코드에서 사용하는 것은 매우 드 (니다 (단일 인스턴스를 기억할 수 없을 정도로 드 ra니다).



답변

먼저이 답변에 대한 가정을 설명하고 싶습니다. 항상 사실은 아니지만 매우 자주 :

인터페이스는 형용사입니다. 수업은 명사입니다.

(실제로 명사 인 인터페이스가 있지만 여기서는 일반화하고 싶습니다.)

따라서, 인터페이스를 예하는 등 뭔가 할 수있다 IDisposable, IEnumerable또는 IPrintable. 클래스는 이러한 인터페이스 중 하나 이상을 실제로 구현 한 것입니다. List또는 Map둘 다의 구현 일 수 있습니다 IEnumerable.

요점을 파악하려면 : 종종 수업이 서로에게 의존합니다. 예를 들어 Database데이터베이스에 액세스 하는 클래스가 있을 수 있지만 (ha, surprise! ;-))이 클래스가 데이터베이스 액세스에 대한 로깅을 수행하기를 원할 수도 있습니다. 다른 클래스가 있다고 가정 Logger다음 Database에 대한 종속성이 있습니다 Logger.

여태까지는 그런대로 잘됐다.

Database다음 행을 사용 하여 클래스 내에서이 종속성을 모델링 할 수 있습니다 .

var logger = new Logger();

그리고 모든 것이 괜찮습니다. 많은 로거가 필요하다는 것을 깨달을 때까지는 괜찮습니다. 때로는 콘솔, 때로는 파일 시스템, 때로는 TCP / IP 및 원격 로깅 서버 등을 사용하여 로그하려는 경우가 있습니다 …

그리고 물론 모든 코드를 변경하고 싶지 는 않지만 모든 코드를 변경하고 싶지 는 않습니다.

var logger = new Logger();

으로:

var logger = new TcpLogger();

첫째, 이것은 재미 없다. 둘째, 오류가 발생하기 쉽습니다. 셋째, 이것은 훈련 된 원숭이를위한 어리 석고 반복적 인 작업입니다. 그래서 당신은 무엇을합니까?

분명히 ICanLog모든 다양한 로거가 구현 하는 인터페이스 (또는 유사한) 를 도입하는 것이 좋습니다 . 따라서 코드의 1 단계는 다음과 같습니다.

ICanLog logger = new Logger();

이제 타입 추론이 더 이상 타입을 변경하지 않고 개발할 인터페이스가 항상 하나 있습니다. 다음 단계는 계속해서 new Logger()반복하고 싶지 않다는 것 입니다. 따라서 단일 인스턴스 팩토리 클래스에 새 인스턴스를 생성 할 수있는 안정성을 제공하고 다음과 같은 코드를 얻습니다.

ICanLog logger = LoggerFactory.Create();

팩토리 자체는 어떤 종류의 로거를 작성할 것인지 결정합니다. 코드는 더 이상 신경 쓰지 않으며 사용중인 로거 유형을 변경하려면 한 번만 변경하십시오 . 공장 내부.

물론이 팩토리를 일반화하고 모든 유형에 적용 할 수 있습니다.

ICanLog logger = TypeFactory.Create<ICanLog>();

이 TypeFactory에는 특정 인터페이스 유형이 요청 될 때 인스턴스화 할 실제 클래스에 대한 구성 데이터가 필요하므로 맵핑이 필요합니다. 물론 코드 내 에서이 매핑을 수행 할 수 있지만 유형 변경은 재 컴파일을 의미합니다. 그러나이 매핑을 XML 파일 안에 넣을 수도 있습니다. 이렇게하면 컴파일 시간 (!) 후에도 실제로 사용되는 클래스를 변경할 수 있습니다. 다시 컴파일하지 않고도 동적으로 의미합니다!

유용한 예를 들어 보자 : 정상적으로 기록되지는 않지만 고객이 문제가있어 도움을 요청하는 경우, 업데이트 된 XML 구성 파일 만 있으면됩니다. 로깅이 사용 가능하며 고객 지원이 로그 파일을 사용하여 고객을 도울 수 있습니다.

이제 이름을 약간 바꾸면 Service Locator 의 간단한 구현으로 끝납니다 . 이것은 Inversion of Control 의 두 가지 패턴 중 하나입니다 (인스턴스를 생성 할 정확한 클래스를 결정하는 사람에 대한 제어를 반전 시키기 때문에).

이 모든 것이 코드의 종속성을 줄여 주지만 이제 모든 코드는 중앙의 단일 서비스 로케이터에 대한 종속성을 갖습니다.

의존성 주입 은 이제이 라인의 다음 단계입니다. 서비스 로케이터에 대한이 단일 의존성을 제거하십시오. 특정 인터페이스에 대한 구현을 서비스 로케이터에게 요청하는 다양한 클래스 대신, 다시 한 번, 누가 무엇을 인스턴스화하는지에 대한 제어를 되돌립니다. .

의존성 주입을 사용하면 Database클래스에 다음 유형의 매개 변수가 필요한 생성자가 있습니다 ICanLog.

public Database(ICanLog logger) { ... }

이제 데이터베이스에는 항상 사용할 로거가 있지만이 로거의 출처는 더 이상 알 수 없습니다.

그리고 이것이 DI 프레임 워크가 작동하는 곳입니다. 다시 한 번 매핑을 구성한 다음 DI 프레임 워크에 응용 프로그램을 인스턴스화하도록 요청하십시오. 애즈 Application클래스가 필요 ICanPersistData구현의 인스턴스가 Database주입 -하지만 것은 제 구성된 로거 종류의 인스턴스를 생성한다 ICanLog. 등등 …

간단히 말해, 의존성 주입은 코드에서 의존성을 제거하는 두 가지 방법 중 하나입니다. 컴파일 타임 후 구성 변경에 매우 유용하며, 스텁 및 / 또는 모의 객체를 매우 쉽게 주입 할 수 있으므로 단위 테스트에 유용합니다.

실제로 서비스 로케이터 없이는 할 수없는 일이 있습니다 (예 : 특정 인터페이스에 필요한 인스턴스 수를 미리 알지 못하는 경우 : DI 프레임 워크는 항상 매개 변수 당 하나의 인스턴스 만 삽입하지만 호출 할 수는 있음) 물론 루프 내부의 서비스 로케이터), 따라서 대부분의 각 DI 프레임 워크는 또한 서비스 로케이터를 제공합니다.

그러나 기본적으로 그게 다입니다.

추신 : 여기에 설명 한 것은 생성자 주입 이라는 기술입니다. 생성자 매개 변수가 아닌 속성 주입 도 있지만 속성은 종속성을 정의하고 해결하는 데 사용됩니다. 속성 삽입은 선택적 종속성으로, 생성자 삽입은 필수 종속성으로 생각하십시오. 그러나 이에 대한 논의는이 질문의 범위를 벗어납니다.


답변

나는 사람들이 의존성 주입 과 의존성 주입 프레임 워크 (또는 종종 컨테이너 라고도 함) 의 차이점에 대해 많은 혼란을 겪고 있다고 생각합니다 .

의존성 주입은 매우 간단한 개념입니다. 이 코드 대신 :

public class A {
  private B b;

  public A() {
    this.b = new B(); // A *depends on* B
  }

  public void DoSomeStuff() {
    // Do something with B here
  }
}

public static void Main(string[] args) {
  A a = new A();
  a.DoSomeStuff();
}

다음과 같은 코드를 작성하십시오.

public class A {
  private B b;

  public A(B b) { // A now takes its dependencies as arguments
    this.b = b; // look ma, no "new"!
  }

  public void DoSomeStuff() {
    // Do something with B here
  }
}

public static void Main(string[] args) {
  B b = new B(); // B is constructed here instead
  A a = new A(b);
  a.DoSomeStuff();
}

그리고 그게 다야. 진심으로. 이것은 당신에게 많은 장점을 제공합니다. 두 가지 중요한 것은 Main()프로그램 전체에 분산시키는 대신 중앙 위치 ( 함수) 에서 기능을 제어하는 ​​기능 과 모의 객체 또는 다른 위조 된 객체를 생성자에 대신 전달할 수 있기 때문에 각 클래스를보다 쉽게 ​​테스트 할 수있는 기능입니다. 실제 가치).

물론 단점은 이제 프로그램에서 사용하는 모든 클래스에 대해 알고있는 메가 기능이 하나 있다는 것입니다. 그것이 DI 프레임 워크가 도울 수있는 것입니다. 그러나이 접근법이 왜 가치가 있는지 이해하는 데 어려움이 있다면 먼저 수동 종속성 주입으로 시작하는 것이 좋습니다. 따라서 다양한 프레임 워크가 당신을 위해 무엇을 할 수 있는지 더 잘 이해할 수 있습니다.


답변

다른 답변에서 언급했듯이 종속성 주입은 종속성 주입을 사용하는 클래스 외부에서 종속성을 만드는 방법입니다. 당신은 외부에서 그것들을 주입하고, 수업의 내부에서 그들의 창조물에 대한 통제권을 가지십시오. 이것이 의존성 주입이 IoC ( Inversion of Control ) 원칙을 실현 한 이유이기도 합니다.

DI가 패턴 인 IoC가 원칙입니다. 내 경험이 진행되는 한 “하나 이상의 로거가 필요”할 수있는 이유는 실제로 충족되지 않지만 실제로는 무언가를 테스트 할 때마다 실제로 필요하기 때문입니다. 예를 들면 :

내 기능 :

쿠폰을 볼 때 자동으로 확인했음을 표시하여 잊지 않도록합니다.

이것을 다음과 같이 테스트 할 수 있습니다.

[Test]
public void ShouldUpdateTimeStamp
{
    // Arrange
    var formdata = { . . . }

    // System under Test
    var weasel = new OfferWeasel();

    // Act
    var offer = weasel.Create(formdata)

    // Assert
    offer.LastUpdated.Should().Be(new DateTime(2013,01,13,13,01,0,0));
}

따라서 어딘가에 OfferWeasel다음과 같은 오퍼 객체를 빌드합니다.

public class OfferWeasel
{
    public Offer Create(Formdata formdata)
    {
        var offer = new Offer();
        offer.LastUpdated = DateTime.Now;
        return offer;
    }
}

여기서 문제 DateTime.Now는 테스트 코드를 입력하더라도 몇 밀리 초 정도 꺼질 수 있기 때문에 설정 한 날짜가 주장 된 날짜와 다르기 때문에이 테스트는 항상 실패한다는 것입니다. 항상 실패합니다. 더 좋은 해결책은 이제이를 위해 인터페이스를 만드는 것입니다.이 인터페이스를 사용하면 몇시에 설정할지 제어 할 수 있습니다.

public interface IGotTheTime
{
    DateTime Now {get;}
}

public class CannedTime : IGotTheTime
{
    public DateTime Now {get; set;}
}

public class ActualTime : IGotTheTime
{
    public DateTime Now {get { return DateTime.Now; }}
}

public class OfferWeasel
{
    private readonly IGotTheTime _time;

    public OfferWeasel(IGotTheTime time)
    {
        _time = time;
    }

    public Offer Create(Formdata formdata)
    {
        var offer = new Offer();
        offer.LastUpdated = _time.Now;
        return offer;
    }
}

인터페이스는 추상화입니다. 하나는 진짜이고 다른 하나는 필요한 곳에 시간을 허비 할 수 있습니다. 그런 다음 테스트를 다음과 같이 변경할 수 있습니다.

[Test]
public void ShouldUpdateTimeStamp
{
    // Arrange
    var date = new DateTime(2013, 01, 13, 13, 01, 0, 0);
    var formdata = { . . . }

    var time = new CannedTime { Now = date };

    // System under test
    var weasel= new OfferWeasel(time);

    // Act
    var offer = weasel.Create(formdata)

    // Assert
    offer.LastUpdated.Should().Be(date);
}

이와 같이 종속성을 주입하여 (현재 시간을 가져옴) “제어 반전”원칙을 적용했습니다. 이 작업을 수행하는 주된 이유는 분리 된 단위 테스트를보다 쉽게하기 위해 다른 방법이 있기 때문입니다. 예를 들어 C # 함수에서 변수로 전달 될 수 있으므로 인터페이스와 클래스는 필요하지 않으므로 인터페이스 대신 Func<DateTime> 를 하여 동일한 결과를 얻을 . 또는 동적 접근 방식을 취하면 동등한 방법을 사용하는 객체 ( duck typing )를 전달하면 인터페이스가 전혀 필요하지 않습니다.

둘 이상의 로거가 거의 필요하지 않습니다. 그럼에도 불구하고 Java 또는 C #과 같은 정적으로 유형이 지정된 코드에는 종속성 주입이 필수적입니다.

과…
모든 종속 항목을 사용할 수있는 경우 런타임에 객체의 목적을 올바르게 수행 할 수 있으므로 속성 삽입을 설정하는 데 많이 사용되지 않습니다. 제 생각에는 생성자가 호출 될 때 모든 종속성이 충족되어야하므로 생성자 주입이 필요합니다.

도움이 되었기를 바랍니다.


답변

고전적인 대답은 더 분리 된 응용 프로그램을 만드는 것입니다.이 응용 프로그램은 런타임 중에 어떤 구현이 사용 될지 알지 못합니다.

예를 들어, Google은 전 세계의 많은 결제 제공 업체와 협력하는 중앙 결제 제공 업체입니다. 그러나 요청이있을 때 어떤 결제 프로세서를 호출할지 모릅니다. 다음과 같은 수많은 스위치 케이스로 하나의 클래스를 프로그래밍 할 수 있습니다.

class PaymentProcessor{

    private String type;

    public PaymentProcessor(String type){
        this.type = type;
    }

    public void authorize(){
        if (type.equals(Consts.PAYPAL)){
            // Do this;
        }
        else if(type.equals(Consts.OTHER_PROCESSOR)){
            // Do that;
        }
    }
}

이제이 코드가 제대로 분리되지 않았기 때문에이 코드를 모두 단일 클래스로 유지해야한다고 상상해보십시오. 지원할 모든 새 프로세서에 대해 새로운 if // switch case를 만들어야한다고 상상할 수 있습니다 그러나 모든 방법에서 Dependency Injection (또는 Inversion of Control-때로는 호출되기 때문에 프로그램 실행을 제어하는 ​​사람은 런타임에만 알고 합병증은 아님)을 사용하면 더 복잡해집니다. 매우 깔끔하고 유지 보수가 쉽습니다.

class PaypalProcessor implements PaymentProcessor{

    public void authorize(){
        // Do PayPal authorization
    }
}

class OtherProcessor implements PaymentProcessor{

    public void authorize(){
        // Do other processor authorization
    }
}

class PaymentFactory{

    public static PaymentProcessor create(String type){

        switch(type){
            case Consts.PAYPAL;
                return new PaypalProcessor();

            case Consts.OTHER_PROCESSOR;
                return new OtherProcessor();
        }
    }
}

interface PaymentProcessor{
    void authorize();
}

** 코드가 컴파일되지 않습니다.


답변

DI를 사용하는 주된 이유는 지식이있는 구현 지식에 대한 책임을지고 싶어하기 때문입니다. DI의 아이디어는 인터페이스에 의한 캡슐화 및 디자인과 매우 일치합니다. 프런트 엔드가 백 엔드에서 일부 데이터를 요청하면 백 엔드가 해당 질문을 해결하는 방법은 프런트 엔드에 중요하지 않습니다. 그것은 requesthandler에 달려 있습니다.

그것은 OOP에서 이미 오랫동안 일반적입니다. 여러 번 다음과 같은 코드 조각을 만듭니다.

I_Dosomething x = new Impl_Dosomething();

단점은 구현 클래스가 여전히 하드 코딩되어 있으므로 프런트 엔드에 구현이 사용되는 지식이 있다는 것입니다. DI는 인터페이스의 디자인을 한 단계 더 발전시켜 프론트 엔드가 알아야 할 것은 인터페이스에 대한 지식뿐입니다. DYI와 DI 사이에는 서비스 로케이터의 패턴이 있습니다. 프런트 엔드는 요청을 해결할 수 있도록 키 (서비스 로케이터의 레지스트리에 있음)를 제공해야하기 때문입니다. 서비스 로케이터 예 :

I_Dosomething x = ServiceLocator.returnDoing(String pKey);

DI 예 :

I_Dosomething x = DIContainer.returnThat();

DI의 요구 사항 중 하나는 컨테이너가 어떤 클래스가 어떤 인터페이스의 구현인지 알아낼 수 있어야한다는 것입니다. 따라서 DI 컨테이너에는 강력한 형식의 디자인과 각 인터페이스에 대해 한 번의 구현 만 필요합니다. 계산기와 같은 인터페이스를 동시에 더 구현해야하는 경우 서비스 로케이터 또는 팩토리 디자인 패턴이 필요합니다.

D (b) I : 인터페이스에 의한 의존성 주입과 디자인. 그러나 이러한 제한은 실용적이지 못합니다. D (b) I를 사용하면 클라이언트와 공급자 간의 통신이 가능하다는 이점이 있습니다. 인터페이스는 객체 또는 일련의 동작에 대한 관점입니다. 후자는 여기서 중요합니다.

코딩에서 D (b) I와 함께 서비스 계약 관리를 선호합니다. 그들은 함께 가야합니다. 서비스 계약을 조직적으로 관리하지 않고 기술 솔루션으로 D (b) I를 사용하는 것은 DI가 캡슐화의 추가 계층이기 때문에 필자의 관점에서 그다지 유익하지 않습니다. 그러나 조직 관리와 함께 사용할 수 있으면 D (b) I가 제공하는 조직 원칙을 실제로 활용할 수 있습니다. 장기적으로 테스트, 버전 관리 및 대안 개발과 같은 주제로 고객 및 기타 기술 부서와의 커뮤니케이션을 구조화하는 데 도움이 될 수 있습니다. 하드 코딩 된 클래스와 같이 암시 적 인터페이스가 있으면 시간이 지남에 따라 D (b) I를 사용하여 명시 적으로 만들면 통신이 훨씬 덜합니다. 그것은 모두 시간이 지남에 따라 유지 보수로 귀결됩니다. 🙂


답변