실행 시점: 의존하는 객체가 정상적으로 동작하기 위해서는 실행 시에 의존 대상 객체가 반드시 존재해야 한다.
구현 시점: 의존 대상 객체에 변경이 발생할 경우 의존하는 객체도 함께 변경된다.
public class PeriodCondition implements DiscountCondition {
private DayOfWeek dayOfWeek;
private LocalTime startTime;
private LocalTime endTime;
public boolean isSatisfiedBy(Screening screeing){
return screeing.getStartTime().getDayOfWeek().equals(dayOfWeek)
&&
startTime.compareTo(screening.getStartTime().toLocalTime())<=0
&&
endTime.compareTo(screening.getStartTime().toLocalTime())>=0;
}
}
실행 시점에 PeriodCondition의 인스턴스가 정상적으로 동작하기 위해서는 Screening의 인스턴스가 존재하고 getStartTime 메시지를 이해할 수 있어야 한다.
어떤 객체가 예정된 작업을 정상적으로 수행하기 위해 다른 객체를 필요로 하는 경우 두 객체 사이에 의존성이 존재한다고 말한다.
의존성은 방향성을 가지며 항상 단방향이다.
두 요소 사이의 의존성은 의존되는 요소가 변경될 때 의존하는 요소도 함께 변경될 수 있다는 것을 의미한다.
의존성은 변경에 의한 영향의 전파 가능성을 암시한다.
PeriodCondition은 DiscountCondition 인터페이스를 구현하고 있으며 DayOfWeek과 LocalTime의 인스턴스를 속성으로 포함하고 isSatisfiedBy 메서드의 인자로 Screening의 인스턴스를 받는다.
또한 DayOfWeek의 인스턴스에게 compareTo 메시지를 전송하고 Screening의 인스턴스에게 getStartTime 메시지를 전송한다.
따라서 PeriodCondition은 DiscountCondition, DayOfWeek, LocalTime, Screening에 대해 의존성을 갖는다.
각각의 의존성을 다른 방식으로 표기했지만 의존성이 가지는 근본적인 특성은 동일하다.
PeriodCondition은 자신이 의존하는 대상이 변경될 때 함께 변경될 수 있다는 것이다.
의존성 전이
의존성은 전이될 수 있다.
의존성 전이가 의미하는 것은 PeriodConditon이 Screening에 의존할 경우 PeriodCondition은 Screening이 의존하는 대상에 대해서도 자동적으로 의존하게 된다는 것이다.
의존성은 함께 변경될 수 있는 "가능성"을 의미하기 때문에 모든 경우에 의존성이 전이되는 것은 아니다.
의존성이 실제로 전이될지 여부는 변경의 방향과 캡슐화의 정도에 따라 달라진다.
Screening이 내부 구현을 효과적으로 캡슐화하고 있다면 Screening에 의존하고 있는 PeriodCondition까지는 변경이 전파되지 않을 것이다.
의존성은 전이될 수 있기 때문에 의존성의 종류를 직접 의존성과 간접 의존성으로 나누기도 한다.
런타임 의존성과 컴파일타임 의존성
런타임은 애플리케이션이 실행되는 시점
컴파일타임이란 일반적으로 작성된 코드를 컴파일하는 시점을 말하지만 문맥에 따라서는 코드 그 자체를 가리키기도 한다.
컴파일타임 의존성이 바로 이런 경우로, 컴파일타임 의존성이라는 용어가 중요하게 생각하는 것은 시간이 아니라 작성한 코드 구조이다.
객체지향 어플리케이션에서 런타임의 주인공은 객체이므로 런타임 의존성이 다루는 주제는 객체 사이의 의존성이다.
반면 컴파일타임, 즉 코드 관점에서의 주인공은 클래스이므로 컴파일타임 의존성이 다루는 주제는 클래스 사이의 의존성이다.
런타임 의존성과 컴파일타임 의존성은 다를 수 있다.
유연하고 재사용 가능한 코드를 설계하기 위해서는 두 종류의 의존성을 서로 다르게 만들어야 한다.
컴파일타임 의존성을 실행 시에 런타임 의존성으로 대체해야 한다.
유연하고 재사용 가능한 설계를 창조하기 위해서는 동일한 소스 코드 구조를 가지고 다양한 실행 구조를 만들 수 있어야 한다.
어떤 클래스의 인스턴스가 다양한 클래스의 인스턴스와 협력하기 위해서는 협력할 인스턴스의 구체적인 클래스를 알아서는 안 된다.
실제로 협력할 객체가 어떤 것인지는 런타임에 결정해야 한다.
컴파일 타임 구조와 런타임 구조 사이의 거리가 멀면 멀수록 설계가 유연해지고 재사용 가능해진다.
컨텍스트 독립성
구체 클래스에 대해 의존하는 것은 클래스의 인스턴스가 어떤 문맥에서 사용될 것인지를 구체적으로 명시하는 것과 같다.
클래스가 특정한 문맥에 강하게 결합될수록 다른 문맥에서 사용하기는 더 어려워진다.
클래스가 사용될 특정한 문맥에 최소한의 가정만으로 이뤄져 있다면 다른 문맥에서 재사용하기가 더 수월해진다.
이를 컨텍스트 독립성이라고 한다.
설계가 유연해지기 위해서는 가능한 한 자신이 실행될 컨텍스트에 대한 구체적인 정보를 최대한 적게 알아야 한다.
의존성 해결하기
의존성 해결: 컴파일타임 의존성을 실행 컨텍스트에 맞는 적절한 런타임 의존성으로 교체하는 것
의존성을 해결하기 위한 세 가지 방법
객체를 생성하는 시점에 생성자를 통해 의존성 해결
객체 생성 후 setter 메서드를 통해 의존성 해결
메서드 실행 시 인자를 이용해 의존성 해결
setter 메서드를 이용하는 방식은 객체를 생성한 이후에도 의존하고 있느 대상을 변경할 수 있는 가능성을 열어 놓고 싶은 경우에 유용
setter 메서드를 이용하는 방법은 실행 시점에 의존 대상을 변경할 수 있기 때문에 설계를 좀더 유연하게 만들 수 있지만 객체가 생성된 후에 협력에 필요한 의존 대상을 설정하기 때문에 객체 생성 후 의존 대상을 설정하기 전까지는 객체의 상태가 불완전할 수 있다.
더 좋은 방법은 생성자 방식과 setter 방식을 혼합하는 것
메서드 인자를 사용하는 방식은 지속적인 의존 관계가 필요 없이 일시적인 의존 관계가 존재해도 무방하거나 메서드가 실행될 때마다 의존 대상이 매번 달라져야 하는 경우에 유용
클래스의 메서드를 호출하는 대부분의 경우에 매번 동일한 타입의 객체를 인자로 전달하고 있다면 생성자를 이용하는 방식이나 setter 메서드를 이용해 의존성을 지속적으로 유지하는 방식으로 변경하는 것이 좋다.
2. 유연할 설계
의존성과 결합도
의존성은 협력을 위해 반드시 필요한 것이다.
문제는 의존성의 존재가 아니라 의존성의 정도다.
바람직하지 못한 의존성이 문제일 뿐이다.
바람직한 의존성은 재사용성과 관련이 있다.
특정 컨텍스트에 강하게 결합된 의존성은 바람직하지 않은 의존성이고 컨텍스트에 독립적인 의존성은 바람직한 의존성이다.
특정 컨텍스트에 강하게 의존하는 클래스를 다른 컨텍스트에서 재사용할 수 있는 유일한 방법은 구현을 변경하는 것뿐이다.
어떤 두 요소 사이에 존재하는 의존성이 바람직할 때 두 요소가 느슨한 결합도 또는 약한 결합도를 가진다고 말한다.
두 요소 사이의 의존성이 바람직하지 못할 때 강한 결합도를 가진다고 말한다.
지식이 결합을 낳는다
결합도의 정도는 한 요소가 자신이 의존하고 있는 다른 요소에 대해 알고 있는 정보의 양으로 결정된다.
한 요소가 다른 요소에 대해 더 많은 정보를 알고 있을수록 두 요소는 강하게 결합된다.
한 요소가 다른 요소에 대해 더 적은 정보를 알고 있을수록 두 요소는 약하게 결합된다.
서로에 대해 알고 있는 지식의 양이 결합도를 결정한다. 더 많이 알수록 더 많이 결합된다.
더 많이 알고 있다는 것은 더 적은 컨텍스트에서 재사용 가능하다는 것을 의미한다.
결합도를 느슨하게 유지하려면 협력하는 대상에 대해 더 적게 알아야 한다.
협력하는 대상에 대해 필요한 정보 외에는 최대한 감추는 것이 중요, 그러기 위해선 추상화가 가장 효과적인 방법이 된다.
추상화에 의존하라
추상화와 결합도의 관점에서 의존 대상 구분
구체 클래스 의존성
추상 클래스 의존성
인터페이스 의존성
클라이언트가 알아야 하는 지식의 양 : 구체 클래스 의존성 > 추상 클래스 의존성 > 인터페이스 의존성
알아야 하는 지식의 양이 적을수록 결합도가 낮다.
의존하는 대상이 더 추상적일수록 결합도는 낮아진다.
명시적인 의존성
public class Movie {
...
private DiscountPolicy discountPolicy;
public Movie(String title, Duration runningTime, Money fee){
...
this.discountPolicy = new AmountDiscountPolicy(...);
}
}
Movie는 추상 클래스인 DiscountPolicy 뿐만 아니라 구체 클래스인 AmountDiscountPolicy에도 의존하게 된다.
결합도를 느슨하게 만들기 위해서는 인스턴스 변수의 타입을 추상 클래스나 인터페이스로 선언하는 것만으로는 부족
클래스 안에서 구체 클래스에 대한 모든 의존성을 제거해야만 한다.
그런데 Movie는 런타임에 구체 클래스의 인스턴스와 협력해야 하기 때문에 의존성을 해결해 줄 수 있는 방법이 필요하다.
의존성 해결: 컴파일 타임 의존성을 실행 컨텍스트에 맞는 적절한 런타임 의존성으로 교체하는 것
의존성을 해결하는 방법에는 생성자, setter 메서드, 메서드 인자를 사용하는 세 가지 방식이 존재한다.
인스턴스 변수의 타입은 추상 클래스나 인터페이스로 정의하고 생성자, setter 메서드, 메서드 인자로 의존성을 해결할 때는 추상 클래스를 상속하거나 인터페이스를 실체화한 구체 클래스 타입을 전달하는 것으로 해결
public class Movie {
...
private DiscountPolicy discountPolicy;
public Movie(String title, Duration runningTime, Money fee,
DiscountPolicy discountPolicy){
...
this.discountPolicy = discountPolicy;
}
}
의존성의 대상을 생성자의 인자로 선언하는 방법은 Movie가 DiscountPolicy에 의존한다는 사실을 Movie의 퍼블릭 인터페이스에 드러내는 것이다.
이는 setter 메서드를 사용하는 방식과 메서드 인자를 사용하는 방식의 경우에도 동일하다.
모든 경우에 의존성은 명시적으로 퍼블릭 인터페이스에 노출된다. => 이를 명시적인 의존성이라고 부른다
Movie의 내부에서 AmountDiscountPolicy의 인스턴스를 직접 생성하는 방식은 Movie가 DiscountPolicy에 의존한다는 사실을 숨긴다.
의존성이 퍼블릭 인터페이스에 표현되지 않는다;. => 이를 숨겨진 의존성이라고 부른다.
의존성이 명시적이지 않으면 의존성을 파악하기 위해 내부 구현을 직접 살펴볼 수 밖에 없다.'
의존성이 명시적이지 않으면 클래스를 다른 컨텍스트에서 재사용하기 위해 내부 구현을 직접 변경해야 한다.
의존성을 명시적으로 드러내면 코드를 직접 수정해야 하는 위험을 피할 수 있다.
의존성은 명시적으로 표현돼야 한다. 유연하고 재사용 가능한 설계란 퍼블릭 인터페이스를 통해 의존성이 명시적으로 드러나는 설계다.
경계해야 할 것은 의존성 자체가 아니라 의존성을 감추는 것이다.
new 는 해롭다
new를 잘못 사용하면 클래스 사이의 결합도가 극단적으로 높아진다.
결합도 측면에 new가 해로운 이유
new를 사용하는 클라이언트는 추상화가 아닌 구체 클래스에 의존할 수밖에 없기 때문에 결합도가 높아진다.
new 연산자는 생성하려는 구체 클래스뿐만 아니라 어떤 인자를 이용해 클래스의 생성자를 호출해야 하는지도 알아야 한다. => 클라이언트가 알아야 하는 지식의 양이 늘어나기 때문에 결합도가 높아진다.
new는 결합도를 높이기 때문에 해롭다.
new는 클래스를 구체 클래스에 결합시키는 것만으로 끝나지 않는다. 협력할 클래스의 인스턴스를 생성하기 위해 어떤 인자들이 필요하고 그 인자들을 어떤 순서로 사용해야 하는지에 대한 정보도 노출시킬 뿐만 아니라 인자로 사용되는 구체 클래스에 대한 의존성을 추가한다.
해결 방법은 인스턴스를 생성하는 로직과 생성된 인스턴스를 사용하는 로직을 분리하는 것이다.
인스턴스 사용과 인스턴스 생성의 책임을 다른 클래스로 분리해서 결합도를 낮출 수 있다.
인스턴스를 생성하는 책임을 객체 내부가 아니라 객체를 사용하는 클라이언트가 지도록 한다.
사용과 생성의 책임을 분리하고, 의존성을 생성자에 명시적으로 드러내고, 구체 클래스가 아닌 추상 클래스에 의존하게 함으로써 설계를 유연하게 만들 수 있다.
인스턴스를 생성하는 책임을 객체 내부가 아니라 클라이언트로 옮겨야 한다.
가끔은 생성해도 무방하다
클래스 안에서 객체의 인스턴스를 직접 생성하는 방식이 유용한 경우도 있다.
주로 협력하는 기본 객체를 설정하고 싶은 경우가 이런 경우에 속한다.
Movie가 대부분의 경우에는 AmountDiscountPolicy의 인스턴스와 협력하고 가끔씩만 PercentDiscountPolicy의 인스턴스와 협력하는 경우
이런 상황에서 모든 경우에 인스턴스를 생성하는 책임을 클라이언트로 옮긴다면 클라이언트들 사이에 중복 코드가 늘어나고 Movie의 사용성도 나빠진다.
해결 방법은 기본 객체를 생성하는 생성자를 추가하고 이 생성자에서 DiscountPolicy의 인스턴스를 인자로 받는 생성자를 체이닝하는 것이다.
public class Movie {
private DiscountPolicy discountPolicy;
public Movie(String title, Duration runningTime, Money fee) {
this(title, runningTime, fee, new AmountDiscountPolicy(...));
// 생성자 체이닝
}
public Moive(String title, Duration runningTime, Money fee,
DiscountPolicy discountPolicy){
this.discountPolicy = discountPolicy;
}
}
이제 클라이언트는 대부분의 경우에 추가된 간략한 생성자를 통해 AmountDiscountPolicy의 인스턴스와 협력하게 하면서도 컨텍스트에 적절한 DiscountPolicy의 인스턴스로 의존성을 교체할 수 있다.
이 방법은 메서드를 오버로딩하는 경우에도 사용할 수 있다.
public class Movie {
public Money calculateMovieFee(Screening screening) {
return calculateMovieFee(screening, new AmountDiscountPolicy(...));
}
public Money calculateMovieFee(Screening screening,
DiscountPolicy discountPolicy) {
return fee.minus(discountPolicy.calculateDiscountAmount(screening));
}
}
표준 클래스에 대한 의존은 해롭지 않다.
의존성이 불편한 이유: 그것이 항상 변경에 대한 영향을 암시하기 때문
자바의 JDK에 포함된 표준 클래스와 같이 변경될 확률이 거의 없는 클래스라면 의존성이 문제가 되지 않는다.
즉, 이런 클래스들에 대해서는 구체 클래스에 의존하거나 직접 인스턴스를 생성하더라도 문제가 없다.
public abstract class DiscountPolicy {
private List<DiscountCondition> conditions = new ArrayList<>();
}
비록 클래스를 직접 생성하더라도 가능한 한 추상적인 타입을 사용하는 것이 확장성 측면에서 유리
의존성의 영향이 적은 경우에도 추상화에 의존하고 의존성을 명시적으로 드러내는 것은 좋은 설계 습관이다.
public abstract class DiscountPolicy {
private List<DiscountCondition> conditions = new ArrayList<>();
public void switchConditions(List<DiscountCondition> conditions) {
this.conditions = conditions;
}
}
조합 가능한 행동
유연하고 재사용 가능한 설계는 객체가 어떻게(how) 하는지를 장황하게 나열하지 않고도 객체들의 조합을 통해 무엇(what)을 하는지를 표현하는 클래스들로 구성된다.
따라서 클래스의 인스턴스를 생성하는 코드를 보는 것만으로 객체가 어떤 일을 하는지를 쉽게 파악할 수 있다.
코드에 드러난 로직을 해석할 필요 없이 객체가 어떤 객체와 연결됐는지를 보는 것만으로도 객체의 행동을 쉽게 예상하고 이해할 수 있기 때문
유연하고 재사용 가능한 설계는 작은 객체들의 행동을 조합함으로써 새로운 행동을 이끌어낼 수 있는 설계다.
훌륭한 객체지향 설계란 객체가 어떻게 하는 지를 표현하는 것이 아니라 객체들의 조합을 선언적으로 표현함으로써 객체들이 무엇을 하는지를 표현하는 설계