08 의존성 관리하기

01 의존성 이해하기

 

변경과 의존성

 

public class PeriodCondition {
    private DayOfWeek dayOfWeek;
    private LocalTime startTime;
    private LocalTime endTime;

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

 

PeriodCondition 클래스의 isSatisfiedBy 메서드는 Screening 인스턴스에게 getStartTime 메시지를 전송한다.

이처럼 어떤 객체가 예정된 작업을 정상적으로 수행하기 위해 다른 객체를 필요로 하는 경우 두 객체 사이에 의존성이 존재한다고 말한다.

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

 

PeriodCondition은 그림과 같이 Screening에 의존하며

 

 

Screening이 변경 될 경우 PeriodCondition은 영향을 받게 된다.

 

PeriodCondition 코드를 다시 살펴 보면 PeriodCondition은 DayOfWeek, LocalTime, Screening, DiscountCondition에 대해서도 의존성을 가진다. 또한 PeriodCondition 관점에서 의존성의 대상이 가지는 특성이 약간씩은 다르다는 사실을 알 수 있다.

 

DiscountCondition은 해당 인터페이스에 정의된 오퍼레이션을 퍼블릭 인터페이스의 일부로 포함시키며,

DayOfWeek, LocalTime은 인스턴스 변수로, Screening은 메서드 인자로 사용된다.

 

 

비록 의존성을 다른 방식으로 표기했지만 의존성이 가지는 근복적인 특성은 동일하다. 

-> PeriodCondition은 자신이 의존하는 대상이 변경될 때 함께 변경될 수 있다는 것이다.

 


의존성 전이

 

public class Screening {
    private Movie movie;
    private int sequence;
    private LocalDateTime whenScreened;
    
    public Reservation reserve(Customer customer, int audienceCount) {
    	return new Reservation(customer, this, calculateFee(audienceCount), audienceCount);
    }
    
    ...
}

 

Screening은 Movie, LocalDateTime, Customer에 의존한다는 사실을 알 수 있다. PeriodCondition은 Screening을 의존한다.

따라서 PeriodCondition은 Sceening이 의존하는 대상에 대해서도 간접적으로 의존하게 된다.

이를 의존성 전이(transitive dependency)라고 한다.

 

 

모든 경우에 의존성이 전이 되는 것은 아니다. 의존성이 실제로 전이 될지 여부는 변경의 영향과 캡슐화의 정도에 따라 달라진다.

 

예를 들어 Screening이 Movie를 의존, PeriodCondition이 Screening을 의존한다고 했을 때 Screening내에 Movie와 협력하여 구현되어있는 퍼블릭인터페이스가 존재하고 해당 퍼블릭인터페이스에 PeriodCondition이 메세지를 보낸다고 가정하자. 만약 Movie가 변경된다면 Screening, PeriodCondition에 영향이 가지만 캡슐화가 잘 되어있는 퍼블릭인터페이스라면 PeriodCondition는 해당 퍼블릭인터페이스의 내부 구현을 몰라도 되기때문에 영향이 가지 않는다는 것을 알 수 있다.

 

그러므로  의존성의 종류를

직접 의존성(direct dependency)간접 의존성(indirect dependency)으로 나누기도 한다.

 


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

 

런타임은 말 그대로 애플리케이션이 실행되는 시점을 가리킨다.

컴파일타임은 작성된 코드를 컴파일하는 시점을 가리키기도하지만 문맥에 따라서는 코드 구조 그 자체를 가리키기도 한다.

(동적 언어타입의 경우에는 컴파일 타임이 존재하지 않기 때문에)

 

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

 

 

컴파일 타임 의존성에서 Movie 클래스는 AmountDiscountPolicy 클래스와 PercentDiscountPolicy 클래스로 향하는 어떤 의존성도 존재하지 않는다.

 

하지만 런타임 의존성을 살펴보면 금액 할인 정책을 적용하기 위해서는 AmountDiscountPolicy의인스턴스와, 비율 할인 정책을 적용하기 위해서는 PercentDiscountPolicy 인스턴스와 협력한다.

 

만약 Movie 클래스가 DiscountPolicy 추상 클래스를 의존하지 않는다면 컴파일 타임 의존성에서 AmountDiscountPolicy 클래스와 PercentDiscountPolicy클래스 둘 모두를 의존하게 될것이다. 이는 새로운 할인 정책이 추가하기 어려워지며 전체적인 결합도가 높아짐을 의미한다.

 

그러나 현재 코드는 DiscountPolicy 추상 클래스를 통해 할인 정책을 구현한 두 클래스의 존재를 모르지만 실행 시점의 Movie객체는 두 클래스의 인스턴스와 협력할 수 있게 된다.

 

따라서 컴파일타임 구조와 런타임 구조 사이의 거리가 멀면 멀수록 설계가 유연해지고 재사용 가능해진다.

 


컨텐스트 독립성

 

Movie가 추상적인 DiscountPolicy가 아닌 AmountDiscountPolicy이나 PercentDiscountPolicy를 의존하여 사용하는 특정한 문맥에 강하게 결합될수록다른 문맥에서 사용하기는 더 어려워진다.클래스가 사용 될 특정한 문맥에 대해 최소한의 가정만으로 이뤄져 있다면 다른 문맥에서 재사용하기가 더 수월해진다. 이를 컨텍스트독립성이라고 부른다.

 

실행될 컨텍스트에 대한 구체적인 정보를 최대한 적게 알면 더 다양한 컨텍스트에서 재사용될 수 있으며 결과적으로 설계는 더 유연해지고 변경에 탄력적으로 대응할 수 있게 된다.

 


의존성 해결하기

 

컴파일타임 의존성을 실행 컨테스트에 맞는 적절한 런타임 의존성으로 교체하는 것을 의존성해결이라고 부른다.

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

 

