의존성 주입 프레임 워크에 대한 논쟁 중 하나에 의문을 제기하는 이유 : 왜 객체 그래프를 만드는 것이 어려운가?


13

Google Guice와 같은 종속성 주입 프레임 워크는 사용법에 대한 다음 동기 ( source )를 제공합니다.

객체를 구성하려면 먼저 해당 객체의 종속성을 작성하십시오. 그러나 각 종속성을 빌드하려면 해당 종속성 등이 필요합니다. 따라서 객체를 만들 때 실제로 객체 그래프를 만들어야합니다.

손으로 객체 그래프를 작성하는 것은 노동 집약적이며 (...) 테스트를 어렵게 만듭니다.

그러나 나는이 주장을 사지 않았다 : 의존성 주입 프레임 워크가 없어도 인스턴스화하기 쉽고 테스트하기 편리한 클래스를 작성할 수있다. 예를 들어 Guice 동기 부여 페이지 의 예제 는 다음과 같이 다시 작성할 수 있습니다.

class BillingService
{
    private final CreditCardProcessor processor;
    private final TransactionLog transactionLog;

    // constructor for tests, taking all collaborators as parameters
    BillingService(CreditCardProcessor processor, TransactionLog transactionLog)
    {
        this.processor = processor;
        this.transactionLog = transactionLog;
    }

    // constructor for production, calling the (productive) constructors of the collaborators
    public BillingService()
    {
        this(new PaypalCreditCardProcessor(), new DatabaseTransactionLog());
    }

    public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard)
    {
        ...
    }
}

의존성 주입 프레임 워크에 대한 다른 주장이있을 수 있지만 ( 이 질문의 범위를 벗어났습니다 !) 테스트 가능한 객체 그래프를 쉽게 만드는 것은 그중 하나가 아닙니다.


1
나는 종종 무시되는 의존성 주입의 또 다른 장점은 어떤 객체가 의존하는지 알도록 강요 한다는 것이다 . 명시 적으로 표시된 속성을 사용하거나 생성자를 통해 직접 객체에 마술 같은 것이 나타나지 않습니다. 코드를 더 테스트 가능하게 만드는 방법은 말할 것도 없습니다.
Benjamin Gruenbaum

나는 의존성 주입의 다른 장점을 찾고 있지 않다는 것에 주목하라-나는 의존성 주입 없이는 객체 인스턴스화가 어렵다는 주장을 이해하고
싶을 뿐이다.

이 대답 은 왜 객체 그래프에 대해 일종의 컨테이너를 원할 것인지에 대한 좋은 예를 제공하는 것 같습니다 (즉, 3 개의 객체 만 사용하여 충분히 생각하지 않습니다).
이즈 카타

@Izkata 아니오, 이것은 크기 문제가 아닙니다. 설명 된 접근 방식을 사용하면 해당 게시물 의 코드는 다음과 같습니다 new ShippingService().
준수

@oberlies 아직도 있습니다; 그런 다음 한 클래스가 아닌 해당 배송 서비스에 어떤 클래스가 사용되는지 파악하기 위해 9 개의 클래스 정의를 파헤쳐 야합니다.
Izkata

답변:


12

의존성 주입을 수행하는 가장 좋은 방법에 대한 오래되고 오래 지속되는 논쟁이 있습니다.

  • 스프링의 원래 컷은 일반 객체를 인스턴스화 한 다음 setter 메소드를 통해 종속성을 주입했습니다.

  • 그러나 많은 사람들이 생성자 매개 변수를 통해 종속성을 주입하는 것이 올바른 방법이라고 주장했습니다.

  • 그런 다음 최근 리플렉션 사용이 일반화되면서 세터 나 생성자 인수없이 개인 멤버의 값을 직접 설정하는 것이 분노가되었습니다.

따라서 첫 번째 생성자는 종속성 주입에 대한 두 번째 접근 방식과 일치합니다. 테스트를 위해 모의 주입과 같은 멋진 일을 할 수 있습니다.

