협력적이면서도 유연한 객체 생성을 위해, 의존성 관리 방법에 대해 알아보자.
PeriodCondition
public class PeriodCondition implements DiscountCondition {
private DayOfWeek dayOfWeek;
private LocalTime startTime;
private LocalTime endTime;
...
public boolean isSatisfiedBy(Screening screening) {
return screening.getStartTime().....
}
PeriodCondition
과 Screening
예시에서 PeriodCondition
은 Screening
을 주입받는다.
이 경우, PeriodCondition은 Screening을 의존한다
고 할 수 있다.
PeriodCondition
정상 동작을 위한 조건Screening
인스턴스의 존재Screening
이 getStartTime
메시지를 이해 할 수 있어야 한다.PeriodCondition
은 Screening
에 의존하고 있으며, 그 반대가 아니다.PeriodCondition
의 의존성 종류DayOfWeek
, LocalTime
Screening
DiscountCondition
의존성을 표기하는 다양한 방식이 존재하지만, 근본적인 특성은 동일하다.
PeriodCondition
은 자신이 의존하는 대상이 변경될 때 함께 변경될 수 있다.
UML과 의존성
- 실제로 UML에서 다루고 있는 의존성들은 아래와 같다.
- 이번장에서 다루는 의존성은 UML의 의존관계(dependency)가 아니라, 모든 관계가 가지는 공통적인 특성으로 이야기하고 있다.
- 의존성은 두 요소 사이에 변경에 의해 영향을 주고받는 힘의 역학관계가 존재한다는 사실에 초점을 맞춘다.
의존성은 전이될 수 있다.
PeriodCondition
은 Screening
에 의존한다. PeriodCondition
은 간접적으로 Screening
이 의존하는 Movie
의존하게 된다.Screening
이 의존하고 있는 요소의 구현이나 인터페이스의 변경에 대해 Screening
이 효과적으로 캡슐화 하고 있다면, PeriodCondition
에게 변경이 전파되지 않을 것이다.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));
}
}
코드를 작성하는 시점의 Movie
에서는 오직 추상 클래스인 DiscountPolicy
만 의존한다.
AmountDiscountPolicy
나 PercentDiscountPolicy
와 같은 구체적인 클래스에 대해서는 전혀 언급하지 않는다.
실행 시점의 Movie
는 AmountDiscountPolicy
나 PercentDiscountPolicy
객체와 협력할 수 있어야 한다.
예시에서,
Movie
인스턴스가 두 클래스의 인스턴스와 함께 협력할 수 있게 만드는 방법은, Movie
가 둘 중 어느것도 알지 못하게 하는 것이다.DiscountPolicy
에 의존한다.PercentDiscountPolicy
or AmountDiscountPolicy
로 대체된다.객체지향 프로그램의 실행 구조는 소스코드 구조와 일치하지 않는 경우가 종종 있다. 코드 구조는 컴파일 시점에 확정되는 것이고 이 구조에는 고정된 상속 클래스 관계들이 포함된다. 그러나 프로그램의 실행 시점 구조는 협력하는 객체에 따라 달라질 수 있다. 즉, 두 구조는 전혀 다른 별개의 독립성을 갖는다. 하나로부터 다른 하나를 이해하려는 것은 생태계의 동적인 성질을 식물과 동물과 같은 정적 분류 구조를 바탕으로 이해하려는 것과 똑같다. ... 컴파일 시점의 구조와 실행 시점 구조 사이에 차이가 있기 때문에 코드 자체가 시스템의 동작 방법을 모두 보여줄 수 없다. 시스템의 실행 시점 구조는 언어가 아닌 설계자가 만든 타입들 간의 관련성으로 만들어진다. 그러므로 객체와 타입 간의 관계를 잘 정의해야 좋은 실행 구조를 만들어낼 수 있다. [GOF94].
public class Movie {
private PercentDiscountPolicy percentDiscountPolicy; // 구체 클래스에 의존
}
Movie
가 비율 할인 정책이 적용된 영화의 요금 계산을 하는 문맥에서 사용될 것을 가정한다.public class Movie {
private DiscountPolicy discountPolicy; // 추상 타입에 의존
}
Movie
가 할인 정책에 따라 요금을 계산하지만, 구체적으로 어떤 정책을 따르지는 결정하지 않았음을 가정한다.시스템을 구성하는 객체가 컨텍스트 독립적이라면 해당 시스템은 변경하기 쉽다. 여기서 컨텍스트 독립적이라는 말은 각 객체가 해당 객체를 실행하는 시스템에 관해 아무것도 알지 못한다는 의미다. 이렇게 되면 행위의 단위(객체)를 가지고 새로운 상황에 적용할 수 있다. ... 컨텍스트 독립성을 따르면 다양한 컨택스트에 적용할 수 있는 응집력있는 객체를 만들 수 있고 객체 구성 방법을 재설정해서 변경 가능한 시스템으로 나아갈 수 있다.[Freeman09].
setter
메서드를 통해 의존성 해결// 클라이언트 코드; AmountDiscountPolicy를 사용하는 경우
Movie avatar = new Movie("아바타",
Duration.ofMinutes(120),
Money.wons(10000),
new AmountDiscountPolicy(...));
// 클라이언트 코드; PercentDiscountPolicy를 사용하는 경우
Movie avatar = new Movie("스타워즈",
Duration.ofMinutes(180),
Money.wons(10000),
new PercentDiscountPolicy(...));
// Movie의 생성자
// => DiscountPolicy를 받는 생성자를 선언한다.
public class Movie {
public Movie(
String title,
Duration runningTime,
Money fee,
DiscountPolicy discountPolicy
){
...
this.discountPolicy = discountPolciy;
}
Movie
인스턴스를 생성한 후 메서드를 이용해 의존성을 해결한다.Movie
에서 DiscountPolicy
를 설정할 수 있는 setter 메서드를 제공해야 한다.// 클라이언트 코드
Movie avatar = new Movie(...);
avatar.setDiscountPolicy(new AmountDisocuntPolciy(...));
// Movie의 setter메서드
public class Movie {
public void setDiscountPolicy(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
}
NullPointException
이 발생 가능하다.// 장점; 런타임 중 유연한 변경가능
Movie avatar = new Movie(...);
avatar.setDiscountPolicy(new AmountDiscountPolicy(...));
...
avatar.setDiscountPolicy(new PercentDiscountPolicy(...));
// 단점
Movie avatar = new Movie(...);
avatar.calcualteFee(...); // NullPointException 예외 발생
avatar.setDiscountPolciy(new AmountDisocuntPolicy(...));
Movie avatar = new Movie(..., new PercentDiscountPolicy(...));
...
avatar.setDiscountPolicy(new AmountDisocuntPolicy(...));
public class Movie {
public Money calcualteMovieFee(Screening screening, DisocuntPolicy discountPolicy) {
return fee.minus(discountPolicy.calcualteDiscountAmount(screening));
}
}
대부분의 경우 동일한 객체를 인자로 전달하고 있다면 생성자, setter를 이용하는 방식으로 변경하는 것이 좋다.
의존성이 존재한다
, 의존성이 존재하지 않는다
결합도가 강하다
, 결합도가 느슨하다
Movie
가 PercentDiscountPolicy
에 의존하는 경우Movie
는 비율 할인 정책에 따라 할인 요금을 계산할 것
이라는 사실을 알고있다.Movie
가 DiscountPolicy
에 의존하는 경우Movie
는 할인 요금을 계산할 것
이라는 사실만 알고있다.public class Movie {
...
private DiscountPolicy discountPolicy;
public Movie(String title, Duration runningTime, Money fee) {
...
this.discountPolicy = new AmountDiscountPolicy(...);
}
}
AmountDiscountPolicy
의 인스턴스를 직접 생성해서 대입하고 있다.public class Movie {
...
private DiscountPolicy discountPolicy;
public Movie(
String title,
Duration runningTime,
Money fee,
DiscountPolicy discountPolicy
) {
...
this.discountPolicy = discountPolicy;
}
}
DiscountPolicy
의 자식 클래스 중 어떤 것이라도 전달 가능하다.위 두 예시의 차이점은, 퍼블릭 인터페이스를 통해 할인 정책을 설정할 수 있는 방법을 제공하는지 여부다.
Movie
가 DisocuntPolicy
에 의존한다는 사실을 Movie
의 퍼블릭 인터페이스에 들어낸다.Movie
가 DiscountPolicy
에 의존한다는 사실이 감춰진다.public class Movie {
...
private DiscountPolicy discountPolicy
public Movie(String title, Duration runningTime, Money fee) {
...
this.discountPolicy = new AmountDiscountPolicy(Money.wons(800),
new Sequence Condition(1),
new Sequence Condition(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)));
}
}
인스턴스 생성로직과 생성된 인스턴스를 사용하는 로직을 분리하면 된다.
앞서 언급된 의존성 해결방법을 통해 분리한다.
Movie
는 입력받은 인스턴스 사용의 책임만 가진다Movie
의 클라이언트가 인스턴스를 생성하고 주입한다.public class Movie {
...
private DiscountPolicy discountPolicy
public Movie(String title, Duration runngingTime, Money fee, DiscountPolicy discountPolicy) {
...
this.discountPolicy = discountPolicy;
}
}
Movie avatar = new Movie("아바타",
Duration.ofMinutes(120),
Money.wons(10000),
new AmountDiscountPolicy(Money.wons(800),
new Sequence Condition(1),
new Sequence Condition(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)));
클라이언트에서 코드가 과도하게 중복되는 것을 막기위해, 객체 인스턴스 기본값을 사용하고 싶은 경우가 있을 수 있다. 이는 결합도와 사용성의 트레이드 오프로, 구체 클래스와의 의존으로 결합도가 증가되더라도 클래스의 사용성이 더 중요하다면, 아래의 방법들을 사용할 수 있다.
public class Movie {
...
private DiscountPolicy discountPolicy;
public Movie(String title, Duration runningTime) {
this(title, runningTime, new AmountDiscountPolicy(...));
}
public Movie(String title, Duration runningTime, Money fee, DiscountPolicy discountPolicy) {
...
this.discountPolicy = discountPolicy;
}
}
public class Movie {
public Money calcualteMovieFee(Screening screening) {
return calculateMovieFee(screening, new AmountDiscountPolicy(...)));
}
puublic Movie calcualteMovieFee(Screening screening, Discount{olicy disscountPolicy) {
returh fee.minus(discountPolicy.calculateDiscountAmount(screenig));
}
}
public abstract class DiscountPolicy {
private List<DiscountCondition> conditions = new ArrayList<>();
}
JDK 표준 컬렉션 라이브러리의 ArrayList
의 변경 가능성은 0에 가깝다.
따라서 직접 인스턴스를 생성하더라도 문제가되지 않는다.
이 경우에도 가능한 한 추상적인 타입을 사용하는 것이 확장성 측면에서 유리하다.
예시에서는 List
를 사용하여 대체의 범위를 높혔다.
뿐만 아니라, 의존성의 영향이 적은 경우에도 추상화에 의존하고 의존성을 명시적으로 드러내는 것은 좋은 습관이다.
public abstract class Movie {
private List<DiscountCondition> conditions = new ArrayList<>();
public void switchConditions(List<DiscountCondition> conditions) {
this.conditions = conditions;
}
}
지금까지 Movie
의 유연한 설계에 대해 알아봤다.
이제 실제 추가적인 요구사항을 기반으로 Movie
의 재사용성을 확인해보자.
public class Movie {
public Movie(String title, Duration runningTime, Money fee) {
this(title, runningTime, fee, null);
}
public Movie(String title, Duration runningTime, Money fee, DiscountPolicy discountPolicy) {
...
this.discountPolicy = discountPolicy;
}
public Money calculateMovieFee(Screening screening) {
if (discountPolicy == null) {
retrun fee;
}
return fee.minus(discountPolicy.calculateDiscountAmont(screening));
}
}
discountPolicy
가 null
인지 체크하는 요소를 추가했다.Movie
와 DiscountPolicy
사이의 협력 방식에 예외 케이스가 추가된다.Movie
내부 코드를 직접 수정해야 했다.할인 정책이 존재하지 않는다
는 사실을 기존의 협력 방식에 통합시킨다.할인 정책이 존재하지 않는다
는 사실을 할인 정책의 한 종류로 간주한다.할인비적용
정책 추가public class NoneDiscountPolicy extends DiscountPolicy {
@Overrie
protected Money getDisocuntAmount(Screening screening) {
return Money.ZERO;
}
}
// 클라이언트 코드
Movie avatar = new Movie("아바타",
Duration.ofMinutes(120),
Money.wons(10000),
new NoneDiscountPolic());
Movie
가 하나 이상의 DiscountPolicy
와 협력할 수 있어야 한다.Movie
가 List<DiscountPolicy>
를 인스턴스 변수로 갖게하는 것.public class OverlappedDiscountPolicy extends DiscountPolicy {
private List<DiscountPolicy> discountPolicies = new ArrayList<>();
public OverlappedDiscountPolciy(DiscountPolicy ... discountPolicies) {
this.discountPolicies = Arrays.asList(discountPolicies);
}
@Override
protected Money getDiscountAmount(Screening screening) {
Money result = Money.ZERO;
for (DiscountPolicy each: discountPolicies) {
result = result.plus(each.calculateDiscountAmount(screening));
}
return result;
}
}
// 클라이언트 코드
Movie avatar = new Movie("아바타",
Duration.ofMinutes(120),
Money.wons(1000)m
new OverlappedDiscountPolicy(
new AmountDiscountPolicy(...),
new PercentDiscountPolicy(...)));
설계가 유연할 수 있었던 이유는
Movie
가 DiscountPolicy
라는 추상화에 의존하고DisocuntPolicy
에 대한 의존성을 명시적으로 드러냈으며new
와 같이 구체 클래스를 직접적으로 다뤄야하는 책임을 Movie
외부로 옮겼기 때문이다.결하도를 낮춤으로써 얻게되는 컨택스트 확장이라는 개념이 유연하고 재사용 가능한 설계의 핵심이다.
Movie
를 재사용할 수 있었던 것은, 추상화를 기반으로 교체기능을 제공했기 때문이다.PercentDiscountPolicy
: 비율 할인 정책AmountDiscountPolicy
: 금액 할인 정책OverlappedDiscountPolicy
: 중복 할인 정책NoneDiscountPolicy
: 할인정책 비적용어떻게(how)
하는지를 장확하게 나열무엇(what)
을 하는지를 표현하는 클래스들의 조합new Movie("아바타",
Duration.ofMinutes(120),
Money.wons(1000),
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)));
Movie
생성 코드로부터 아래의 사항들을 알 수 있다.객체지향 시스템은 협력하는 객체들의 네트워크로 구성돼 있다. 시스템은 객체를 생성해 서로 메시지를 주고 받을 수 있게 조립하는 과정을 거쳐 만들어진다. 시스템의 행위는 객체의 조합(객체의 선택과 연결 방식)을 통해 나타나는 특성이다.
따라서 시스템에 포함된 객체의 구성을 변경해(절차적인 코드를 작성하기보다는 인스턴스 추가나 제거 또는 조합을 달리해서) 시스템의 작동 방식을 변경할 수 있다. 시스템을 이런 방식으로 구축하면 방법(how)이 아니라 목적(what)에 집중할 수 있어 시스템의 행위를 변경하기가 더 쉽다[Freeman09].