  • 객체를 생성하는 시점에 생성자를 통해 의존성 해결
  • 객체 생성 후 setter 메서드를 통해 의존성 해결
  • 메서드 실행시 인자를 이용해 의존성 해결

 

 

 

02 유연한 설계

 

의존성을 관리하는데 유용한 몇 가지 원칙과 기법이 존재한다.

 


의존성과 결합도

 

앞서 예제로 봐왔듯이 Movie가 DicountPolicy가 아닌 어떤 구체적인 클래스를 의존하면 그 의존성이 다양한 환경에서 클래스를 재사용할 수 없도록 제한하기 때문에 그 의존성은 바람직하지 못한 것이다. 이는 결합도가 강하다고 표현한다.

반대로 Movie가 추상적인 DiscountPolicy를 의존하게되면 AmountDiscountPolicy와 PercentDisountPolicy와 모두 협력이 가능하므로 이는 결합도가 느슨하다고 표현한다.

 


자식이 결합을 낳는다

 

서로에 대해 알고있는 지식의 양이 결합도를 결정한다.

Movie가 추상적인 DiscountPolicy의 의존하는 것보다 구체적인 PercentDiscountPolicy에 의존한다면 Movie가 알아야하는 지식이 양이 더 많아 지기 때문에 결합도가 높아진다. 결합도를 느슨하게 유지하려면 협력하는 대상에 대해서 더 적게 알아야 하므로 필요한 정보 외에는 최대한 감추는 것이 중요하다.

 


추상화에 의존하라

 

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

 

추상화와 결합도의 관점에서 의존 대상을 다음과 같이 구분하는 것이 유용하다.

(목록에서 아래쪽으로 갈수록 클라이언트가 알아야 하는 지식의 양이 적어져 결합도가 느슨해진다.)

 