그러나 인수가없는 생성자는이 문제가 있습니다. and 에 대한 구현 클래스를 인스턴스화하기 때문에 PayPal 및 데이터베이스에 대해 컴파일 타임에 대한 엄격한 종속성을 만듭니다. 전체 종속성 트리를 올바르게 작성하고 구성해야합니다.PaypalCreditCardProcessorDatabaseTransactionLog

  • PayPay 프로세서가 매우 복잡한 서브 시스템이고 추가로 많은 지원 라이브러리가 필요하다고 상상해보십시오. 해당 구현 클래스에서 컴파일 타임 종속성을 작성하면 해당 전체 종속성 트리에 대한 끊길 수없는 링크가 작성됩니다. 객체 그래프의 복잡성은 아마도 2 배 정도 증가했습니다.

  • 종속성 트리의 많은 항목은 투명하지만 많은 항목도 인스턴스화해야합니다. 승산은, 당신은 단지 인스턴스화 할 수 없습니다 PaypalCreditCardProcessor.

  • 인스턴스화 외에도 각 객체에는 구성에서 적용된 속성이 필요합니다.

인터페이스에 대한 종속성 만 있고 외부 팩토리가 종속성을 빌드하고 주입하도록 허용하면 전체 PayPal 종속성 트리를 잘라 내고 코드의 복잡성이 인터페이스에서 중지됩니다.

구성 (예 : 컴파일 타임이 아닌 런타임)에서 구현 클래스를 지정하거나 환경 (테스트, 통합, 프로덕션)에 따라 다양한 동적 종속성 스펙을 갖는 다른 이점이 있습니다.

예를 들어, PayPalProcessor에 3 개의 종속 오브젝트가 있고 각 종속 항목에 2 개가 더 있다고 가정하십시오. 그리고 이러한 모든 개체는 구성에서 속성을 가져와야합니다. 코드는있는 그대로 구성, 속성 등을 설정하는 등 DI 프레임 워크가 처리해야 할 모든 문제를 책임집니다.

처음에 DI 프레임 워크를 사용하여 자신을 보호하는 것은 분명하지 않지만 시간이 지남에 따라 추가되고 고통스럽게 분명해집니다. (웃음 나는 그것을 어려운 방법으로 시도한 경험에서 말합니다)

...

실제로, 아주 작은 프로그램이라도 DI 스타일로 작성하고 클래스를 구현 / 팩토리 쌍으로 나눕니다. 즉, Spring과 같은 DI 프레임 워크를 사용하지 않으면 간단한 팩토리 클래스를 함께 던집니다.

그것은 우리 학급이 그 일을 할 수 있도록 우려의 분리를 제공하며 팩토리 클래스는 물건을 만들고 구성하는 책임을집니다.

필수 접근 방식은 아니지만 FWIW

...

보다 일반적으로 DI / 인터페이스 패턴은 다음 두 가지 작업을 수행하여 코드의 복잡성을 줄입니다.

  • 다운 스트림 종속성을 인터페이스로 추상화

  • 업스트림 종속성을 코드와 컨테이너에 "리프팅"

또한 개체 인스턴스화 및 구성은 매우 친숙한 작업이므로 DI 프레임 워크는 표준화 된 표기법과 반사와 같은 트릭을 사용하여 많은 규모의 경제를 달성 할 수 있습니다. 수업에서 똑같은 관심사를 흩 뜨리면 생각보다 훨씬 더 복잡해집니다.


이 코드는 모든 것을 구축해야 할 책임이 있다고 가정합니다 . 클래스는 협력자 구현이 무엇인지 아는 것만 책임집니다. 이러한 공동 작업자가 필요로하는 것을 알 필요는 없습니다. 그래서이 정도는 아닙니다.
oberlies

1
그러나 PayPalProcessor 생성자에 2 개의 인수가 있으면 어디에서 왔을까요?
Rob

