[오브젝트] 의존성 관리하기
1) 의존성 관리하기란?
- 잘 설계된 객체지향 애플리케이션은 작고 응집도 높은 객체들로 구성된다.
작고 응집도 높은 객체란 책임의 초점이 명확하고 한 가지 일만 잘 하는 객체를 의미한다.
이런 작은 객체들이 단독으로 수행할 수 있는 작업은 거의 없기 때문에
일반적인 애플리케이션의 기능을 구현하기 위해서는
다른 객체에게 도움을 요청해야 한다.
이런 요청이 객체 사이의 협력을 낳는다.
- 협력을 필수적이지만 과도한 협력은 설계를 곤경에 빠뜨릴 수 있다.
협력은 객체가 다른 객체에 대해 알 것을 강요한다.
다른 객체와 협력하기 위해서는 그런 객체가 존재한다는 사실을 알고 있어야 한다.
객체가 수신할 수 있는 메시지에 대해서도 알고 있어야 한다.
- 이런 관점에서 객체지향 설계란 의존성을 관리하는 것이고,
객체가 변화를 받아들일 수 있게 의존성을 정리하는 기술이라고 할 수 있다.
2) 의존성 이해하기
- 어떤 객체가 협력하기 위해 다른 객체를 필요로 할 때,
두 객체 사이에 의존성이 존재하게 된다.
의존성은 실행 시점과 구현 시점에 서로 다른 의미를 가진다.
(1) 실행 시점: 의존하는 객체가 정상적으로 동작하기 위해서는 실행 시에 의존 대상 객체가 반드시 존재해야 한다
(2) 구현 시점: 의존 대상 객체가 변경될 경우 의존하는 객체도 함께 변경된다.
- 언제나 그렇듯이 의존성의 개념을 이해할 수 있는 가장 좋은 방법은
구체적인 코드를 살펴보는 것이다.
여기서는 영화 예매 시스템의 PeriodCondition 클래스를 이용해 의존성의 개념을 설명하겠다.
- PeriodCondition 클래스의 isSatisfiedBy 메서드는 Screening 인스턴스에게 getStartTime 메시지를 전송한다.
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;
}
}
- 실행 시점에 PeriodCondition의 인스턴스가 정상적으로 동작하기 위해서는 Screening의 인스턴스가 존재해야 한다.
만약 Screening의 인스턴스가 존재하지 않거나 getStartTime 메시지를 이해할 수 없다면
PeriodCondition의 isSatisfiedBy 메서드는 예상했던대로 동작하지 않을 수 있다.
- 이처럼 어떤 객체가 예정된 작업을 정상적으로 수행하기 위해 다른 객체를 필요로 하는 경우
두 객체 사이에 의존성이 존재한다고 말한다.
의존성은 방향성을 가지며 항상 단방향이다.
Screening이 변경될 때 PeriodCondition은 영향을 받게 되지만, 그 역은 성립하지 않는다.
이 경우 PeriodCondition은 Screening에 의존한다.
3) 의존성 전이
- 의존성은 전이될 수 있다.
Screening의 코드를 살펴보면 Screening이 Movie, LocalDateTime, Customer에 의존한다는 사실을 알 수 있다.
- 의존성 전이가 의미하는 것은 PeriodCondition이 Screening에 의존할 경우,
PeriodCondition은 Screening이 의존하는 대상에 대해서도 자동적으로 의존하게 된다는 것이다.
다시 말해서 Screening이 가지고 있는 의존성이 Screening에 의존하고 있는 PeriodCondition으로도 전파된다.
따라서 Screening이 Movie, LocalDateTime, Customer에 의존하기 때문에
PeriodCondition 역시 간접적으로 Movie, LocalDateTime, Customer에 의존하게 된다.
- 의존성은 함께 변경될 수 있는 가능성을 의미하기 때문에
모든 경우에 의존성이 전이되는 것은 아니다.
의존성이 실제로 전이될지 여부는 변경의 방향과 캡슐화의 정도에 따라 달라진다.
- Screening이 의존하고 있는 어떤 요소의 구현이나 인터페이스가 변경되는 경우에
Screening이 내부 구현을 효과적으로 캡슐화하고 있다면
Screening에 의존하고 있는 PeriodCondition까지는 변경이 전파되지 않을 것이다.
의존성 전이는 변경에 의해 영향이 널리 전파될 수 있다는 경고일 뿐이다.
4) 런타임 의존성과 컴파일타임 의존성
- 의존성과 관련해서 다뤄야 하는 또 다른 주제는
런타임 의존성(run-time dependency)과 컴파일타임 의존성(compile-time dependency)의 차이다.
먼저 여기서 사용하는 런타임과 컴파일타임의 의미를 이해할 필요가 있다.
- 런타임은 간단하다. 말 그대로 애플리케이션이 실행되는 시점을 가리킨다.
컴파일타임은 약간 미묘하다.
일반적으로 컴파일타임이란 작성된 코드를 컴파일하는 시점을 가리키지만
문맥에 따라서는 코드 그 자체를 가리키기도 한다.
- 객체지향 애플리케이션에서 런타임의 주인공은 객체다.
따라서 런타임 의존성이 다루는 주제는 객체 사이의 의존성이다.
반면 코드 관점에서 주인공은 클래스다.
따라서 컴파일타임 의존성이 다루는 주제는 클래스 사이의 의존성이다.
- 여기서 중요한 것은 런타임 의존성과 컴파일타임 의존성이 다를 수 있다는 것이다.
사실 유연하고 재사용 가능한 코드를 설계하기 위해서는 두 종류의 의존성을 서로 다르게 만들어야 한다.
- 영화 예매 시스템을 예로 들어 살펴보자.
Movie는 가격을 계산하기 위해 비율 할인 정책과 금액 할인 정책 모두를 적용할 수 있게 설계해야 한다.
다시 말해서 Movie는 AmountDiscountPolicy와 PercenDiscountPolicy 모두와 협력할 수 있어야 한다.
- 이를 위해 AmountDiscountPolicy와 PercentDiscountPolicy가 추상 클래스인 DiscountPolicy를 상속받게 한 후
Movie가 이 추상 클래스에 의존하도록 클래스 관계를 설계했다.
- 여기서 중요한 것은 Movie 클래스에서 AmountDiscountPolicy 클래스와 PercentDiscountPolicy 클래스로 향하는
어떤 의존성도 존재하지 않는다는 것이다.
Movie 클래스는 오직 추상 클래스인 DiscountPolicy 클래스에만 의존한다.
Movie 클래스의 코드를 살펴보면 AmountDiscountPolicy나 PercentDiscountPolicy에 대해서는 언급조차 하지 않는다.
public class Movie {
...
private DiscountPolicy discountPolicy;
public Movie(String title, Duration runningTime, Money fee, DiscountPolicy discountPolicy){
...
this.discountPolicy = discountPolicy;
}
public Money calculateMovieFee(Screening screening) {
return fee.minus(discountPolicy.calculateDiscountAmount(screening));
}
}
- 하지만 런타임 의존성을 살펴보면 상황이 완전히 달라진다.
금액 할인 정책을 적용하기 위해서는 AmountDiscountPolicy의 인스턴스와 협력해야 한다.
비율 할인 정책을 적용하기 위해서는 PercentDiscountPolicy의 인스턴스와 협력해야 한다.
- 코드를 작성하는 시점의 Movie 클래스는 AmountDiscountPolicy 클래스와 PercentDiscountPolicy 클래스의
존재에 대해 전혀 알지 못하지만
실행 시점의 Movie 인스턴스는 AmountDiscountPolicy 인스턴스 혹은 PercentDiscountPolicy 인스턴스와
협력할 수 있어야 한다.