  • 구체 클래스 의존성(concrete class dependency)
  • 추상 클래스 의존성(abstract class dependency)
  • 인터페이스 의존성(interface dependency)

추상클래스를 사용하면 구체클래스에 비해 자식 종류에 대한 지식을 클라이언트에게 숨길 수 있다.

하지만 추상클래스의 클라이언트는 여전히 협력하는 대상이 속한 클래스 상속 계층이 무엇인지에 대해 알고 있어야 한다.

 

인터페이스에 의존하면 상속 계층을 모르더라도 협력이 가능해진다.

(협력하는 객체가 어떤 메시지를 수신할 수 있는지에 대한 지식만 남기기 때문)

 

즉 실행 컨텍스트에 대해 알아야 하는 정보를 줄일수록, 의존하는 대상이 더 추상적일수록 결합도는 낮아진다.

 


명시적인 의존성

 

pulbic class Movie {
 ...
 
    private DiscountPolicy discountPolicy;
    
    public Movie(String title, Duration runningTime, Money fee) {
    
    	...
        this.discountPolicy = new AmountDiscountPolicy(...);
    }
}

 

생성자내에 new 연산자를 통해 Movie가 AmountDiscountPolicy를 의존하고있다.

이를 앞서 설명했던것처럼 생성자 인자로 의존성을 주입하게끔 변경할 수 있다.

 

pulbic class Movie {
 ...
 
    private DiscountPolicy discountPolicy;
    
    public Movie(String title, Duration runningTime, Money fee, DiscountPolicy discountPoliicy) {
    
    	...
        this.discountPolicy = discountPoliicy;
    }
}

 

의존성을 명시적으로 퍼블릭 인터페이스에 노출시키게 됨으로 이를 명시적인 의존성(explicity dependency)이라고 부른다.

반면 Movie 생성자 내부에 new 연산자를 통해 AmountDiscountPolicy를 직접 인스턴스화하는 방식은 Movie가 DiscountPolicy에 의존한다는 사실을 감춘다. 의존성이 퍼블릭인터페이스에 노출되지 않으므로 숨겨진 의존성(hidden depedency)이라고 부른다.

 

의존성이 명지적이지 않으면 의존성을 파악하기 위해 내부 구현을 직접 살펴볼 수 밖에 없다. 또한 다른 컨텍스트에서 재사용하기 위해 내부 구현을 직접 변경해야 하는 문제도 생긴다.

 

숨겨져 있는 의존성을 밝은 곳으로 드러내서 널리 알려라. 그러면 설계가 유연하고 재사용 가능해질 것이다.

 


new는 해롭다

 

public class Movie {
    ...
    private DiscountPolicy discountPolicy;
    
    public Movie(String title, Duration duration, Money fee) {
        ...
        this.discountPolicy = new AmountDiscountPolicy(Money.wons(800), 
                                                       new SequenceCondition(1), 
                                                       new SequenceCondition(10), 
                                                       new PeriodCondition(DayOfWeek.MONDAY, 
                                                       LocalTime.of(10, 0), LocalTime.of(11, 59)), 
                                                       new PeriodCondition(DayOfWeek.THURSDAY, 
                                                       LocalTime.of(10, 0), LocalTime.of(20, 59))));
    }
}

 

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

그러므로 생성과 사용책임을 분리를 함으로써 new 연산자를 통해 생성하는 부분을 클라이언트에게 위임한다면 설계가 유연해질 것이다.

다음과 같이 말이다.

 

Movie movie = new Movie("아바타",
  Duration.ofMinutes(120),
  Money.wons(10000),
  new AmountDiscountPolicy(
  	Money.wons(800),
  	new SequenceCondition(1), 
  	new SequenceCondition(10), 
  	new PeriodCondition(DayOfWeek.MONDAY, LocalTime.of(10, 0), LocalTime.of(11, 59)), 
  	new PeriodCondition(DayOfWeek.THURSDAY, LocalTime.of(10, 0), LocalTime.of(20, 59))
  )
);

 


가끔은 생성해도 무방하다

 