그렇지 않습니다. BillingService와 마찬가지로 PayPalProcessor에는 필요한 공동 작업자를 생성하는 제로 인수 생성자가 있습니다.
7

다운 스트림 종속성을 인터페이스로 추상화 -예제 코드도이를 수행합니다. 프로세서 필드는 CreditCardProcessor 유형이며 구현 유형이 아닙니다.
7

2
@oberlies : 코드 예제는 생성자 인수 스타일과 0-args 구성 가능 스타일의 두 스타일 사이에 있습니다. 오류는 제로 인수 구성 이 항상 가능하지는 않다는 것입니다. DI 또는 Factory의 장점은 개체가 제로 아거 생성자로 보이는 것으로 구성 될 수 있다는 것입니다.
rwong

4
  1. 수영장의 얕은 끝에서 수영을하면 모든 것이 "쉽고 편리합니다". 12 개 정도의 물건을 지나면 더 이상 편리하지 않습니다.
  2. 귀하의 예에서, 귀하는 청구 프로세스를 영원히 그리고 하루에 PayPal에 묶었습니다. 다른 신용 카드 프로세서를 사용하고 싶다고 가정 해보십시오. 네트워크에 제약이있는 특수 신용 카드 프로세서를 생성한다고 가정 해보십시오. 아니면 신용 카드 번호 처리를 테스트해야합니까? 이식 불가능한 코드를 작성했습니다. "한 번 작성하십시오. 설계된 특정 오브젝트 그래프에 따라 달라지기 때문에 한 번만 사용하십시오."

프로세스 초기에 객체 그래프를 바인딩 (즉, 코드에 하드와 이어링)함으로써 계약과 구현이 모두 있어야합니다. 다른 사람 (아마도)이 약간 다른 목적으로 해당 코드를 사용하려면 전체 객체 그래프를 다시 계산하고 다시 구현해야합니다.

DI 프레임 워크를 사용하면 런타임에 여러 구성 요소를 가져와 함께 연결할 수 있습니다. 이것은 시스템을 "모듈 식"으로 만들고, 서로의 구현 대신 서로의 인터페이스에 작동하는 많은 모듈로 구성됩니다.


내가 당신의 주장을 따를 때, DI에 대한 추론은 "손으로 객체 그래프를 작성하는 것은 쉽지만 유지하기 어려운 코드로 이어진다"는 것이 었습니다. 그러나 주장은 "손으로 개체 그래프를 만드는 것이 어렵다"는 것이며 그 주장을 뒷받침하는 주장만을 찾고 있습니다. 그래서 귀하의 게시물이 내 질문에 대답하지 않습니다.
준수

1
당신이 "만드는 질문을 줄이면 것 같아요 ... 객체 그래프를"다음은 간단합니다. 제 요점은 DI가 하나의 객체 그래프 만 다루지 않는 문제를 해결한다는 것 입니다. 당신은 그들의 가족을 다루고 있습니다.
BobDalgleish


4

"내 협력자 인스턴스화"접근 방식은 종속성 트리에 대해 작동 할 수 있지만 DAG ( General Directed Acyclic Graph ) 인 종속성 그래프에는 제대로 작동하지 않습니다 . 종속성 DAG에서 여러 노드가 동일한 노드를 가리킬 수 있습니다. 즉, 두 개체가 공동 작업자와 동일한 개체를 사용합니다. 이 사례는 실제로 질문에 설명 된 접근 방식으로 구성 할 수 없습니다.

공동 작업자 중 일부 (또는 공동 작업자의 공동 작업자)가 특정 개체를 공유해야하는 경우이 개체를 인스턴스화하여 공동 작업자에게 전달해야합니다. 따라서 실제로 직접 공동 작업자보다 더 많은 정보가 필요합니다.


1
그러나 의존성 트리는 그래프 이론상 트리 여야합니다 . 그렇지 않으면 사이클이 생겨서 만족스럽지 않습니다.
Xion

