의존성 주입이 너무 많습니까?


38

말 그대로 클래스의 종속성 인 모든 것에 대해 (Spring) Dependency Injection을 사용하는 프로젝트에서 작업합니다. 우리는 Spring 설정 파일이 약 4000 줄로 성장한 시점에 있습니다. 얼마 전 나는 YouTube에서 Bob Uncle의 대화 중 하나를 보았습니다 (불행히도 링크를 찾을 수 없었습니다). 분배된다.

이 접근법의 장점은 DI 프레임 워크를 응용 프로그램의 대부분에서 분리하는 것입니다. 또한 팩토리는 이전에 구성에 있던 내용을 훨씬 더 많이 포함하므로 Spring 구성을 더 깨끗하게 만듭니다. 반대로, 이것은 많은 팩토리 클래스에 생성 로직을 확산시키고 테스트가 더 어려워 질 수 있습니다.

그래서 제 질문은 실제로 당신이 하나 또는 다른 접근 방식에서 볼 수있는 다른 장점이나 단점입니다. 모범 사례가 있습니까? 답변 주셔서 감사합니다!


3
4000 라인 구성 파일을 갖는 것은 의존성 주입의 결함이 아닙니다 ... 파일을 여러 개의 작은 파일로 모듈화하고 주석 기반 주입으로 전환하고 xml 대신 JavaConfig 파일을 사용하여 파일을 수정하는 방법에는 여러 가지가 있습니다. 기본적으로 발생하는 문제는 응용 프로그램의 종속성 주입 요구 사항의 복잡성과 크기를 관리하지 못하는 것입니다.
Maybe_Factor

1
@Maybe_Factor TL; DR은 프로젝트를 더 작은 구성 요소로 나눕니다.
Walfrat

답변:


44

항상 그렇듯이 It Depends ™. 답은 해결하려는 문제에 달려 있습니다. 이 답변에서 나는 일반적인 동기 부여를 다루려고 노력할 것이다.

더 작은 코드 기반 선호

4,000 줄의 Spring 구성 코드가 있다면 코드베이스에 수천 개의 클래스가 있다고 가정합니다.

사실 후에 해결할 수있는 문제는 아니지만 일반적으로 코드베이스가 더 작은 더 작은 응용 프로그램을 선호하는 경향이 있습니다. 예를 들어 Domain-Driven Design을 사용하는 경우 경계 컨텍스트별로 코드베이스를 만들 수 있습니다.

경력의 대부분을 위해 웹 기반 업무용 코드를 작성했기 때문에 제한된 경험을 바탕으로이 조언을하고 있습니다. 데스크톱 응용 프로그램이나 임베디드 시스템 또는 기타를 개발하는 경우 문제를 해결하기가 더 어렵다고 생각할 수 있습니다.

이 첫 번째 조언이 가장 실용적이지 않다는 것을 잘 알고 있지만, 그것이 가장 중요하다고 생각합니다. 코드의 복잡성은 코드베이스의 크기에 따라 비선형 적으로 (지수 적으로) 변합니다.

순수한 DI 선호

이 질문이 기존 상황에 해당한다는 것을 여전히 알고 있지만 Pure DI를 권장 합니다. DI 컨테이너를 사용하지 말고 사용하는 경우 최소한 컨벤션 기반 컴포지션을 구현하는 데 사용하십시오 .

Spring에 대한 실질적인 경험은 없지만 구성 파일 에 의해 XML 파일이 암시 된다고 가정합니다 .

XML을 사용하여 종속성을 구성하는 것은 두 세계에서 최악입니다. 첫째, 컴파일 타임 유형의 안전을 잃지 만 아무것도 얻지 못합니다. XML 구성 파일은 대체하려는 코드만큼 쉽게 커질 수 있습니다.

의존성 주입 구성 파일은 해결해야 할 문제와 비교할 때 구성 복잡성 시계 에서 잘못된 위치를 차지합니다 .

거친 의존성 주입의 경우

거친 의존성 주입의 경우를 만들 수 있습니다. 또한 세분화 된 종속성 주입에 대한 사례를 만들 수도 있습니다 (다음 섹션 참조).

