변경과 의존성의존성 전이런타임 의존성과 컴파일 타임 의존성컨텍스트 독립성의존성 해결하기유연한 설계의존성과 결합도명시적인 의존성new는 해롭다가끔은 생성해도 무방컨텍스트 확장하기할인 혜택 제공 x다수의 할인 정책 중복 적용
요약 : 한마디로 추상화에 의존하라. 인터페이스 > 추상 클래스 > 구체 클래스 순서로 결합도가 낮다.
- 잘 설계된 객체지향 애플리케이션은 작고 응집도 높은 객체들로 구성된다. 작고 응집도 높은 객체란 책임의 초점이 명확하고 한 가지 일만 잘하는 객체를 의미함
- 그러나 이런 작은 객체들이 단독으로 수행할 수 있는 작업은 거의 없기 때문에 일반적인 애플리케이션의 기능을 구현하기 위해서는 다른 객체에게 도움을 요청해야 한다. 이런 요청이 객체 사이의 협력을 낳는다.
- 협력은 필수적이지만 과도한 협력은 설계를 곤경에 빠트릴 수 있음. ⇒ 객체 사이의 의존성
- 협력을 위해서는 의존성이 필요하지만 과도한 의존성은 애플리케이션을 수정하기 어렵게 만든다
객체지향 설계의 핵심은 협력을 위해 필요한 의존성은 유지하면서도 변경을 방해하는 의존성은 제거하는 데 있다. 이런 관점에서 객체지향 설계란 의존성을 관리하는 것이고 객체가 변화를 받아들일 수 있게 의존성을 정리하는 기술이라고 할 수 있다. ⇒
인터페이스, 추상클래스에 의존하라. 구체클래스가 아니라
변경과 의존성
- 두 요소 사이의 의존성은 의존되는 요소가 변경될 때 의존하는 요소도 함께 변경될 수 있다는 것을 의미함
- 의존성 ↔ 변경에 의한 영향의 전파 가능성
의존성 전이
의존성 전이
가 의미하는 것은 PeriodCondition이 Screening에 의존할 경우 PeriodCondition은 Screening이 의존하는 대상에 대해서도 자동적으로 의존하게 된다는 것임- Screening이 Movie, LocalDateTime, Customer에 의존한다면 PeriodCondition 또한 간접적으로 해당 대상에 의존하게 됨
- 의존성은 함께 변경될 수 있는 가능성을 의미하기 때문에 모든 경우에 의존성이 전이되는 것은 아님. 의존성이 실제로 전이될 지 여부는 변경의 방향과 캡슐화의 정도에 따라 달라짐
- 의존성은 전이될 수 있기에 의존성의 종류를
직접 의존성
과간접 의존성
으로 나누기도 함 - 직접 의존성이란 말 그대로 한 요소가 다른 요소에 직접 의존하는 경우. 코드에 바로 드러남
- 간접 의존성은 직접적인 관계는 존재하지 않지만 의존성 전이에 의해 영향이 전파되는 경우를 가리킴
런타임 의존성과 컴파일 타임 의존성
- 객체지향 애플리케이션에서 런타임의 주인공은 객체다. 따라서 런타임 의존성이 다루는 주제는 객체 사이의 의존성
- 반면 코드 관점에서 주인공은 클래스다. 따라서 컴파일타임 의존성이 다루는 주제는 클래스 사이의 의존성
- 중요한 것은 런타임 의존성과 컴파일타임 의존성이 다를 수 있다는 것임. 유연하고 재사용 가능한 코드를 설계하기 위해서는 두 종류의 의존성을 서로 다르게 만들어야 함
- 어떤 클래스의 인스턴스가 다양한 클래스의 인스턴스와 협력하기 위해서는 협력할 인스턴스의 구체적인 클래스를 알아서는 안 된다. 실제로 협력할 객체가 어떤 것인지는 런타임에 해결해야 한다.
컨텍스트 독립성
- 클래스가 특정한 문맥에 강하게 결합될수록 다른 문맥에서 사용하기는 더 어려워짐
- 컨텍스트 독립적이라는 말은 각 객체가 해당 객체를 실행하는 시스템에 관해 아무것도 알지 못한다는 의미
의존성 해결하기
의존성 해결의 세가지 방법
객체를 생성하는 시점에 생성자를 통해 의존성 해결
Movie avatar = new Movie("아바타", Duration.ofMinutes(120), Money.wons(10000), new AmountDiscountPolicy(...)); Movie starWars = new Movie("스타워즈", Duration.ofMinutes(100), Money.wons(10000), new PercentDiscountPolicy(...));
객체 생성 후 setter 메서드를 통해 의존성 해결
- 이 방식은 객체의 상태가 불완전할 수 있다는 단점이 존재함. 그래서 생성자 방식과 setter 방식을 같이 혼합해서 쓰는 방식이 자주 사용됨
Movie avatar = new Movie(...); avatar.setDiscountPolicy(new AmountDiscountPolicy(...));
메서드 실행 시 인자를 이용해 의존성 해결
- 해당 의존성을 항상 알 필요 없고 특정 행위를 할 때만 일시적으로 알아도 무방할 때 메서드 인자를 이용해 의존성 해결 가능
public class Movie public Money calculateMovieFee(Screening screening, DiscountPolicy discountPolicy) { ... } }
유연한 설계
의존성과 결합도
- 바람직한 의존성이란? 어떤 의존성이 다양한 환경에서 클래스를 재사용 할 수 없도록 제한한다면 그 의존성은 바람직하지 못한 것이다.
- 다시 말해, 컨텍스트에 독립적인 의존성은 바람직한 의존성이고 특정한 컨텍스트에 강하게 결합된 의존성은 바람직하지 않은 의존성임
- 의존성이 바람직할 때 → 약한 결합도(loose coupling)
- 의존성이 바람직하지 못할 때 → 단단한 결합도(tight coupling)
- 결합도의 정도는 한 요소가 자신이 의존하고 있는 다른 요소에 대해 알고 있는 정보의 양으로 결정된다.
- Movie가
PercentDiscountPolicy
에 의존한다고 가정해보면 Movie는 협력할 객체가 비율 할인 정책에 따라 할인 요금을 계산할 것이라는 사실을 알고 있는 것 - 반면
DiscountPolicy
클래스에 의존하는 경우에는 구체적인 계산 방법은 알 필요가 없음. 그저 할인 요금을 계산한다는 사실만 알고 있을 뿐
명시적인 의존성
- 명시적인 의존성과 숨겨진 의존성
- 의존성이 명시적이지 않으면 클래스를 다른 컨텍스트에서 재사용하기 위해 내부 구현을 직접 변경해야 함
- 의존성을 명시적으로 드러내면 코드를 직접 수정해야 하는 위험을 피할 수 있음. 실행 컨텍스트에 적절한 의존성을 선택할 수 있기 때문에
- 의존성은 명시적으로 표현되어야 한다(생성자, setter, 메서드 인자를 통해). 의존성을 구현 내부에 숨겨두지 마라
public class Movie { private DiscountPolicy discountPolicy; public Movie(String title, Duration runningTime, Money fee) { ... this.discountPolicy = new AmountDiscountPolicy(...); } }
new는 해롭다
- 클래스의 내부에서 new를 이용해서 인스턴스 변수를 직접 할당하게 되면 결합도가 극단적으로 높아짐
public class Movie { private DiscountPolicy discountPolicy; public Movie(String title, Duration runningTime, 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)));
- 해결 방법은 인스턴스를 생성하는 로직과 생성된 인스턴스를 사용하는 로직을 분리하는 것임
- 사용과 생성의 책임을 분리해서 Movie의 결합도를 낮추면 설계를 유연하게 만들 수 있음
가끔은 생성해도 무방
- 클래스 안에서 객체의 인스턴스를 직접 생성하는 방식이 유용한 경우도 있음. 주로 협력하는 기본 객체를 설정하고 싶은 경우가 여기에 속함
- 예로, Movie가 대부분의 경우에는 AmountDiscountPolicy와 협력하고 가끔씩만 PercentDiscountPolicy의 인스턴스와 협력한다고 하면 Movie의 사용성이 나빠질 것임
- 이럴 때는 생성자 체이닝을 통해서 사용성을 높일 수 있음
public class Movie { private DiscountPolicy discountPolicy; public Movie(String title, Duration runningTime, Money fee) { this(title, runningTime, fee, new AmountDiscounyPolicy(...)); } public Movie(String title, Duration runningTime, Money fee, DiscountPolicy discountPolicy) { ... this.discountPolicy = discountPolicy } }
컨텍스트 확장하기
- Movie가 유연하다는 사실을 입증하기 위한 두가지 예시 도입 : 할인 혜택 제공하지 않는 영화, 다수의 할인 정책 중복해서 적용하는 영화
할인 혜택 제공 x
- 이 경우 DiscountPolicy에 null을 넣어줄 수 있지만 그렇게 하지 않고 NoneDiscountPolicy라는 클래스를 생성해 해당 객체와 Movie 가 협력하도록 해주면 협력관계를 변경하지 않고 확장이 가능함
Movie avatar = new Movie("아바타", Duration.ofMinutes(120), Money.wons(10000), new NoneDiscountPolicy());
다수의 할인 정책 중복 적용
- 처음 생각해보면 DiscountPolicy의 List를 갖도록 Movie를 변경하려고 생각할 수 있지만, 아래와 같이 하면 협력관계를 바꿀 필요 없이 확장이 가능함
public class OverlappedDiscountPolicy extends DiscountPolicy { private List<DiscountPolicy> discountPolicies = new ArrayList(); @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(10000), new OverlappedDiscountPolicy( new AmountDiscountPolicy(...), new PercentDiscountPolicy(...)));
훌륭한 객체지향 설계란 객체가 어떻게 하는지를 표현하는 것이 아니라 객체들의 조합을 선언적으로 표현함으로써 객체들이 무엇을 하는지를 표현하는 설계다.
이런 설계를 창조하는 데 있어서 핵심은 의존성을 관리하는 것임