1
@Xion 종속성 그래프는 방향이 지정된 비순환 그래프 여야합니다. 트리 에서처럼 두 노드 사이에 정확히 하나의 경로 만있을 필요는 없습니다.
7

@Xion : 반드시 그런 것은 아닙니다. UnitOfWork단일 DbContext및 다중 저장소 가있는 a 를 고려하십시오 . 이러한 모든 리포지토리는 동일한 DbContext개체를 사용해야 합니다. OP의 제안 된 "자체 인스턴스화"로 인해 불가능 해졌습니다.
Flater

1

Google Guice를 사용하지는 않았지만 .Net의 기존 레거시 N- 계층 애플리케이션을 의존성 주입을 사용하여 분리하기 위해 Onion Layer와 같은 IoC 아키텍처로 마이그레이션하는 데 많은 시간을 들였습니다.

왜 의존성 주입인가?

Dependency Injection의 목적은 실제로 테스트 가능성을위한 것이 아니라 실제로 밀접하게 결합 된 응용 프로그램을 사용하고 가능한 한 결합을 느슨하게하는 것입니다. (적절한 단위 테스트에 맞게 코드를 훨씬 쉽게 적용 할 수있는 제품으로 바람직한 것은 무엇입니까)

왜 결합에 대해 걱정해야합니까?

커플 링이나 밀접한 의존성은 매우 위험한 일이 될 수 있습니다. (특히 컴파일 된 언어)이 경우 전체 응용 프로그램을 효과적으로 오프라인으로 만드는 문제가있는 라이브러리, dll 등이 거의 사용되지 않을 수 있습니다. (중요하지 않은 부분에 문제가 있기 때문에 전체 응용 프로그램이 종료됩니다 ... 이것은 나쁩니다 ... 정말 나쁩니다) 이제 물건을 분리하면 실제로 응용 프로그램을 설정하여 해당 DLL 또는 라이브러리가 완전히 누락 된 경우에도 실행할 수 있습니다! 해당 라이브러리 또는 DLL이 필요한 한 부분은 작동하지 않지만 나머지 응용 프로그램은 가능한 한 행복합니다.

적절한 테스트를 위해 Dependency Injection이 필요한 이유

실제로 당신은 느슨하게 결합 된 코드를 원한다. Dependency Injection은 그것을 가능하게한다. IoC없이 느슨하게 연결할 수 있지만 일반적으로 더 많은 작업과 적응력이 떨어집니다 (누군가 예외가 있다고 확신합니다)

당신이 준 경우 의존성 주입을 설정하는 것이 훨씬 쉬울 것이라고 생각합니다. 그래서이 테스트의 일부로 계산에 관심이없는 코드를 모의 할 수 있습니다. "저장소를 호출하라고 말했지만 여기서 데이터가 변경되지 않기 때문에"이것은 윙크를 반환해야합니다 "라는 방법 을 알려주세요. 데이터가 변경되지 않기 때문에 해당 데이터를 사용하는 부분 만 테스트한다는 것을 알고 있습니다. 데이터의 실제 검색.

테스트 할 때 기본적으로 원하는 기능을 처음부터 끝까지 테스트하는 통합 (기능) 테스트와 각 코드 조각 (일반적으로 방법 또는 기능 수준에서)을 독립적으로 테스트하는 전체 단위 테스트를 원합니다.

작동하지 않는 코드의 정확한 부분을 알고 싶지 않다면 전체 기능이 작동하는지 확인하는 것이 좋습니다.

이 작업은 Dependency Injection없이 수행 할 있지만 일반적으로 프로젝트가 커짐에 따라 Dependency Injection을 설치하지 않아도 점점 더 번거로워집니다. (항상 프로젝트가 성장할 것이라고 가정하십시오! 프로젝트가 빠르게 증가하고 상황이 이미 시작된 후에 심각한 리팩토링 및 리엔지니어링을 요구하는 것보다 유용한 기술을 불필요하게 연습하는 것이 좋습니다.)


