의존성 관리하기 (chapter8)

잘 설계된 객체지향 애플리케이션 이란?

  • 작고 응집도 높은 객체들로 구성
  • 책임의 초점이 명확하고 한가지 일을 잘 수행
  • 이러한 객체들이 모여 협력을 함

협력을 위해서 의존성이 필요.
하지만 과도한 협력은 의존성 문제로 애플리케이션의 변경을 힘들게 함
이러한 관점에서 객체지향 설계란 의존성을 관리하고 의존성을 정리하는 기술이라고 할 수 있다


의존성 이해하기

변경과 의존성

객체는 협력을 위해 다른 객체를 필요로 하고 두 객체 사이에 의존성이 생기게 된다
의존성은 실행시점과 구현시점에 따라 서로 다른 의미를 갖는다

  • 실행시점
    • 의존하는 객체가 정상적으로 동작하기 위해서는 실행시 의존 객체가 존재해야함
  • 구현 시점
    • 의존 객체가 변경될 경우 의존하는 객체도 함께 변경
public class PeriodCondition implements DiscountCondition {
    private DayOfWeek dayOfWeek;
    private LocalTime startTime;
    private LocalTime endTime;

    ...
    public boolean isSatisfiedBy(Screening screening) {
        return screening.getStartTime().getDayOfWeek().equals(dayOfWeek) &&
        startTime.compareTo(screening,getStartTime().toLocalTime()) <= 0 &&
        endTime.compareTo(screening.getStartTime().toLocalTime()) >= 0;
    }
}

위의 코드를 해당 시점에 경우로 나누어 살펴보자

  • 실행시점
    • isSatisfiedBy() 실행시 Screening 인스턴스가 존재해야 한다. 그렇지 않다면 해당 내부의 로직 수행중에 에러가 날 것이다
  • 구현시점
    • isSatisfiedBy() 내부는 DayOfWeek, Screening, LocalTime, DiscountCondition에 영향을 받는다.
    • 따라서 해당 클래스나 변수가 변경이 생기게 된다면 PeriodCondition에게 당연히 영향을 끼친다

따라서, 두 요소 사이의 의존성은 의존되는 요소가 변경 될때 의존하는 요소도 함께 변경될 수 있음을 의미한다

의존성 전이

PeriodCondition -> Screening (직접 의존)
Screening -> Movie (직접 의존)

PeriodCondition ---> Movie (간접 의존)

Screeing의 내부 코드는 Movie, LocalDateTime, Customer에 의존하고 있다.
위의 그림을 잠깐 살펴보자.

[직접의존] - PeriodCondition은 Screening에, Screeing은 Movie에 의존하고 있다.
이렇듯 객체가 다른 객체에 의존하고 있을 경우 ‘직접 의존한다’라고 한다

[간접의존] - PeriodCondition은 Movie에 직접 의존하지 않고 있다.
코드상에 드러나지는 않지만 PeriodCondition->Screening->Moive로 이어지는 연결고리가 잠재적으로 영향을 미칠 수 있다.
이렇듯, 직접 연관되지 않지만 연관된 객체에 의존된 관계가 맺어질 경우 ‘간접 의존한다’ 라고 한다

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

의존성과 관련해서 또 다뤄야 할 부분은 런타임 의존성과 컴파일 의존성이다

  • 런타임 의존성
    • 애플리케이션이 실행되는 시점에 의존성
  • 컴파일 의존성
    • 컴파일 시점에 작성된 코드에 따라 문맥에 의존

아래의 예제를 살펴보자

public class Movie {
    ...
    private DiscountPolicy discountPolicy;

    public Movie(String title, Duration runningTime, DiscountPolicy discountPolicy) {
    ...
        this.discountPolicy = discountPolicy;
    }
}

코드상에서 Movie는 DiscountPolicy에 의존하고 있다.
어느 부분에도 AmountDiscountPolicy, PercentDiscountPolicy이 명시되어 있지 않다.
이렇듯, 코드상에는 구체화된 할인정책이 명시되어 있지 않지만 런타임 의존성을 이용해서 의존 관계를 가질 수 있다.

클래스가 어떠한 인스턴스 타입과 협력하는지 코드상에서 알 수 없게 되면
코드의 재사용이 가능해지고 다양한 협력관계를 가질 수 있는 장점이 있다.

