diff --git "a/8\354\236\245 - \354\235\230\354\241\264\354\204\261 \352\264\200\353\246\254\355\225\230\352\270\260/\354\235\264\354\244\200\354\204\235.md" "b/8\354\236\245 - \354\235\230\354\241\264\354\204\261 \352\264\200\353\246\254\355\225\230\352\270\260/\354\235\264\354\244\200\354\204\235.md" new file mode 100644 index 0000000..cbe49c2 --- /dev/null +++ "b/8\354\236\245 - \354\235\230\354\241\264\354\204\261 \352\264\200\353\246\254\355\225\230\352\270\260/\354\235\264\354\244\200\354\204\235.md" @@ -0,0 +1,256 @@ +# 8장 의존성 관리하기 + +# 정리 + +--- + +객체지향 설계란 의존성을 관리하는 것이고 객체가 변화를 받아들일 수 있게 의존성을 정리하는 기술이라고 할 수 있다[Metz12]. + +## 변경과 의존성 + +의존성이란 의존하고 있는 대상의 변경에 영향을 받을 수 있는 가능성이다. + +어떤 객체가 협력하기 위해 다른 객체를 필요로 할 때 두 객체 사이에 의존성이 존재하게 된다. + +의존성은 실행 시점과 구현 시점에 서로 다른 의미를 가진다. + +- 실행 시점: 의존하는 객체가 정상적으로 동작하기 위해서는 실행 시에 의존 대상 객체가 반드시 존재해야 한다. +- 구현 시점: 의존 대상 객체가 변경될 경우 의존하는 객체도 함께 변경된다. + +### 의존성은 **방향성을 가지며 항상 단방향**이다. + +EX) PeriodCondition - - -> Screening + +Screening이 변경되면 PeriodCondition이 영향을 받지만 그 역은 성립하지 않는다. + +### 의존성 전이 + +EX) PeriodCondition - - -> Screening - - -> Movie + +의존성 전이에 의해 잠재적으로 PeroidCondition은 Movie에 의존한다. + +의존성은 전이될 수 있기 때문에 의존성의 종류를 다음과 같이 나누기도 한다. + +- ************\*\*\*\*************직접 의존성:************\*\*\*\************* 한 요소가 다른 요소에 직접 의존하는 경우 +- ************\*\*\*\*************간접 의존성:************\*\*\*\************* 직접적인 관계는 존재하지 않지만 의존성 전이에 의해 영향이 전파되는 경우를 가리킨다. + +### 런타임 의존성과 컴파일타임 의존성 + +런타임이란 말 그대로 애플리케이션이 실행되는 시점을 가리킨다. + +컴파일타임이란 일반적으로 작성된 코드를 컴파일하는 시점을 가리키지만 문맥에 따라 코드 그 자체를 가리키도 한다. + +객체지향 애플리케이션에서 런타임의 주인공은 객체다. 따라서 런타임 의존성이 다루는 주제는 객체사이의 의존성이다. 반면 코드 관점에서 주인공은 클래스다. 따라서 컴파일타임 의존성이 다루는 주제는 클래스 사이의 의존성이다. + +금액 할인 정책과 비율 할인 정책 클래스를 할인 정책 추상 클래스를 상속하도록 만들고, Movie 클래스가 할인 정책 추상 클래스인 DiscountPolicy에 의존하도록 만든다. 그리고 이 컴파일타임 의존성을 런타임에 Amount DiscountPolicy나 Percent Discount Policy 인스턴스에 의존하도록 대체한다. + +이렇게 함으로써 코드 작성 시점의 Movie 클래스는 할인 정책을 구현한 두 클래스의 존재를 모르지만 실행 시점의 Movie 객체는 두 클래스의 인스턴스와 협력할 수 있게 된다. + +이처럼 **유연하고 재사용 가능한 설계를 창조하기 위해서는 컴파일타임 의존성과 런타임 의존성이 달라야한다**. + +### 컨텍스트 독립성 + +각 객체가 해당 객체를 실행하는 시스템에 관해 아무것도 알지 못한다는 의미이다. + +따라서 시스템을 구성하는 객체가 컨텍스트 독립적이라면 다양한 컨텍스트에 적용할 수 있는 응집력 있는 객체를 만들 수 있고 객체 구성 방법을 재설정해서 변경하기 쉬운 시스템으로 나아갈 수 있다. + +### 의존성 해결 + +컴파일타임 의존성을 실행 컨텍스트에 맞는 적절한 런타임 의존성으로 교체하는 것. + +### Sol 1. 객체를 생성하는 시점에 생성자를 통해 의존성 해결 + +EX) 생성자 인자로 할인정책 인스턴스 전달 + +```java +Movie movie = new Movie("영화", new PercentDiscountPolicy(...)); +``` + +### Sol 2 . 객체 생성 후 setter 메서드를 통해 의존성 해결 + +```java +Movie movie = new Movie(...); +movie.setDiscountPolicy(new PercentDiscountPolicy(...)); +``` + +setter 메서드만 이용하면 객체를 생성하고 의존 대상을 설정하기 전까지 객체의 상태가 불완전할 수 있따는 단점이 있다. + +EX) + +```java +Movie movie = new Movie(...); +// 할인 정책을 갖고 가격 계산해야 하지만 DiscountPolicy가 null -> NPE 발생 +movie.calculateFee(...); +movie.setDiscountPolicy(new PercentDiscountPolicy(...)); +``` + +**따라서 생성자 방식과 setter 방식을 혼합하는 것이 낫다.** + +항상 객체를 생성할 때 의존성을 해결한 상태로 객체를 생성하고, 필요할 때 setter 메서드를 통해 의존 대상을 변경할 수 있게 한다. + +이 방법은 시스템의 상태를 안정적으로 유지하면서 유연성을 향상시킬 수 있기 때문에 의존성 해결을 위해 가장 선호되는 방법이다. + +EX) + +```java +Movie movie = new Movie("영화", new PercentDiscountPolicy(...)); +// 할인 정책을 갖고 가격 계산해야 하지만 DiscountPolicy가 null -> NPE 발생 +movie.calculateFee(...); +movie.setDiscountPolicy(new AmountDiscountPolicy(...)); +``` + +### Sol 3. 메서드 실행 시 인자를 이용해 의존성 해결 + +객체가 항상 특정 속성에 대해 알 필요없고, 메서드를 실행할 때만 **일시적으로** 알아도 무방할 때 사용한다. + +EX) + +```java +public class Moive{ + public Money calculateMovieFee(Screening screening, DiscountPolicy discountPolicy){ + return fee.minus(discountPolicy.calculateDiscountAmount(screening)); + } +} +``` + +하지만 클래스의 메서드를 호출하는 대부분의 경우 매번 동일한 객체를 인자로 전달한다면 생성자를 이용하거나 setter메서드를 이용해 의존성을 지속적으로 유지하는 방식이 더 좋다. + +## 의존성과 결합도 + +의존성과 결합도는 재사용성과 관련이 있다. + +어떤 의존성이 다양한 환경에서 재사용할 수 있다면 결합도가 낮은 것이고, 재사용성이 낮다면 결합도가 높은 것이다. + +특정 객체의 자식에게 의존하는 경우 결합도가 강하다. + +따라서 **추상화에 의존해야 한다.** + +**추상화**란 어떤 양상, 세부사항, 구조를 좀 더 명확하게 이해하기 위해 특정 절차나 물체를 의도적으로 생략하거나 감춤으로써 복잡도를 극복하는 방법이다[Kramer07]. + +일반적으로 추상화와 결합도의 관점에서 의존 대상을 다음과 같이 구분하는 것이 유용하다. 목록에서 아래쪽으로 갈수록 클라이언트가 알아야 하는 지식의 양이 적어지기 때문에 결합도가 느슨해진다. + +- 구체 클래스 의존성(concrete class dependency) +- 추상 클래스 의존성(abstract class dependency) +- 인터페이스 의존성(interface dependency) + +실행 컨텍스트에 대해 알아야 하는 정보를 줄일수록 결합도가 낮아진다. + +의존하는 대상이 더 추상적일수록 결합도는 더 낮아진다. + +### 명시적 의존성과 숨겨진 의존성 + +**의존성은 명시적으로 표현돼야 한다.** + +- 명시적 의존성: 의존성이 명시적으로 퍼블릭 인터페이스에 노출된 것 +- 숨겨진 의존성: 의존성이 퍼블릭 인터페이스에 표현되지 않은 것 + +유연하고 재사용 가능한 설계란 인터페이스를 통해 의존성이 명시적으로 드러나는 설계다. + +의존성이 명시적이지 않으면 클래스를 다른 컨텍스트에서 재사용하기 위해 내부 구현을 직접 변경해야 한다. + +코드 수정은 언제나 잠재적으로 버그 발생 가능성을 내포하는데 의존성을 명시적으로 드러내면 코드를 직접 수정해야 하는 위험을 피할 수 있다. + +### new사용을 ‘지양’하자(ArrayList와 같은 표준 클래스 제외) + +- new 연산자를 사용하기 위해서는 구체 클래스의 이름을 직접 기술해야 한다. 따라서 new를 사용하는 클라이언트는 추상화가 아닌 구체 클래스에 의존할 수 밖에 없기 때문에 결합도가 높아진다. +- new 연산자는 생성하려는 구체 클래스뿐만 아니라 어떤 인자를 이용해 클래스의 생성자를 호출해야 하는지도 알아야 한다. 따라서 new를 사용하면 클라이언트가 알아야 하는 지식의 양이 늘어나기 때문에 결합도가 높아진다. + +### 사용하는 것이 더 나은 경우도 있다 + +EX) + +```java +public class Movie { + private DiscountPolicy discountPolicy; + + public Movie(String title, Duration runningTime, Money fee) { + this(title, runningTime, fee, new AmountDiscountPolicy(...)); // 두 번째 생성자로 체이닝 + } + + public Movie(String title, Duration runningTime, Money fee, DiscountPolicy discountPolicy) { + ... + this.discountPolicy = discountPolicy; + } +} +``` + +Movie가 대부분 AmountDiscountPolicy 인스턴스와 협력할 경우 + +위와 같이 기본 객체를 생성하는 생성자를 추가하고 이 생성자에서 DiscountPolicy 인스턴스를 인자로 받는 생성자를 체이닝하는 것이 낫다[Kerievsky04]. + +이 방법은 메서드 오버로딩하는 경우에도 사용 가능하다. + +EX) + +```java +public class Movie { + public Money caculateMovieFee(Screening screening) { + return calculateMovieFee(screening, new AmountDiscountPolicy(...)); + } + + public Money caculateMovieFee(Screening screening, DiscountPolicy discountPolicy) { + return fee.minus(discountPolicy.calculateDiscountAmount(screening)); + } +} +``` + +### 컨텍스트 확장 + +Case1. 할인 혜택을 제공하지 않는 영화의 경우 + +내부 코드에서 discountPolicy가 null인지 예외 케이스로 처리하지 말고 기존에 Movie와 DiscountPolicy가 협력하던 방식으로 처리해야한다. + +즉, 할인할 금액으로 0원을 반환하는 NoneDiscountPolicy클래스를 추가하고 DiscountPolicy를 상속하도록 하는 것이다. + +이렇게 함으로써 기존과 같은 방식인 생성자에 NoneDiscountPolicy 인스턴스를 전달하는 방식으로 할인 혜택을 제공하지 않는 영화를 구현할 수 있다. + +Case2. 다수의 할인 정책을 중복해서 적용하는 영화를 처리하는 경우 + +간단한 방식으로는 Movie가 DiscountPolicy의 인스턴스들로 구성된 List를 인스턴스 변수로 갖게 하는 것이다. 하지만 이 방법은 기존 협력 방식과 다른 예외 케이스를 추가하게 한다. + +따라서 이 문제 역시 Case1과 마찬가지로 DiscountPolicy를 상속하는 클래스를 하나 생성하여 해결할 수 있다. + +EX) + +```java +public class OverlappedDiscountPolicy extends DiscountPolicy { + private List discountPolicies = new ArrayList<>(); + + public OverlappedDiscountPolicy(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; + } +} +``` + +어떤 경우든 코드 내부를 직접 수정하는 것은 버그 발생 가능성을 높이는 것이다. + +시스템에 포함된 객체의 구성을 변경해(절차적인 코드르 작성하기보다는 인스턴스 추가나 제거 또는 조합을 달리해서) 시스템의 작동 방식을 바꿀 수 있다. 이러한 객체 구성을 관리할 목적으로 작성하는 코드를 객체 네트워크의 행위에 대한 선언적인 정의라고 한다. 시스템을 이런 방식으로 구축하면 방법(how)이 아니라 목적(what)에 집중할 수 있어 시스템의 행위를 변경하기가 더 쉽다[Freeman09]. + +# 읽고 난 후 + +--- + +디자인 패턴을 수강하는데 교수님께서 커피가게를 예로 Size(Tall, Grande, Venti)를 데코레이터 패턴으로 적용해보라고 하셨다. Size는 휘핑, 에스프레소와 같은 첨가물처럼 음료(추상클래스)와 1:N관계가 아닌 1:1의 관계라 음료의 속성으로 들어가는 것이 맞다고 판단했고, 데코레이터 패턴을 적용하는 것이 옳지 않다고 생각했다. + +하지만 이번 장을 읽고 생각이 바뀌게 되었다. + +위와 같이 설계를 할 때는 음료(추상 클래스)를 상속받는 모든 구체 클래스(Concrete Class) 내부 코드에 예외 케이스를 두어야 한다. 또한, 사이즈가 추가가 되거나 사이즈 이름이 변경되었을 경우 모든 구체 클래스 내부 코드를 수정해야 하는 일이 발생한다. + +따라서 음료(추상클래스)를 상속하는 데코레이터 클래스 Size를 만들고, 원하는 사이즈 클래스를 만든다. 그리고 음료클래스와 Size 대응하도록 음료 속성에 Size를 두면 결합도가 낮아지고, 변화를 잘 받아들일 수 있게 될 것이라고 생각했다. + +++ 다시 바뀌었다.~~(ㅋㅋㅋ ㅋㅋ ㅋ)~~ + +Size와 음료가 1:1 대응이 되어야 하는데 이를 위해선 음료의 속성으로 Size 인스턴스가 추가되어야 한다. 하지만 Size를 데코레이터 패턴을 사용하여 구현을 하게 되면 Size의 하위 클래스가 음료를 여러번 감쌀 수 있게 된다. + +이는 처음 음료당 Size가 하나이어야 한다는 요구사항을 벗어나게 됨으로 잘못된 패턴의 적용이라고 할 수 있다. + +이를 제대로 구현하기 위해서는 Size와 음료 사이의 상속관계만 없애주면 된다.