8장: 의존성 관리하기

의존성

  • 어떤 객체가 협력하기 위해 다른 객체를 필요로 할 때 두 객체 사이에 의존성이 존재한다.

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

  • 의존성이 있다는 것은 변경이 발생했을 때 전파 가능성이 있음을 암시한다.

  • 의존성은 전이될 수 있다. 직접적으로 의존 관계가 없는 객체에 변경이 발생하더라도, 간접적인 의존 관계에 의해 영향이 전파될 수 있다.

  • 런타임 의존성과 컴파일 타임(코드 작성 시점) 의존성이 다를 수 있다. 유연하고 재사용 가능한 코드를 설계하기 위해서는 두 종류의 의존성을 서로 다르게 만들어야 한다.

  • 컴파일 타임에는 추상 클래스에 의존성을 갖도록 하고, 런타임에는 구현체의 인스턴스에 의존성을 갖도록 해야 한다. 즉, 실제로 협력할 객체가 어떤 것인지는 런타임에 결정해야 하며, 그렇지 않다면 다른 클래스의 인스턴스와 협력할 가능성 자체가 없어져 재사용이 어려워진다.

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

의존성 해결

  • 컴파일타임 의존성을 실행 컨텍스트에 맞는 적절한 런타임 의존성으로 교체하는 것을 의미한다.

  • 생성자, setter 메서드, 메서드 인자 등을 사용해 의존성 해결이 가능하다.

  • setter를 사용할 경우 생성되고 나서 setter가 호출되기 전까지는 객체가 NullPointerException이 발생할 수 있는 불안정한 상태이므로, 생성자와 setter 방식을 혼합하여 사용하는 것이 좋다.

  • 메서드 인자를 사용할 경우 협력 대상에 대해 지속적으로 의존 관계를 맺을 필요없이, 메서드가 실행되는 동안만 일시적으로 의존 관계가 존재해도 무방하거나, 메서드가 실행될 때마다 의존 대상이 매 번 달라져야 하는 경우에 유용하다.

의존성과 결합도

  • 의존성은 객체들의 협력을 가능하게 만드는 매개체라는 관점에서는 바람직한 것이지만 과하면 문제가 될 수 있다.

  • 구체적인 클래스를 의존하게 될 경우 다양한 환경에서 재사용할 수 없게 된다. 따라서 특정한 컨텍스트에 강하게 결합되지 않도록 만들어야 한다.

  • 의존성은 두 요소 사이의 관계 유무를 설명하며, 결합도는 두 요소 사이에 존재하는 의존성의 정도를 상대적으로 표현한다.

  • 결합도의 정도는 한 요소가 자신이 의존하고 있는 다른 요소에 대해 알고 있는 정보의 양으로 결정된다. 많은 정보를 알고 있을수록 두 요소는 강하게 결합된다.

  • 더 많이 알고 있다는 것은 더 적은 컨텍스트에서 재사용 가능하다는 것을 의미한다. 결합도를 느슨하게 만들기 위해서는 협력하는 대상에 대해 필요한 정보 외에 는 최대한 감추는 것이 중요하다.

추상화에 의존하기

  • 목록에서 아래로 갈수록 결합도가 느슨해진다. 내부 구현과 클래스의 종류를 감추기 때문에 클라이언트가 아는 정보가 줄어들기 때문이다.

    • 구체 클래스 의존성

    • 추상 클래스 의존성

    • 인터페이스 의존성

  • 결합도를 느슨하게 만들기 위해서는 클래스 내에서 다른 구체 클래스에 대한 모든 의존성을 제거해야만 한다.

  • 이를 위해 인스턴스 변수의 타입은 추상 클래스나 인터페이스로 정의하고, 해당 변수의 초기화는 생성자, setter 메서드, 메서드 인자로 구체 클래스를 전달받아 의존성을 해결하도록 한다.

  • 명시적인 의존성(explicit dependency)은 생성자의 인자로 타입을 입력받도록 하여 어떠한 퍼블릭 인터페이스에 의존하는지 드러내는 것이고, 숨겨진 의존성(hidden dependency)은 내부에서 인스턴스를 직접 생성하여 의존 관계를 감추는 것이다.

  • 의존성이 명시적이지 않으면 의존 관계 파악을 위해 내부 구현을 직접 살펴봐야 하며, 다른 컨텍스트에서 재사용하기 위해 내부 구현을 직접 변경해주어야 한다.

  • 의존성을 명시적으로 드러내면 내부 구현의 변경 없이 재사용이 가능하므로 코드 수정으로 인한 버그 발생 가능성을 없앨 수 있다.

  • 의존성을 해결하기 위해 new 연산자로 협력할 클래스의 인스턴스를 생성한다면, 어떤 인자들이 필요하고 그 인자들을 어떤 순서로 사용해야 하는지에 대한 정보를 노출시킬뿐만 아니라 인자로 사용되는 구체 클래스에 대한 의존성도 추가한다.

  • 따라서 객체를 생성하는 책임을 객체 내부가 아니라 클라이언트로 옮겨야 한다.

  • 다만 대부분의 클래스가 특정 클래스에 의존한다면 협력하는 기본 객체를 설정하는 것이 나을 수 있다. 이러한 경우를 위해 생성자를 오버로딩하여 기본 객체를 설정하는 생성자와 객체를 입력받는 생성자를 따로 둘 수 있다.

public class Movie {
  private DiscountPolicy discountPolicy;
	
  public Movie(String title, Duration runningTime, Money fee) {
    this(title, runningTime, fee, new AmountDiscountPolicy(...));
  }
  
  public Movie(String title, Duration runningTime, Money fee, DiscountPolicy discountPolicy) {
    // ...
    this.discountPolicy = discountPolicy;
  }
}
  • 구체 클래스에 의존하게 되더라도 클래스의 사용성이 더 중요하다면 결합도를 높이는 방향으로 코드를 작성할 수 있다.

표준 클래스에 대한 의존

  • JDK에 포함된 표준 클래스의 경우 변경될 확률이 거의 없기 때문에 구체 클래스에 의존하거나 직접 인스턴스를 생성하더라도 문제가 없다.

  • 예를 들어 ArrayList의 경우 수정될 확률이 거의 없기 때문에 인스턴스를 직접 생성하더라도 문제가 되지 않는다.

  • 다만 클래스를 직접 생성하더라도 타입은 추상적인 타입을 사용하는 것이 확장성 측면에서 유리하다.

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

컨텍스트 확장

  • 할인 정책 인터페이스를 필드로 가지는 영화 클래스에 할인을 하지 않는 정책, 여러 할인을 중첩해 적용하는 정책을 추가하는 상황에도, 영화 클래스에 변경을 가하지 않고 할인 정책 인터페이스를 구현함으로서 새로운 기능을 추가할 수 있다.

  • 유연하고 재사용 가능한 설계는 객체가 어떻게(how) 하는지를 장황하게 나열하지 않고도 객체들의 조합을 통해 무엇(what)을 하는지를 표현하는 클래스들로 구성된다.

  • 코드에 드러난 로직을 해석할 필요 없이 객체가 어떤 객체와 연결 됐는지를 보는 것만으로도 객체의 행동을 쉽게 예상하고 이해 할 수 있다.

  • 객체 구성을 관리할 목적으로 작성하는 코드를 객체 네트워크의 행위에 대한 선언적인 정의라고 한다.

Last updated