이렇듯 런타임 의존성과 컴파일 의존성은 서로 다를 수 있다. 이러한 다름 때문에 프로그래머는 유연하고 재사용 가능한 코드를 설계할 수 가 있다

컨텍스트 독립성

클래스가 구체적인 클래스를 알게 될수록, 그 클래스가 사용하는 문맥에 강하게 결합된다
따라서, 클래스가 사용될 특정한 문맥에 대해 최소한의 가정만으로 이뤄진다면
다른 문맥에서 재사용하기가 더 수월해진다. 이를 컨텍스트 독립성이라고 부른다

결국 의존하는 관계에서는 실행될 컨텍스트에 대한 구체적 정보를 최대한 적게 알도록 설계해야 한다

의존성 해결하기

결국 컴파일 타임 의존성이 구체적인 런타임 의존성으로 대체되어야 재사용 가능한 코드가 작성될 수 있다.
이처럼 컴파일타임 의존성을 실행 컨텍스에 맞는 런타임 의존성으로 교체하는 것을 의존성 해결이라고 부른다

의존성을 해결하기 위해서는 다음과 같은 세 가지 방법을 사용한다

  1. 객체를 생성하는 시점에 생성자를 통해 의존성 해결
    Movie avatar = new Movie("아바타", Duration.ofMinutes(120), Money.wons(10000), new AmountDiscountPolicy(...));
    생성자를 통해 주입하여 준다
  1. 객체를 생성후 setter 메서드를 통해 의존성 해결
    Movie avatar = new Movie(...);
    avatar.calculateFee(...); // 예외발생
    avatar.setDiscountPolicy(new AmountDiscountPolicy(...));
  • set method를 구현해주어야 한다
  • set을 하기 전까지 객체가 불안정하여 NullPointerException이 발생할 수 있다
  1. 메서드 실행 시 인자를 이용해 의존성 해결
    public class Movie {
     public Money calculateMovieFee(Screening screening, DiscountPolicy discountPolicy) {
         return fee.minus(discountPolicy.calculateDiscountAmount(screening));
     }
    }

가장 좋은 방법은 생성자와 수정자를 같이 이용하는 것이다. 시스템의 안전성을 보장해주고 필요에 따라 의존성을 변경할 수 있기 때문에 유연성을 향상시킬 수 있다


유연한 설계

설계를 유연하고 재사용 가능하게 만들기로 결정했다면 의존성을 관리하는데 유용한 몇가지 원칙을 익힐 필요가 있다. 먼저 의존성과 결합도의 관계를 살표보자

의존성과 결합도

객체지향 패러다임의 근간은 협력이다.

객체들은 협력을 위해 서로의 존재와 책임을 알아야 한다. 이러한 지식들이 객체 사이의 의존성을 낳는다.
의존성은 객체들의 협력을 가능케 하는 매개체이고 바람직한 것이다. 다만, 과하면 문제가 될 수 있다.

다음 코드를 살펴보자

Movie는 PercentDiscountPolicy에 의존한다. 의존한다는 사실 자체는 바람직한 관계이다.
다만, Movie가 PercentDiscountPolicy라는 구체적 클래스에 의존하기 때문에 다른 할인 정책을 문맥에서 사용하기 힘들어졌다.

그렇다면 바람직한 의존성이란 무엇인가? 바람직한 의존성은 재사용과 관련이 있다.
의존성이 다양한 환경에서 클래스를 재사용할 수 없도록 제한한다면 그 의존성은 바람직하지 못한 것이다.

이러한 의존성을 세련된 단어로 표현한 것이 결합도이다.
두 요소가 의존성이 바람직할때를 느스한 결합도, 약한 결합도라 한다.
반대로, 두 요소 사이의 의존성이 바람직하지 못할때 단단한 결합도 또는 강한 결합도라고 표현한다

지식이 결합을 낳는다

더 구체화된 지식일 수록 강한 결합으로 연결된다.
Movie -> PercentDiscountPolicy (강결합)
Movie -> DiscountPolicy (느슨한 결합)

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

이러한 목적을 달성하기 위해 가장 효과적인 방법이 추상화이다

추상화에 의존하라