주로 협력하는 기본 객체가 있다 하더라도 인스턴스 생성 책임을 모두 클라이언트에게 옮긴다면 중복코드가 늘어날 수 도 있다.

이런 경우엔 클래스 안에서 객체의 인스턴스를 직접 생성하는 방식이 유용할 수 있다.

 

private DiscountPolicy discountPolicy;

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

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

 

대신 체이닝 방식을 사용하여 AmountDiscountPolicy가 아닌 다른 타입으로도 교체하여 사용될 수 있도록 구현하면 된다.

 


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

 

의존성이 불편한 이유는 변경에 대한 영향이 존재하기 때문이지만 ArrayList 같은 클래스는 불변에 가깝기 때문에 문제가 되지 않는다.

추가로

 

List<String> list = new ArrayList<>();

 

와 같이 List 인터페이스 타입으로 인스턴스화 할시 ArrayList 뿐만이 아닌 List를 상속받아 구현한 다양한 타입으로 교체될 수 있어 확작성 측면에서 유리하다.

 


컨텍스트 확장하기

 

  • 할인 혜택을 제공하지 않는 영화가 존재
  • 다수의 할인 정책을 중복해서 적용하는 영화가 존재

기존에 개발된 내용에서 위와 같은 추가적인 요구사항이 발생했을 때 앞서 설명한 내용을 토대로 Movie를 기존 코드 변경이 아닌 확장을 통해서 요구사항을 만족시킬 수 있다.

 


할인 혜택을 제공하지 않는 영화가 존재한다면

 

할인금액 0원을 반환하는 퍼블릭인터페이스를 가진 새로운 클래스 타입을 정의하면 된다.

 

public class NoneDiscountPolicy extends DiscountPolicy {
    @Override
    protected Money getDiscountAmount(Screenning screening) {
        return Money.ZERO;
    }
}

 


다수의 할인 정책을 중복해서 적용하는 영화가 존재한다면

 

다수의 할인 정책을 가진 List타입 인스턴스 변수를 가진 클래스 타입을 정의하면 된다.

 

public class OverlappedDiscountPolicy extends DiscountPolicy {
    private List<DiscountPolicy> discountPolicied = new ArrayList<>();
    
    public OverlappedDiscountPolicy(DiscountPolicy ...discountPolicies) {
        this.discountPolicies = Arrays.asList(discountPolicies);
    }
    
    @Override
    protected Money getDiscountAmount(Screening screening) {
        ...
    }
}

 


조합 가능한 행동

 

훌륭한 객체지향 설계란 객체가 어떻게(how) 하는지를 표현하는 것이 아니라 객체들의 조합을 선언적으로 표현함으로써 객체들이 무엇(what)을 하는지를 표현하는 설계다. 

 

다음 코드와 같이 말이다.

 

Movie movie = new Movie("아바타",
  Duration.ofMinutes(120),
  Money.wons(10000),
  new AmountDiscountPolicy(
  	Money.wons(800),
  	new SequenceCondition(1), 
  	new SequenceCondition(10), 
  	new PeriodCondition(DayOfWeek.MONDAY, LocalTime.of(10, 0), LocalTime.of(12, 0)), 
  	new PeriodCondition(DayOfWeek.THURSDAY, LocalTime.of(10, 0), LocalTime.of(21, 0))
  )
);

 

코드를 쭉 읽는 것만으로도 첫 번째 상영, 10번째 상영, 월요일 10시 ~ 12시 사이, 목요일 10시 ~ 21시 사이 상영의 경우 800원을 할인해 준다는 사실을 쉽게 이해할 수 있다.

'📚 Book > Object' 카테고리의 다른 글

10 상속과 코드 재사용  (0) 2020.03.22
09 유연한 설계  (1) 2020.03.14
07 객체 분해  (0) 2020.02.29
06 메세지와 인터페이스  (0) 2020.02.19
05 책임 할당하기  (1) 2020.02.16