Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
256 changes: 256 additions & 0 deletions 8장 - 의존성 관리하기/이준석.md
Original file line number Diff line number Diff line change
@@ -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<DiscountPolicy> 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를 두면 결합도가 낮아지고, 변화를 잘 받아들일 수 있게 될 것이라고 생각했다.

++ 다시 바뀌었다.~~(ㅋㅋㅋ ㅋㅋ ㅋ)~~
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

한번에 답을 내리지 않고 여러번 고민하는 모습 좋습니다 🔥


Size와 음료가 1:1 대응이 되어야 하는데 이를 위해선 음료의 속성으로 Size 인스턴스가 추가되어야 한다. 하지만 Size를 데코레이터 패턴을 사용하여 구현을 하게 되면 Size의 하위 클래스가 음료를 여러번 감쌀 수 있게 된다.

이는 처음 음료당 Size가 하나이어야 한다는 요구사항을 벗어나게 됨으로 잘못된 패턴의 적용이라고 할 수 있다.

이를 제대로 구현하기 위해서는 Size와 음료 사이의 상속관계만 없애주면 된다.
Comment on lines +252 to +256
Copy link
Member

@waterfogSW waterfogSW Nov 13, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

맞아요 저도 교수님 패턴 적용이 이상하다 생각했습니다 👍🏼

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

음료와 Size를 상속관계로 구현하는 것 대신 위에서 언급한 생성자 방식이나 setter 방식을 통해 Size를 넣어주는 것이 좋아보이네요!

"상속보다는 합성을 사용하라" 라는 원칙을 적용하기 좋은 사례인 것 같습니다. 👍