추상화란 어특정 절차나 물체를 의도적으로 생략하거나 감춤으로써 복잡도를 극복하는 방법이다.
추상화를 이용하면 불필요한 정보를 감추고 지식의 양을 줄일 수 있다

PercentDiscountPolicy보다 DiscountPolicy가 지식의 양이 더 적다.
Movie와 DiscountPolicy 사이의 결합도가 더 느스한 이유는 Movie가 구체적인 대상이 아닌 추상화에 의존하기 때문이다

  • 구체 클래스
  • 추상 클래스
  • 인터페이스

클래스는 3개의 타입에 대하여 의존할 수 있다. 당연히 인터페이스와 의존 관계를 맺는 것이 느스한 결합을 만들어 준다

명시적인 의존성

아래의 코드를 살펴보며 의존성에 대한 문제를 찾아보자

public class Movie {
    ...
    private DiscountPolicy discountPolicy;

    public Movie(String title, Duration runningTime, DiscountPolicy discountPolicy) {
    ...
        this.discountPolicy = new AmountDiscountPolicy(...);
    }
}

New AmountDiscountPolicy가 결합도를 높이고 있다.

public class Movie {
    ...
    private DiscountPolicy discountPolicy;

    public Movie(String title, Duration runningTime, DiscountPolicy discountPolicy) {
    ...
        this.discountPolicy = discountPolicy;
    }
}

명시적인 부분을 제거하여 결합도를 낮추어 주자. 이제 객체를 생성할 때 생성자의 인자로 DiscountPolicy의 어떠한 자식 클래스도 전달할 수 있다.
따라서, 런타임에 해당 인스턴스를 선택적으로 전달할 수 있게 되었다.

이처럼 의존하는 사실을 Public 인터페이스에 드러내는 것을 명시적인 의존성이라 한다.
반면, 내부에 AmountDiscountPolicy를 직접 생성하는 방식을 의존하는 사실은 감춘다고 하여 숨겨진 의존성이라 한다.

숨겨진 의존성의 경우 직접 코드 내부를 살펴보아야 하고, 인스턴스를 생성하는 코드를 훓어 파악해야 하므로 버그의 가능성도 내포하고 있다.
따라서, 우리는 재사용성을 높이고 안정성을 높이는 명시적인 의존성을 선택해야 한다.

new는 해롭다

클래스는 인스턴스를 생성할 수 있게 new 연산자를 제공한다. 하지만 잘못 사용한다면 두 객체 사이의 결합도가 높아질 수 있다.

  1. New 연산자로 구체 클래스 이름을 직접 기술한다.
  2. 구체 클래스 뿐만 아니라, 어떠한 인자를 이용해 클래스를 생성할지 인자 정보도 알고 있어야 한다
public class Movie {
    ...
    private DiscountPolicy discountPolicy;

    public Movie(String title, Duration runningTime, DiscountPolicy discountPolicy) {
    ...
        this.discountPolicy = discountPolicy;
    }
}

두개를 비교해보면 구체 클래스를 생성하는 것이 많은 고통을 유반한다는 것을 볼 수 있다

가끔은 생성해도 무방하다

new가 꼭 나쁜것만은 아니다. 생성되는 비율이 다를 경우 아래와 같이 여러 생성자를 이용하여 구현할 수 있다.
해당 생성자는 아래의 생성자를 호출하는 방식이다. 이런 방식으로 구현함으로써 유연성도 가져갈 수있따.

### 컨텍스트 확장하기

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

의존성이 문제가 되었던 것은 변경 가능성으로 인해 유발되는 파생효과들이다.
다만, 표준 클래스처럼 변화가 거의 없는 것에 해당한다면 크게 문제될 것이 없다.


참조

  • 오브젝트, 코드로 이해하는 객체지향 설계, chapter8 의존성 관리하기 [조영호]

'Books > Object' 카테고리의 다른 글

[Object] 상속과 코드 재사용  (1) 2021.08.03
[Object] 유연한 설계  (0) 2021.06.28
[Object] 의존성 관리하기  (0) 2021.06.10
[Object] 객체 분해  (0) 2021.05.29
[Object] 메시지와 인터페이스  (0) 2021.05.25
[Object] 책임 할당하기  (0) 2021.05.16
블로그 이미지

yhmane

댓글을 달아 주세요