# 08장 의존성 관리하기

조영호 저 "오브젝트: 코드로 이해하는 객체지향 설계, 2019"를 읽고 정리한 내용입니다.

1 의존성 이해하기

의존성이란 의존하고 있는 대상의 변경에 영향을 받을 수 있는 가능성이다.

의존성 전이

1 객체에 2,3객체가 인스턴스로 들어 있을때, 4가 1을 사용하고 있다면, 4는 2,3에 대한 의존성을 전파 받는다. (직접의존과 간접의존)

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

런타임 의존성이 다루는 주제는 객체 사이의 의존인 반면, 코드 (컴파일) 관점에서 주인공은 클래스다.

시스템의 실행시점 구ㅋ조는 언어가 아닌 설계자가 만든 타입들 간의 관련성으로 만들어진다. 그러므로 객체와 타입간의 관계를 잘정의해야 좋은 실행 구조를 만들어낼 수 있다.

컨택스트 독립성

협력할 객체에 대해 자세히 알면 결합도가 높아진다. DiscountPolicy가 좋은 예.

가능한 자신이 실행된 컨택스트에 대한 구체적인 정보를 최소화.

의존성 해결하기

컴파일 타임 의ㅋ존성을 실행 컨택스트에 맞는 적절한 런타임 의존성으로 교체하는 것이 의존성 해결.

  • 3가지 방법

//1 생성자를 이용.
Movie avatar = new Movie("아바타",
                         Duration.ofMinutes(120)z
                         Money.wons(10000),
                         new AmountDiscountPolicy(...));

//2 setter 이용.
//setter 메서드를 이용하는 방식은 객체를 생성한 이후에도 의존하고 있는 대상을 변경할 수 있는 가능성열 열어놓고 싶을 때 유용하다. 단점, 객체의 상태 불완전. 
movie.setDiscountPolicy(new PercentDiscountPolicy(...));

//3 메드 인자를 이용. 
// Movie가 항상 할인 정책을 알 필요까지는 없고 가격을 계산할 때만 일시적으로 알아도 무방하다면 3 선택. 메서드 실행시 마다 의존관계 달라질시 유용.
public class Movie {
    public Money calculateMovieFee(Screening screening, DiscountPolicy discountPolicy) {
        return fee.minus(discountPolicy.calculateDiscountAmount(screening));
    }
}

가장 이상적인 방법은 생성자 방식(1)과 setter 방식(2)을 혼합하는 것이다. 항상 객체를 생성할 때 의존성을 해결해 서 완전한 상태의 객체를 생성한 후, 필요에 따라 setter 메서드를 이용해 의존 대상을 변경할 수 있게 할 수 있다. 이 방법은 시스템의 상태를 안정적으로 유지하면서도 유연성을 향상시킬 수 있기 때문에 의존성 해결을 위해 가장 선호되는 방법이다.

2 유연한 설계

loose coupling = weak coupling tight coupling = strong coupling

의존성과 결합도

일반적으로 의존성과 결합도를 동의어로 사용하지만 사실 두 용어는 서로 다른 관점에서 관계의 특성을 설명하는 용어다. 의존성은 두 요소 사이의 관계 유무를 설명한다. 따라서 의존성의 관점에서는 "의존성이 존재한다" 또는 "의존성이 존재하지 않는다"라고 표현해야 한다. 그에 반해 결합도는 두 요소 사이에 존재하는 의존성의 정도를 상대적으로 표현한다. 따라서 결합도의 관점에서는 "결합도가 강하다"또는 "결합도가 느슨하다"라고 표현한다.

바람직하지 못한 의존성이란 설계를 재사용하기 어렵게 만드는 의존성이다. 어떤 의존성이 재사용을 방해한다면 결합도가 강하다고 표현한 다. 어떤 의존성이 재사용을 쉽게 허용한다면 결합도가 느슨하다고 표현한다.

지식이 결합을 낳는다.

너무 많은 지식이 (세부정보) 결합도를 높이므로 추상화를 하자.

추상화에 의존해라

추상화란 어떤 양상, 세부사항, 구조를 좀 더 명확하게 이해하기 위해 특정 절차나 물체를 의도적으로 생략하거나 감춤으로써 복잡도를 극복하는 방법이다.

아래로 갈수록 클라이언트가 알아야되는 지식이 줄어든다 = 결합도가 느슨해진다.

  • concrete class dependency 구체클래스

  • abstract class dependency 추상클래스

  • interface dependency 인터페이스

