자동화 테스트를 배우고 사랑했기 때문에 거의 모든 프로젝트에서 의존성 주입 패턴을 사용하는 것을 발견했습니다. 자동화 된 테스트 작업시이 패턴을 사용하는 것이 항상 적절합니까? 의존성 주입을 피해야하는 상황이 있습니까?
자동화 테스트를 배우고 사랑했기 때문에 거의 모든 프로젝트에서 의존성 주입 패턴을 사용하는 것을 발견했습니다. 자동화 된 테스트 작업시이 패턴을 사용하는 것이 항상 적절합니까? 의존성 주입을 피해야하는 상황이 있습니까?
답변:
기본적으로 의존성 주입은 객체의 특성에 대해 (보통은 아니지만 항상 유효한 것은 아닙니다) 가정합니다. 잘못된 경우 DI가 최상의 솔루션이 아닐 수 있습니다.
첫째, 가장 기본적으로 DI는 객체 구현의 긴밀한 결합이 항상 나쁘다고 가정합니다 . 이것이 의존성 역전 원칙의 본질이다 : "결석에 의존해서는 안되며 추상화에만 의존해야한다".
이것은 구체적인 구현에 대한 변경에 따라 변경 될 종속 개체를 닫습니다. 출력이 파일로 이동 해야하는 경우 ConsoleWriter에 따라 클래스를 변경해야하지만 Write () 메서드를 노출하는 IWriter에만 의존하는 클래스는 현재 사용중인 ConsoleWriter를 FileWriter 및 종속 클래스는 차이를 알지 못합니다 (Liskhov 대체 원칙).
그러나 디자인이 모든 유형의 변경에 대해 폐쇄 될 수는 없습니다. IWriter 인터페이스의 디자인 자체가 변경되면 Write ()에 매개 변수를 추가하기 위해 구현 객체 / 메소드 및 해당 사용법 위에 추가 코드 객체 (IWriter 인터페이스)를 변경해야합니다. 실제 인터페이스의 변경이 상기 인터페이스의 구현에 대한 변경보다 더 가능성이 높으면, 느슨한 결합 (및 DI 결합의 느슨하게 결합 된 종속성)이 해결하는 것보다 더 많은 문제를 야기 할 수 있습니다.
둘째, 그리고 결론적으로, DI는 의존 클래스가 의존성을 생성하기에 좋은 장소라고 절대 가정하지 않는다 . 이것은 단일 책임 원칙으로갑니다. 종속성을 생성하고 사용하는 코드가있는 경우 종속 클래스가 SRP를 위반하여 변경 (사용 또는 구현 변경)해야하는 두 가지 이유가 있습니다.
그러나 DI에 대한 간접 레이어를 추가하는 것은 존재하지 않는 문제에 대한 해결책이 될 수 있습니다. 종속성에서 논리를 캡슐화하는 것이 논리적이지만 해당 논리가 종속성의 유일한 구현이라면 종속성의 느슨하게 결합 된 해상도 (사출, 서비스 위치, 팩토리)를 코딩하는 것보다 더 고통 스럽습니다. 그냥 사용 new
하고 잊어 버리십시오.
마지막으로, DI는 그 특성상 모든 의존성과 구현에 대한 지식을 중앙 집중화합니다 . 이것은 주입을 수행하는 어셈블리가 가져야하는 참조 수를 증가 시키며 대부분의 경우 실제 종속 클래스의 어셈블리에 필요한 참조 수를 줄이지 않습니다.
SOMETHING, SOMEWINGE는 "도트를 연결"하고 해당 종속성을 만족시키기 위해 종속 인터페이스, 종속성 인터페이스 및 종속성 구현에 대한 지식이 있어야합니다. DI는 모든 지식을 IoC 컨테이너 또는 의존성을 수화 (또는 팩토리 방식으로 제공)해야하는 기본 형식 또는 컨트롤러와 같은 "주요"개체를 생성하는 코드에서 매우 높은 수준으로 배치하는 경향이 있습니다. 이를 통해 앱의 상위 레벨에 꼭 필요한 코드와 어셈블리 참조를 많이 넣을 수 있습니다. 실제 종속 클래스에서 코드를 숨기려면이 지식 만 있으면됩니다. 이 지식을 얻는 가장 좋은 장소; 그것이 사용되는 곳).
또한 일반적으로 코드에서 아래쪽에서 상기 참조를 제거하지 않습니다. 종속물은 여전히 의존성에 대한 인터페이스를 포함하는 라이브러리를 참조해야하며, 다음 세 위치 중 하나에 있습니다.
이 모든 것이 다시없는 곳에서 문제를 해결하기 위해 다시 한 번.
의존성 주입 프레임 워크 외부에서, 의존성 주입 (생성자 주입 또는 세터 주입을 통한)은 거의 제로섬 게임입니다. 객체 A와 의존성 B 사이의 연결을 줄이지 만 이제 A의 인스턴스가 필요한 모든 객체는 이제 또한 객체 B를 구성하십시오.
A와 B 사이의 연결을 약간 줄 였지만 A의 캡슐화를 줄이고 A와 A의 종속성에 연결하여 A의 인스턴스를 구성해야하는 클래스 간의 연결을 늘 렸습니다.
따라서 프레임 워크가없는 종속성 주입은 도움이되므로 거의 해롭지 않습니다.
그러나 추가 비용은 종종 정당화하기 쉽다. 클라이언트 코드가 객체 자체보다 의존성을 구성하는 방법에 대해 더 많이 알고 있다면 의존성 주입은 실제로 커플 링을 줄인다. 예를 들어, 스캐너는 입력을 구문 분석하기 위해 입력 스트림을 가져 오거나 구성하는 방법 또는 클라이언트 코드가 입력을 구문 분석하려는 소스에 대해 잘 모르므로 입력 스트림의 생성자 주입이 확실한 솔루션입니다.
모의 의존성을 사용하기 위해서는 테스트가 또 다른 정당화입니다. 즉, 의존성을 주입 할 수 있도록 테스트에만 사용되는 추가 생성자를 추가해야합니다. 대신 의존성을 항상 주입해야하도록 생성자를 변경하는 경우 갑자기 의존성을 생성하기 위해 의존성의 종속성에 대해 알아야합니다. 직접적인 의존성, 어떤 작업도 할 수 없습니다.
도움이 될 수 있지만 각 의존성에 대해 스스로에게 물어봐야하며, 테스트 가치가 비용의 가치가 있으며, 테스트하는 동안이 의존성을 조롱하고 싶습니까?
의존성 주입 프레임 워크가 추가되고 의존성 구성이 클라이언트 코드가 아닌 프레임 워크에 위임되면 비용 / 이익 분석이 크게 변경됩니다.
의존성 주입 프레임 워크에서 트레이드 오프는 약간 다릅니다. 의존성을 주입하여 잃어버린 것은 어떤 구현에 의존하고 있는지 쉽게 알 수 있고 자동화 된 해결 프로세스에 의존하는 의존성을 결정하는 책임을 바꾸는 것입니다 (예 : @ Inject'ed Foo가 필요한 경우) , @Provides Foo를 제공하고 주입 된 종속성을 사용할 수있는 것) 또는 각 리소스에 사용해야하는 공급자를 규정하는 일부 고급 구성 파일 또는 두 가지의 하이브리드에 대한 것 (예 : 필요한 경우 구성 파일을 사용하여 재정의 할 수있는 종속성에 대한 자동화 된 해결 프로세스 여야합니다.
생성자 주입에서와 같이 그렇게하는 것의 장점은 비용과 매우 유사하다는 결론을 내릴 수 있습니다. 누가 의존하는 데이터를 제공하는지 누가 여러 가지 가능성이 있는지 알 필요가 없습니다. 제공자는 제공자를 체크인하기 위해 선호하는 순서를 알 필요가 없으며 모든 잠재적 제공자 등의 데이터 검사가 필요한 모든 위치를 확인해야합니다. 모든 위치는 종속성 주입에 의해 높은 수준으로 처리되므로 플랫폼.
개인적으로 DI 프레임 워크에 대한 경험이 많지는 않지만, 필요한 데이터 또는 서비스의 올바른 공급자를 찾는 데 어려움이 두통보다 비용이 높을 때 비용보다 더 많은 이점을 제공한다는 인상이 있습니다. 어떤 코드가 실패했는지, 어떤 코드가 로컬에서 나쁜 데이터를 제공했는지 나중에 코드에서 나중에 실패하게하는 원인을 즉시 알지 못하는 경우.
경우에 따라 DI 프레임 워크가 현장에 나타 났을 때 의존성을 모호하게하는 다른 패턴 (예 : 서비스 로케이터)이 이미 채택되었고 그 가치가 입증 된 경우도있었습니다. 더 적은 상용구 코드, 또는 실제로 사용중인 제공자를 판별해야 할 때 제공자의 종속성을 가릴 수있는 가능성이 줄어 듭니다.
데이터베이스 엔티티를 작성하는 경우 컨트롤러에 대신 주입 할 팩토리 클래스가 있어야합니다.
int 또는 long과 같은 기본 객체를 만들어야하는 경우 또한 날짜, 안내 등과 같은 대부분의 표준 라이브러리 객체를 "손으로"작성해야합니다.
구성 문자열을 주입하려면 일부 구성 오브젝트를 주입하는 것이 좋습니다. 일반적으로 단순 유형을 의미있는 오브젝트로 랩핑하는 것이 좋습니다. int temperatureInCelsiusDegrees-> CelciusDeegree temperature)
그리고 의존성 주입 대안으로 Service locator를 사용하지 마십시오. 안티 패턴, 자세한 정보 : http://blog.ploeh.dk/2010/02/03/ServiceLocatorIsAnAntiPattern.aspx
프로젝트를 유지 관리하고 테스트 할 수있게하여 아무것도 얻지 못하는 경우.
진심으로, 나는 일반적으로 IoC와 DI를 좋아하며, 98 %의 시간이 그 패턴을 반드시 사용할 것이라고 말하고 싶습니다. 로직을 구현에서 분리하므로 다른 팀 구성원과 다른 프로젝트에서 코드를 반복해서 재사용 할 수있는 다중 사용자 환경에서 특히 중요합니다. 로깅은 이것의 대표적인 예입니다. 클래스에 주입 된 ILog 인터페이스는 로깅 프레임 워크를 단순히 플러그인하는 것보다 유지 관리가 가능합니다. 다른 프로젝트가 동일한 로깅 프레임 워크를 사용한다는 보장이 없기 때문입니다 (사용하는 경우) 하나!).
그러나 적용 가능한 패턴이 아닌 경우가 있습니다. 예를 들어, 재정의 불가능한 이니셜 라이저 (WebMethods, 나는 당신을보고 있지만 Program 클래스의 Main () 메소드는 또 다른 예)에 의해 정적 컨텍스트에서 구현되는 기능적 진입 점은 초기화시 의존성을 주입 할 수 없습니다. 시각. 또한 프로토 타입 또는 모든 버리기 조사 코드도 나쁜 후보라고 말할 수 있습니다. 일주일 이내에 코드의 대부분을 버릴 것이라고 확신한다면 DI의 이점은 거의 중장기적인 이점 (테스트 가능성 및 유지 관리 가능성)입니다. 의존성, 일반적으로 코드를 작동시키는 데 의존성을 테스트하고 격리하는 데 소비하는 시간을 보내십시오.
대체로 100 % 적용 할 수있는 방법이 없기 때문에 모든 방법론이나 패턴에 실용적인 접근 방식을 취하는 것이 합리적입니다.
주목할 것은 자동화 된 테스트에 대한 귀하의 의견입니다.이 정의는 자동화 된 기능 테스트입니다 (예 : 웹 컨텍스트에있는 경우 스크립트 된 셀레늄 테스트). 이것들은 일반적으로 코드의 내부 작동에 대해 알 필요가없는 완전 블랙 박스 테스트입니다. 단위 테스트 또는 통합 테스트를 언급하는 경우 DI 패턴은 거의 항상 이러한 종류의 화이트 박스 테스트에 의존하는 모든 프로젝트에 적용 할 수 있다고 말합니다. 예를 들어 다음과 같은 것을 테스트 할 수 있기 때문입니다. DB가 없어도 DB를 만지는 방법.
다른 답변은 기술적 측면에 중점을두고 있지만 실용적인 차원을 추가하고 싶습니다.
수년에 걸쳐 Dependency Injection을 도입하는 데 성공하려면 몇 가지 실용적인 요구 사항이 충족되어야한다는 결론에 도달했습니다.
소개 할 이유가 있어야합니다.
이것은 분명한 것처럼 들리지만 코드가 데이터베이스에서 물건을 가져 와서 논리없이 반환하면 DI 컨테이너를 추가하면 실제로 이익을 얻지 않고 더 복잡하게 만듭니다. 여기서 통합 테스트가 더 중요합니다.
팀은 훈련을 받고 탑승해야합니다.
팀의 대다수가 기내에 있고 DI가 컨트롤 컨테이너의 반전을 추가하는 것이 작업을 수행하는 또 다른 방법이되고 코드 기반을 더욱 복잡하게 만드는 것을 이해하지 않는 한.
DI가 팀의 새로운 구성원에 의해 도입 된 경우, DI를 이해하고 좋아하고 자신이 훌륭하다는 것을 보여주고 싶기 때문에 팀이 적극적으로 참여하지 않으면 실제로 품질이 저하 될 위험이 있습니다. 코드.
테스트해야합니다
디커플링은 일반적으로 좋은 방법이지만 DI는 종속성의 해결을 컴파일 시간에서 런타임으로 이동할 수 있습니다. 잘 테스트하지 않으면 실제로 매우 위험합니다. 런타임 해결 실패는 추적 및 해결 비용이 많이들 수 있습니다.
테스트 결과는 분명하지만 많은 팀이 DI에서 요구하는 범위까지 테스트하지는 않습니다.
이것은 완전한 대답이 아니라 또 다른 요점입니다.
한 번 시작하는 응용 프로그램이 있으면 웹 응용 프로그램과 같이 오랫동안 실행되는 DI가 좋습니다.
모바일 앱과 같이 여러 번 시작되고 짧은 시간 동안 실행되는 응용 프로그램이 있으면 컨테이너를 원하지 않을 것입니다.
기본 OOP 원칙을 사용하십시오. 상속 을 사용하여 개인 / 내부 / 보호 멤버 / 유형을 사용하여 외부에서 보호해야하는 공통 기능을 추출하고 사물을 캡슐화 (숨기기)하십시오. 강력한 테스트 프레임 워크를 사용하여 테스트 전용 코드 (예 : https://www.typemock.com/ 또는 https://www.telerik.com/products/mocking.aspx) 를 삽입 하십시오 .
그런 다음 DI로 다시 작성하고 DI에서 일반적으로 볼 수있는 코드를 비교하십시오.
DI에서 코드 품질이 거의 항상 저하되는 것을 보았습니다.
그러나 클래스 선언에서 "공개"액세스 수정 자 및 / 또는 멤버에 대한 공개 / 개인 수정자를 사용하거나 값 비싼 테스트 도구를 구매할 수없고 동시에 테스트 할 수있는 단위 테스트가 필요한 경우 통합 테스트로 교체하거나 이미 주입하고자하는 수업을위한 인터페이스가 있다면 DI가 좋은 선택입니다!
추신 : 아마도이 게시물에 대한 많은 마이너스를 얻을 것입니다. 현대 개발자의 대부분은 내부 키워드 를 사용하는 방법과 이유 및 구성 요소의 결합을 줄이는 방법을 이해하지 못하기 때문에 마침내) 코딩하고 비교하려고
Dependency Injection 의 대안은 Service Locator를 사용하는 것 입니다. 서비스 로케이터는 DI 프레임 워크를 사용하지 않는 경우 이해하기 쉽고 디버그가 용이하며 객체 구성을 더 단순하게 만듭니다. 서비스 로케이터는 외부 정적 종속성 ( 예 : 데이터 액세스 계층의 모든 개체에 전달해야하는 데이터베이스) 을 관리하기 위한 좋은 패턴입니다 .
때 레거시 코드를 리팩토링 , 종속성 주입보다 서비스 로케이터로 리팩토링하는 것이 종종 더 쉽다. 인스턴스화를 서비스 조회로 교체 한 다음 단위 테스트에서 서비스를 위조하기 만하면됩니다.
그러나 Service Locator 에는 몇 가지 단점 이 있습니다 . 종속성은 생성 자나 설정자가 아닌 클래스의 구현에 숨겨져 있기 때문에 클래스의 depandancies를 아는 것이 더 어렵습니다. 그리고 동일한 서비스의 서로 다른 구현에 의존하는 두 개의 객체를 생성하는 것은 어렵거나 불가능합니다.
TLDR : 클래스에 정적 종속성이 있거나 레거시 코드를 리팩토링하는 경우 서비스 로케이터가 DI보다 낫습니다.