Ch.8 의존성 관리하기

변경과 의존성

  • 의존성은 방향성을 가지며 항상 단방향이다.

  • 의존성은 변경에 의한 영향의 전파 가능성을 암시한다.

의존성 전이 (Transitive Dependency)

  • 예제 - PeriodConditionScreening에 의존할 경우 PeriodConditionScreening의존하는 대상에 대해서도 자동적으로 의존하게 된다.

  • 의존성은 전이될 수 있기 때문에 의존성의 종류를 다음과 같이 나눌 수 있다.

    • 직접 의존성 (Direct Dependency)

      • 한 요소가 다른 요소에 직접 의존하는 경우를 가리킨다.

    • 간접 의존성 (Indirect Dependency)

      • 직접적인 관계는 존재하지 않지만 의존성 전이에 의해 영향이 전파되는 경우를 가리킨다.

      • 코드 안에서 명시적으로 드러나지 않는다.

런타임 의존성과 컴파일타임 의존성

  • 의존성이 컴파일타임 시점에 존재하는지 런타임 시점에 존재하는지에 따라 다음과 같이 나눈다.

    • 런타임 의존성(Run-Time Dependency)

      • 객체 사이의 의존성.

      • 객체지향 애플리케이션에서 런타임의 주인공은 객체다.

    • 컴파일타임 의존성(Compile-Time Dependency)

      • 우리가 작성한 코드의 구조 그 자체.

      • 코드 관점에서 주인공은 클래스이다.

      • 클래스 사이의 의존성.

  • 예제에서...

    • Movie -> DiscountPolicy : 컴파일 타임 의존. implements/extends 등으로 의존성을 알 수 있음

    • Movie -> Amount|PercentDiscountPolicy : 런타임 의존 - 의존성을 갖는 클래스/인터페이스의 interface, abstract class 구현체로 연결된 런타임 의존성

  • 다양한 인스턴스와 협력하기 위해서는 구체적인 클래스를 알아서는 안되고, 런타임에 협력할 객체를 해결해야 한다.

컨텍스트 독립성

  • 구체 클래스에 의존하는 것은 클래스의 인스턴스가 어떤 문맥에서 사용될 것인지를 구체적으로 명시하는 것이므로, 클래스가 이 특정한 문맥에 강하게 결합되므로 다른 문맥에서 사용하기 어려워진다.

  • 반대로 클래스가 사용될 특정한 문맥에 대한 최소한의 가정으로만 이뤄져 있다면 다른 문맥에서 재사용하기 더 수월해지는데, 이를 컨텍스트 독립성이라고 부른다.

그럼 클래스가 실행 컨텍스트에 독립적인데 어떻게 런타임에 실행 컨텍스트에 적합한 객체들과 협력할 수 있을까?

의존성 해결

컴파일타임 의존성을 실행 컨텍스트에 맞는 적절한 런타임 의존성으로 교체하는 것을 의존성 해결이라고 부른다. 다음과 같은 세 가지 방법이 있다.

  1. 객체를 생성하는 시점에 생성자를 통해 의존성 해결

  2. 객체 생성 후 setter 메서드를 통해 의존성 해결

  3. 메서드 실행 시 인자를 이용해 의존성 해결

의존성과 결합도

  • 바람직한 의존성 - 느슨한 결합도(Loose Coupling) 또는 약한 결합도 (Weak Coupling)

  • 바람직하지 못한 의존성 - 단단한 결합도 (Tight Coupling) 또는 강한 결합도 (Strong Coupling)

추상화와 결합도의 관점에 따른 의존성의 종류

추상화와 결합도의 관점에서 의존 대상을 다음과 같이 구분하는 것이 유용하다. 목록에서 아래쪽으로 갈수록 클라이언트가 알아야 하는 지식의 양이 적어지기 때문에 결합도가 느슨해진다.

  • 구체 클래스 의존성 (Concrete Class Dependency)

  • 추상 클래스 의존성 (Abstract Class Dependency)

  • 인터페이스 의존성 (Interface Dependency) -> 가장 바람직!

인터페이스에 의존하면 상속 계층을 모르더라도 협력이 가능해진다 -- "컨텍스트 독립성".

  • 결합도를 느슨하게 하기 위해서는 추상클래스나 인터페이스로 선언하는 것만으로는 부족하다.

  • 생성자, setter 메서드, 메서드 인자로 의존성을 해결할 때는 추상 클래스를 상속받거나 인터페이스를 실체화한 구체 클래스를 전달하는 것이다.

스프링에서의 의존성 주입을 생각하자.

의존성을 - 생성자의 인자로 받기 v.s. 생성자 안에서 직접 생성하기

퍼블릭 인터페이스를 통해 할인 정책을 설정할 수 있는 방법을 제공하는지 여부이다.

  • 생성자의 인자로 선언하면 어떤 특정한 의존성을 갖는다는 사실을 명시적으로 퍼블릭 인터페이스에 노출한다. 이를 명시적인 의존성 (Explicit Dependency)라고 부른다.

  • 생성자 안에서 직접 생성하면 의존성을 갖는다는 사실을 감추게 되는데, 이를 숨겨진 의존성 (Hidden Dependency)이라고 부른다.

의존성을 명시적으로 표현하자.

  • 의존성이 명시적이지 않으면 의존성을 파악하기 위해 내부 구현을 직접 살펴봐야 한다. 규모가 커질 수록 코드를 파악하기는 쉽지 않다.

  • 클래스를 다른 컨텍스트에서 재사용하기 위해 내부 구현을 수정해야 한다.

new는 해롭다

  • new 연산자를 사용하려면 구체 클래스의 이름을 직접 기술해야 하므로 추상화가 아닌 구체 클래스에 의존하게 되어 결합도가 높아진다.

예외 케이스

  • 하지만 가끔은 생성해도 무방하다. 클래스 안에서 객체의 인스턴스를 직접 생성하는 방식이 유용한 경우도 있다.

    • (기본값 설정) 주로 협력하는 기본 객체를 설정하고 싶은 경우가 여기에 속한다.

      • (체이닝) 생성자를 여러 개 선언해 한 쪽에서 다른 생성자를 호출할 수 있다. 또는 메서드를 오버로딩 할 수 도 있다. 이렇게 체이닝을 하면 간략한 생성자를 통해 협력하게 하면서 의존성을 교체할 수 있다.

  • 표준 클래스에 대한 의존은 해롭지 않다. 자바라면 JDK 표준 클래스가 이 부류에 속한다.

    • ArrayList의 코드가 수정될 확률은 0에 가깝다.

    • 클래스를 선언하더라도 가능한 추상적인 타입을 사용하는 것이 확장성 측면에서 유리하므로, 인터페이스인 List를 사용하는 것이 유리하다.

null 체킹 대신...

if (discountPolicy == null)과 같은 null 체킹 코드, 즉 예외 케이스를 다른 방식으로 바꿀 수 있다.

  • NoneDiscountPolicy를 만들어 할인 정책이 존재하지 않는다는 사실을 할인 정책의 한 종류로 간주하는 것이다.

컨텍스트 확장

정리하자면, 이렇게 설계를 유연하게 만들자.

  • (추상화) 추상화에 의존시키자. 인터페이스에 퍼블릭 인터페이스를 만들고 그것을 구현하는 구현 클래스를 만들자.

  • (명시적 의존성) 생성자를 통해 의존성을 명시적으로 드러내자. 그리고 new와 같이 구체 클래스를 다루는 책임을 외부로 옮기자.

이렇게 하면 추상화된 클래스에 자식 클래스를 추가함으로서 간단하게 컨텍스트를 확장할 수 있다.

Last updated