추상클래스는 상속계층에 대해서 알아야 한다. 인터페이스는 상속계층을 몰라도 된다.

명시적인 의존성

추상클래스와 구체클래스 모두에게 의존성이 생기고, 재사용성이 불가하다.

앞서 얘기한 것처럼 의존성 해결 방법은, 인스턴스 변수의 타입은 추상 클래스나 인터페이스로 정의 하고 생성자, setter 메서드, 메서드 인자로 의존성을 해결할 때는 추상 클래스를 상속받거나 인터페이스를 실체화한 구체 클래스를 전달하는 것이다.

생성자, setter 메서드은 명시적으로 퍼블릭 인터페이스에 노출됨으로, 명시적인 의존성 explicit 이라고 부른다. 반대는 숨겨진 의존성 hidden dependency이다. 숨겨진 의존성을 수정하기 위해서는 내부를 다 찾아야 되고, 클래스를 다른 컨택스트에서 재사용하기 위해 내부 구현을 직접 변경해야 한다. 이로 인해 버그가 증가하기에 의존성은 명시적으로 표현해야 좋다.

의존성 감추는 것을 경계하자.

new는 해롭다

new 연산자를 사용하기 위해서는 구체 클래스의 이름을 직접 기술해야 한다. 따라서 new를 사용하는 클라이언트는 추상화가 아닌 구체 클래스에 의존할 수밖에 없기 때문에 결합도가 높아진다. 또한 new 연산자는 생성하려는 구체 클래스뿐만 아니라 어떤 인자를 이용해 클래스의 생성자를 호출해야 하는지도 알아야 한다. 따라서 new를 사용하면 클라이언트가 알아야 하는 지식의 양이 늘어나기 때문에 결합도가 높아진다.

new는 결합도를 높이기 때문에 해롭다. new는 여러분의 클래스를 구체 클래스에 결합시키는 것만으로 끝나지 않는다. 협력할 클래스의 인스턴스를 생성하기 위해 어떤 인자들이 필요하고 그 인자들을 어떤 순서로 사용해야 하는지에 대한 정보도 노출시킬뿐만 아니라 인자로 사용되는 구체 클래스에 대한 의존성을 추가한다.

가끔은 생성해도 무방하다

Movie가 대부분의 경우에는 AmountDiscountPolicy 의 인스턴스와 협력하고 가끔씩만 PercentDiscountPolicy의 인스턴스와 협력한다고 가정해보자. 이런 상황에서 모든 경우에 인스턴스를 생성하는 책임을 클라이언트로 옮긴다면 클라이언트들 사이에 중복 코드가 늘어나고 Movie의 사용성도 나빠질 것이다.

생성자를 체이닝해서 해결보자.

위 방법으로 결정할 수 있지만, 가급적 구체 클래스에 대한 의존성을 제거할 수 있는 방법을 찾아 봐야한다. 9장의 팩토리가 바로 그런 경우이다.

표준 클래스에 대한 의존은 해롭지 않다.

자바 JDK의 클래스들은 변경될 확률이 거의 없다. 이런 클래스들에 대해서는 구체 클래스에 의존하거나 직접 인스턴스를 생성하더라도 문제가 없다.

ArrayList의 코드가 수정될 확률은 0에 가깝기 때문에 인스턴스를 직접 생성하더라도 문제가 되지 않는다.

컨택스트 확장하기

할인정책이 없을때 잘못된 코드 예시.

내부를 직접 수정해 예외케이스 추가하는 것은 좋지 않기에, 아래 처럼 NoneDiscountPolicy 추가.

다른예, 중복 적용이 가능한 할인 정책을 구현.

위 잘못된 예처럼 예외 케이스를 추가하지 말고, OverlappedDiscountPolicy 만들어서 해결.

설계를 유연하게 만들 수 있었던 이유는 Movie가 DiscountPolicy라는 추상화에 의존하고, 생성자를 통해 DiscountPolicy에 대한 의존성을 명시적으로 드러냈으며, new와 같이 구체클래스를 직접적으로 다뤄야 하는 책임을 Movie 외부로 옮겼기 때문이다.

조합 가능한 행동

이 코드를 읽는 것만으로도 쉽게 이해할 수 있다. 그리고 인자를 변경하는 것만으로도 새로운 할인 정책과 할인 조건을 적용할 수 있다는 것 역시 알 수 있을 것이다.

Last updated