1
대부분의 의존성 그래프를 사용하지 않고 테스트를 작성하는 것은 실제로 DI에 대한 좋은 논거입니다.
08:21 준수

1
DI를 사용하면 프로그래밍 방식으로 코드 전체를 쉽게 스왑 할 수 있습니다. 시스템이 완전히 다른 클래스의 인터페이스를 지우고 다시 주입하여 원격 서비스의 중단 및 성능 문제에 대응하는 사례를 보았습니다. 그것을 처리하는 더 좋은 방법이 있었을 지 모르지만 놀랍게 잘 작동했습니다.
RualStorge

0

내가 언급으로 다른 답변에서 , 여기에 문제는 수업이 원하는 것입니다 A에 의존하는 일부 클래스B 하드 코딩하지 않고 있는 클래스 B유일한 방법은 클래스를 가져 오기 때문에 A.이의 소스 코드에 사용되는 자바와 C #에서 불가능하다 전역 고유 이름으로이를 참조합니다.

인터페이스를 사용하면 하드 코딩 된 클래스 종속성을 해결할 수 있지만 여전히 인터페이스 인스턴스에 손을 대야하며 생성자를 호출 할 수 없거나 1로 돌아갑니다. 이제 코드 그렇지 않으면 의존성을 만들어 다른 사람에게 그 책임을지게 할 수 있습니다. 그리고 그 의존성은 같은 일을하고 있습니다. 따라서 클래스의 인스턴스가 필요할 때마다 전체 종속성 트리를 수동으로 작성하는 반면 클래스 A가 B에 직접 의존하는 경우 호출 new A()하고 해당 생성자를 호출 할 수 있습니다 new B().

의존성 주입 프레임 워크는 클래스 간의 매핑을 지정하고 의존성 트리를 작성함으로써이를 해결하려고합니다. 중요한 것은 매핑을 망칠 때 매핑 모듈을 일급 개념으로 지원하는 언어 에서처럼 컴파일 타임이 아니라 런타임에 알 수 있다는 것입니다.


0

나는 이것이 큰 오해라고 생각합니다.

Guice는 의존성 주입 프레임 워크 입니다. DI를 자동으로 만듭니다 . 그들이 인용 한 발췌문에서 그들이 지적한 요점은 Guice가 예제에서 제시 한 "테스트 가능한 생성자"를 수동으로 생성 할 필요가 없다는 것입니다. 의존성 주입 자체와는 전혀 관련이 없습니다.

이 생성자 :

BillingService(CreditCardProcessor processor, TransactionLog transactionLog)
{
    this.processor = processor;
    this.transactionLog = transactionLog;
}

이미 의존성 주입을 사용하고 있습니다. 당신은 기본적으로 DI를 사용하는 것이 쉽다고 말했습니다.

Guice가 해결하는 문제는 해당 생성자를 사용 하려면 이미 생성 된 객체를 해당 생성자의 인수로 수동으로 전달 하는 객체 그래프 생성자 코드가 어딘가에 있어야한다는 것 입니다. Guice를 사용하면 실제 구현 클래스가 해당 클래스 CreditCardProcessorTransactionLog인터페이스에 해당하는 것을 구성 할 수있는 단일 위치를 가질 수 있습니다 . 해당 구성 후에 BillingServiceGuice 를 사용 하여 만들 때마다 해당 클래스가 자동으로 생성자에 전달됩니다.

이것이 의존성 주입 프레임 워크 가하는 일입니다. 그러나 제시 한 생성자 자체는 이미 디펜 던시 인젝션 원칙 의 구현입니다 . IoC 컨테이너와 DI 프레임 워크는 해당 원칙을 자동화하는 수단이지만 모든 것을 직접 수행해야하는 것은 아닙니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.