몇 가지 '중앙'종속성을 주입하면 대부분의 클래스는 다음과 같습니다.

public class Foo
{
    private readonly Bar bar;

    public Foo()
    {
        this.bar = new Bar();
    }

    // Members go here...
}

이것은 작성 하기 때문에 클래스 상속보다 Design Patterns선호 오브젝트 구성에 여전히 적합 Foo합니다 Bar. 구성을 변경해야하는 경우에 대한 소스 코드를 편집하기 때문에 유지 관리 측면에서 유지 관리가 가능한 것으로 간주 될 수 있습니다 Foo.

이것은 의존성 주입보다 유지 관리가 거의 불가능합니다. 사실, Bar의존성 주입 고유의 간접 지시를 따르지 않고 를 사용하는 클래스를 직접 편집하는 것이 더 쉽다고 말하고 싶습니다 .

의존성 주입에 관한 저의 책 의 첫 번째 판에서 , 나는 휘발성과 안정적인 의존성을 구별합니다.

휘발성 종속성은 주입을 고려해야하는 종속성입니다. 그들은 포함

  • 컴파일 후 재구성 할 수있는 종속성
  • 다른 팀과 병행하여 개발 한 종속성
  • 비 결정적 행동 또는 부작용이있는 행동의 의존성

반면에 안정적인 종속성은 잘 정의 된 방식으로 동작하는 종속성입니다. 어떤 의미에서, 당신은이 차이로 인해 거친 의존성 주입이 가능하다고 주장 할 수 있지만, 나는이 책을 썼을 때 그것을 완전히 깨닫지 못했다는 것을 인정해야합니다.

그러나 테스트 관점에서는 단위 테스트가 더 어렵습니다. 당신은 더 이상 단위 테스트 할 수있는 Foo독립적 Bar. JB Rainsberger가 설명 했듯이 통합 테스트는 복잡한 조합의 폭발로 인해 어려움을 겪습니다. 4-5 클래스의 통합을 통해 모든 경로를 다루려면 문자 그대로 수만 개의 테스트 사례를 작성해야합니다.

이에 대한 반론은 종종 당신의 임무는 수업을 프로그래밍하지 않는 것입니다. 당신의 임무는 몇 가지 특정 문제를 해결하는 시스템을 개발하는 것입니다. 이것이 BDD ( Behaviour-Driven Development )의 동기 입니다.

이에 대한 또 다른 견해는 DDD가 제시 한 것으로 TD는 테스트로 인한 설계 손상을 초래 한다고 주장합니다 . 그는 또한 거친 통합 테스트를 선호합니다.

소프트웨어 개발에 대한 이러한 관점을 취한다면, 대략적인 의존성 주입이 의미가 있습니다.

세분화 된 의존성 주입의 경우

반면에 세밀한 의존성 주입은 모든 것을 주입하는 것으로 설명 될 수 있습니다 !

거친 의존성 주입에 대한 나의 주요 관심사는 JB Rainsberger가 표현한 비판입니다. 모든 코드 경로를 포괄하기 위해 문자 그대로 수천 또는 수만 개의 테스트 사례를 작성해야하므로 통합 테스트로 모든 코드 경로를 처리 할 수는 없습니다.

BDD의 지지자들은 테스트로 모든 코드 경로를 다룰 필요는 없다는 주장에 대응할 것이다. 비즈니스 가치를 창출하는 제품 만 커버하면됩니다.

그러나 내 경험상 모든 '이국적인'코드 경로는 대량 배포에서도 실행되며 테스트하지 않으면 많은 결함이 발생하여 런타임 예외 (종종 null 참조 예외)가 발생합니다.

이로 인해 모든 객체의 불변량을 분리하여 테스트 할 수 있기 때문에 세분화 된 종속성 주입을 선호했습니다.

기능성 프로그래밍 선호

세밀한 의존성 주입에 의존하는 동안 함수 프로그래밍에 중점을 두었습니다. 그 이유 는 본질적으로 테스트 가능 하기 때문 입니다.

