오브젝트; 의존성 주입과 의존 역전

🗓️

의존성 주입

  • 의존성 주입 Dependency Injection : 사용하는 객체가 아닌 외부의 독립적인 객체가 인스턴스를 생성한 후 이를 전달해서 의존성을 해결하는 방법

의존성 주입 기법

  • 생성자 주입
  • Setter 주입
  • 메서드 주입

숨겨진 의존성은 나쁘다

SERVICE LOCATOR 패턴

서비스를 사용하는 코드로부터 서비스가 누구인지 (서비스를 구현한 구체 클래스의 타입이 무엇인지), 어디에 있는지(클래스 인스턴스를 어떻게 얻을지)를 몰라도 되게 해준다.

public class Movie {
    private DiscountPolicy discountPolicy;

    public Movie(String title, Duration runningTime, Money fee) {
        //.. constructure
        this.discountPolicy = ServiceLocator.discountPolicy();
    }
}

public class ServiceLocator {
    private static ServiceLocator soleInstance = new ServiceLocator();
    private DiscountPolicy discountPolicy;

    public static DiscountPolicy discountPolicy() {
        return soleInstance.discountPolicy;
    }

    public static void provide(DiscountPolicy discountPolicy) {
        soleInstance.discountPolicy = discountPolicy;
    }

    private ServiceLocator() {
    }
}
  • 사용처에서 특정 인스터스에 의존하기 원한다면 아래와 같이 사용해야 한다.
//...
ServiceLocator.provide(new AmountDiscountPolicy(...));
Movie avatar = new Movie("아바타",
                         Duration.ofMinutes(120),
                         Money.wons(10_000));
  • Movie 생성자 만으로 의존성이 해결되지 않기 때문에 SERVICE LOCATOR 패턴은 의존성을 감추는 문제가 있다.
  • 이런식으로 의존성을 감추는 경우 컴파일러는 알아낼 수 없다. 결국 런타임에 가야 문제가 발생한다는 것을 알 수 있다.
  • 문제의 원인은 숨겨진 의존성이 캡슐화를 위반했기 때문이다.
  • 숨겨진 의존성이 가지는 문제는 의존성을 이해하기 위해 코드의 내부 구현을 이해할 것을 강요하기 때문이다.
  • 명시적인 의존성이 숨겨진 의존성보다 좋다.

접근해야 할 객체가 있다면 전역 메커니즘 대신, 필요한 객체를 인수로 넘겨줄 수는 없는지부터 생각해보자. 이 방법은 굉장히 쉬운데다 결합을 명확하게 보여줄 수도 있다. 대부분은 이렇게만 해도 충분하다. 하지만 직접 객체를 넘기는 방식이 불필요하거나 도리오 코드를 읽기 어렵게 하기도 한다. 로그나 메모리 관리 같은 정보가 모듈의 공개 API에 포함되어 있어서는 안 된다. 렌더링 함수 매개변수에는 렌더링에 관한 것만 있어야 하며 로그가 같은 것이 섞여 있어서는 곤란하다. 또한 어떤 시스템은 본질적으로 하나 뿐이다. 대부분의 게임 플랫폼에는 오디오나 디스플레이 시스템이 하나만 있다. 이런 환경적인 특징을 10겹의 메서드 계층을 통해 가장 깊숙히 들어있는 함수에 전달하는 것은 쓸데없이 복잡성을 늘리는 셈이다.

의존성 역전 원칙

추상화와 의존성 역전

public abstract class DiscountPolicy {
    private List<DiscountCondition> conditions = new ArrayList<>();
}

public class AmountDiscountPolicy extends DiscountPolicy {
    // ..
}

public class Movie {
    private AmountDiscountPolicy discountPolicy; //<<<<<
}

상위수준 클래스가 하위 수준 클래스를 의존할때 생기는 문제

  • 구체 클래스에 대한 의존으로 결합도가 높아진다.
  • 하위 수준의 변경에 의해 상위 수준 클래스가 영향을 받는다.
  • 의존성은 Movie에서 AmountDiscountPolicy로 흐르면 안되고 Movie에서 AmountDiscountPolicy로 흘러야 한다.
  • Movie를 재사용하기 위해서는 의존 대상인 AmountDiscountPolicy도 재사용 해야 하기 때문에 어려워 진다.

의존성 역전 원칙 Dependency Inversion Principle

  1. 상위 수준의 모듈은 하위 수준의 모듈에 의존해서는 안 된다. 둘 모두 추상화에 의존해야 한다.
  2. 추상화는 구체적인 사항에 의존해서는 안된다. 구체적인 사항은 추상화에 의존해야 한다.

의존성 역전 원칙과 패키지

  • 역전은 의존성의 방향 뿐만 아니라 인터페이스의 소유권에도 적용된다.
  • 객체지향 프로그래밍 언어에서 어떤 구성 요소의 소유권을 결정하는 것은 모듈이다.
    • 자바 : Package
    • C#, C++ : Namespace

SPERATED INTERFACE 패턴

  • 재사용될 필요가 없는 클래스들은 별도의 독립적인 패키지에 모으는 패턴.

유연성에 대한 조언

유연한 설계는 유연성이 필요할 때만 옳다

유연하고 재사용 가능한 설계

  • 런타임 의존성과 컴파일타임 의존성의 차이를 인식하고 동일한 컴파일타임 의존성으로부터 다양한 런타임 의존성을 만들 수 있는 코드 구조를 가지는 설계

유연성은 항상 복잡성을 수반한다

  • 유연하지 않은 설계는 단순하고 명확하다.
  • 유연한 설계는 복잡하고 암시적이다.
  • 컴파일타임과 런타임의 동적인 객체 구조가 다르다는것은 객체지향 프로그래밍의 난이도.
  • 설계가 유연할수록 클래스 구조와 객체 구조 사이의 거리는 점점 멀어진다.
  • 유연함을 얻기 위해 단순성과 명확성을 희생해야 한다.

협력과 책임이 중요하다

  • 설계를 유연하게 만들기 위해서는 먼저 역할, 책임, 협력에 초점을 맞춰야 한다. 다양한 컨텍스트에서 협력을 재사용할 필요가 없다면 설계를 유욘하게 만들 당위성도 함께 사라진다.
  • 객체들이 메시지 전송자의 관점에서 동일한 책임을 수행하는지 여부를 판단할 수 없다면 공통의 추상화를 도출할 수 없다.
  • 동일한 역할을 통해 객체들을 대체 가능하게 만들지 않았다면 협력에 참여하는 객체들은 교체할 필요가 없다.
  • 객체의 생성에 너무 몰입하면 생성과 관련된 불필요한 세부사항에 객체를 결합시킨다. 이것의 방법에 대한 결정은 항상 가장 마지막으로 미뤄야 한다.
  • 불필요한 SINGLETON 패턴은 객체 생성에 관해 너무 이른 시기에 고민하고 결정할 때 도입되는 경향이 있다.