SOLID 코드로 이동 하면할수록 기능이 향상됩니다 . 조만간, 당신도 뛰어들 수 있습니다. 기능적 아키텍처는 포트 및 어댑터 아키텍처 이며 종속성 주입도 시도이며 포트 및 어댑터 입니다. 그러나 Haskell과 같은 언어는 유형 시스템을 통해 해당 아키텍처를 적용한다는 점이 다릅니다.

정적으로 유형화 된 기능 프로그래밍 선호

이 시점에서 필자는 OOP (Object-Oriented Programming)를 기본적으로 포기했지만 OOP의 많은 문제는 본질적으로 개념 자체보다 Java 및 C #과 같은 주류 언어에 본질적으로 연결되어 있습니다.

주류 OOP 언어의 문제점은 테스트되지 않은 런타임 예외로 이어지는 조합 폭발 문제를 피하는 것이 거의 불가능하다는 것입니다. 반면에 Haskell 및 F #과 같이 정적으로 형식이 지정된 언어를 사용하면 형식 시스템에서 많은 결정 지점을 인코딩 할 수 있습니다. 즉, 수천 개의 테스트를 작성하는 대신 컴파일러가 가능한 모든 코드 경로를 처리했는지 여부를 알 수 있습니다 (은색 총알이 아님).

또한 의존성 주입은 작동하지 않습니다 . 진정한 함수형 프로그래밍 은 종속성의 전체 개념을 거부 해야합니다 . 결과는 더 간단한 코드입니다.

요약

C #으로 작업해야한다면, 세밀한 의존성 주입을 선호하는데, 그 이유는 관리 가능한 수의 테스트 사례로 전체 코드베이스를 커버 할 수 있기 때문입니다.

결국 제 동기는 빠른 피드백입니다. 그럼에도 불구하고 단위 테스트 만이 피드백을 얻는 유일한 방법은 아닙니다 .


1
저는이 답변, 특히 Spring과 관련하여 사용 된이 문장이 정말 마음에 듭니다. "XML을 사용하여 종속성을 구성하는 것은 두 세계에서 최악입니다. 첫째, 컴파일 타임 유형 안전을 잃지 만 아무 것도 얻지 못합니다. XML 구성 파일은 대체하려는 코드만큼 쉽게 커질 수 있습니다. "
토마스 칼라일

@ThomasCarlisle은 코드를 변경하기 위해 코드를 만질 필요가없는 XML 구성의 핵심이 아니 었습니까? 마크가 언급 한 두 가지 이유로 나는 그것을 사용하지 않았거나 간신히 사용했지만, 당신은 그 대가로 무언가를 얻습니다.
El Mac

1

거대한 DI 설정 클래스가 문제입니다. 그러나 그것이 대체하는 코드를 생각하십시오.

모든 서비스를 인스턴스화해야합니다. 그렇게하는 코드는 app.main()시작점에 있고 수동으로 주입되거나 this.myService = new MyService();클래스 내 에서처럼 밀접하게 결합됩니다 .

설정 클래스를 여러 설정 클래스로 분할하여 프로그램 시작 지점에서 호출하여 설치 클래스의 크기를 줄입니다. 즉.

main()
{
   var c = new diContainer();
   var service1 = diSetupClass.SetupService1(c);
   var service2 = diSetupClass.SetupService2(c, service1); //if service1 is required by service2
   //etc

   //main logic
}

service1 또는 2를 다른 ci 설정 방법으로 전달하는 것을 제외하고는 서비스를 참조 할 필요가 없습니다.

이것은 설정 호출 순서를 강제하기 때문에 컨테이너에서 가져 오는 것보다 낫습니다.


1
이 개념을 더 자세히 살펴보고 싶은 사람들 (프로그램 시작 지점에서 클래스 트리 작성)은 검색 할 가장 좋은 용어는 "composition root"입니다.
e_i_pi

@ Ewan 그 ci변수는 무엇입니까?
superjos September

정적 메소드를 사용하는 ci 설정 클래스
Ewan

urg는 di 여야합니다. '컨테이너'를 계속 생각합니다
Ewan
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.