목차
- ☆ 표시가 붙은 부분은 스터디 중 나온 얘기 혹은 제 개인적인 생각이나 제가 이해한 방식을 적어놓은 것으로, 책에 나오지 않는 내용입니다. 따라서 책에서 말하고자 하는 바와 다를 수 있습니다.
- 모든 이미지의 출처는 오브젝트(조용호 저) 책 입니다.
CHAPTER 08. 의존성 관리하기
⚈ ☆ 마침 운좋게 스터디에서 8장, 9장이 같은 주차에 들어갔다. 8장과 9장 자체가 연계되는거라 운이 좋았다. 객체에서 자기 자신이 사용할걸 직접 생성하는게 맞는지, 쓰는쪽에서 주입해주는게 맞는지와 유연성있게 짜면 코드가 복잡해지는 느낌이라는게 고민이었다. 8장, 9장에서 그 부분을 설명해줘서 평소 궁금하던 부분이 해결되어 좋았다.
⚈ 8장은 충분히 협력적이면서도 유연한 객체를 만들기 위해 의존성을 관리하는 방법을 살펴본다.
01 의존성 이해하기
⚈ 의존성
- 두 요소 사이의 의존성은 의존되는 요소가 변경될 때 의존하는 요소도 함께 변경될 수 있다는 것을 의미한다.
- 어떤 객체가 협력하기 위해 다른 객체를 필요로 할 때 두 객체 사이에 의존성이 존재
- 의존성은 방향성을 가지며 항상 단방향이다.
- 의존성은 변경에 의한 영향의 전파 가능성을 암시한다.
- 즉, 의존성은 함께 변경될 수 있는 가능성을 의미한다.
⚈ 예를들어 이하와 같이 의존성이 존재할 경우 어떤 형태로든 DayOfWeek, LocalTime, Screening, DiscountCondition이 변경된다면 PeriodCondition도 함께 변경될 수 있다.
⚈ 의존성 전이 (transitive dependency)
- A가 B에 의존하고, B가 C에 의존할 경우 A도 C에 간접적으로 의존하게 된다는 의미
- 실제로 전이될지 여부는 변경의 방향과 캡슐화의 정도에 따라 달라진다. 즉, 의존성 전이는 변경에 의해 영향이 널리 전파될 수도 있다는 경고일 뿐이다.
⚈ 런타임 의존성과 컴파일타임 의존성
- 런타임 : 애플리케이션이 실행되는 시점
- 컴파일타임 : 일반적으로 작성된 코드를 컴파일하는 시점이지만, 여기서는 코드 그 자체를 가리킴.
- 런타임 의존성 : 객체 사이의 의존성
- 컴파일타임 의존성 : 클래스 사이의 의존성
- 런타임 의존성과 컴파일타임 의존성이 다를 수 있다. (☆ 다형성)
- 유연하고 확장 가능한 설계를 만들기 위해서는 컴파일타임 의존성과 런타임 의존성이 달라야 한다.
- e.g. 컴파일타임 의존성(위), 런타임 의존성(아래) : 코드 작성 시점의 Movie 클래스는 할인 정책을 구현한 두 클래스의 존재를 모르지만 실행 시점의 Movie 객체는 두 클래스의 인스턴스와 협력할 수 있게 된다.
⚈ 컨텍스트 독립성
- 클래스는 자신과 협력할 객체의 구체적인 클래스에 대해 알아서는 안 된다. 구체적인 클래스를 알면 알수록 그 클래스가 사용되는 특정한 문맥에 강하게 결합되기 때문이다. 강하게 결합될수록 다른 문맥에서 사용하기는 더 어려워진다.
- 컨텍스트 독립성 : 클래스가 사용될 특정한 문맥에 대해 최소한의 가정만으로 이뤄져 있다면 다른 문맥에서 재사용하기가 더 수월해진다.
⚈ 의존성 해결
- 클래스가 실행 컨텍스트에 독립적인데도 어떻게 런타임에 실행 컨텍스트에 적절한 객체들과 협력할 수 있을까?
- 컴파일타임 의존성은 구체적인 런타임 의존성으로 대체돼야 한다.
- 컴파일타임 의존성을 실행 컨텍스트에 맞는 적절한 런타임 의존성으로 교체하는 것을 의존성 해결이라고 부른다.
⚈ 의존성 해결 방법
- 객체를 생성하는 시점에 생성자를 통해 의존성 해결 (☆스프링 DI가 해결해주는 것)
- ☆ 제일 좋은 방법같다.
- 객체 생성 후 setter 메서드를 통해 의존성 해결 (☆스프링 DI가 해결해주는 것)
- 객체를 생성한 이후에도 의존하고 있는 대상을 변경할 수 있다.
- 의존 대상 설정 전까지는 불완전할 수 있으므로 생성자 방식과 setter 방식을 혼합하는 것이 더 좋다.
- 메서드 실행 시 인자를 이용해 의존성 해결
- 협력 대상에 대해 지속적으로 의존 관계를 맺을 필요 없이 메서드가 실행되는 동만만 일시적으로 의존 관계가 존재해도 무방하거나, 메서드가 실행될 때마다 의존 대상이 매번 달라져야 하는 경우에 유용하다.
02 유연한 설계
⚈ 의존성과 결합도
- 의존성과 결합도는 일반적으로 동의어이지만 서로 다른 관점에서 관계의 특성을 설명하는 용어다.
- 의존성은 두 요소 사이의 관계 유무를 설명한다.(e.g. "의존성이 존재한다.")
- 결합도는 두 요소 사이에 존재하는 의존성의 정도를 상대적으로 표현한다. (e.g. "결합도가 느슨하다")
- 의존성이 바람직할 때(재사용을 쉽게 허용한다면) 두 요소가 느슨한 결합도(loose coupling) 또는 약한 결합도(weak coupling)을 가진다고 말한다.
- 두 요소 사이의 의존성이 바람직하지 못할 때(어떤 의존성이 재사용을 방해할 때) 단단한 결합(tight coupling) 또는 강한 결합도(strong coupling)을 가진다고 말한다.
- 모든 의존성이 나쁜 것은 아니다. 의존성은 객체들의 협력을 가능하게 만드는 매개체라는 관점에서 바람직하지만, 의존성이 과하면 문제가 될 수 있다.
⚈ 지식의 양과 결합도
- 서로에 대해 알고 있는 지식의 양이 결합도를 결정한다.
- 한 요소가 다른 요소에 대해 더 많은 정보를 알고 있을수록 두 요소는 강하게 결합된다.
- 한 요소가 다른 요소에 대해 더 적은 정보를 알고 있을수록 두 요소는 약하게 결합된다.
- 결합도를 느슨하게 만들기 위해서는 협력하는 대상에 대해 필요한 정보 외에는 최대한 감추는 것이 중요하다. -> 추상화
⚈ 추상화와 결합도
- 추상화 : 특정 절차나 물체를 의도적으로 생략하거나 감춤으로써 복잡도를 극복하는 방법
- 대상에 대해 알아야 하는 지식의 양을 줄일 수 있기 때문에 결합도를 느슨하게 유지할 수 있다.
- 의존하는 대상이 더 추상적일수록 결합도는 더 낮아진다.
⚈ 의존 대상 구분 (아래쪽으로 갈수록 알아야 하는 지식의 양이 적어지기 때문에 결합도가 느슨해진다.)
- 구체 클래스 의존성
- 추상(abstract) 클래스 의존성
- 인터페이스(interface) 의존성
⚈ 명시적인 의존성 (explicit dependency)
- 퍼블릭 인터페이스에 의존성을 명시적으로 노출하는 것
- 의존성이 명시적이지 않으면 의존성을 파악하기 위해 내부 구현을 직접 살펴볼 수밖에 없다(숨겨진 의존성(hidden dependency)). 또한 클래스를 다른 컨텍스트에서 재사용하기 위해 내부 구현을 직접 변경해야 한다.
- 의존성을 명시적으로 드러내면 코드를 직접 수정해야 하는 위험을 피할 수 있다. 실행 컨텍스트에서 적절한 의존성을 선택할 수 있기 때문이다.
- 유연하고 재사용 가능한 설계란 퍼블릭 인터페이스를 통해 의존성이 명시적으로 드러나는 설계다.
⚈ new는 해롭다
- new 연산자 사용을 위해 구체 클래스의 이름을 직접 기술해야 한다. new를 사용하는 클라이언트는 추상화가 아닌 구체 클래스에 의존할 수밖에 없기 때문에 결합도가 높아진다.
- new 연산자는 생성하려는 구체 클래스뿐만 아니라 어떤 인자를 이용해 클래스의 생성자를 호출해야 하는지도 알아야 한다. new를 사용하면 클라이언트가 알아야 하는 지식의 양이 늘어나기 때문에 결합도가 높아진다.
- 예를들어 코드가 아래와 같은 경우, Movie가 더 많은 것에 의존하면 의존할수록 점점 더 변경에 취약해진다.
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))));
}
}
⚈ new로 인한 결합도 증가 해결 방법
- 인스턴스를 생성하는 로직과 생성된 인스턴스를 사용하는 로직을 분리해서 해결할 수 있다.
- 위 코드에서 Movie는 인스턴스를 생성해서는 안 된다. 단지 해당하는 인스턴스를 전달받아(생성자, setter, 인자를 통해) 사용하기만 해야 한다.
- 이하는 Movie에서 사용할 DiscountPolicy를 생성하는 책임을 Movie의 클라이언트로 옮기고, Movie에서는 생성된 인스턴스를 사용하는 책임만 남은 코드이다.
public class Movie {
private DiscountPolicy discountPolicy;
public Movie(String title, Duration runningTime, Money fee, DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
}
- 생성의 책임을 클라이언트로 옮김으로써 이제 Movie는 DiscountPolicy의 모든 자식 클래스와 협력할 수 있게 됐다. -> 설계가 유연해졌다.
- 사용과 생성의 책임을 분리하고 생성하는 책임을 클라이언트로 옮김, 의존성을 생성자에 명시적으로 드러냄, 구체 클래스가 아닌 추상 클래스에 의존하게 함 -> 설계가 유연해졌다.
⚈ 가끔은 생성해도 무방하다
- 인스턴스를 직접 생성하는 방식이 유용한 경우도 있다. 주로 협력하는 기본 객체를 설정하고 싶은 경우이다.
- 기본값을 생성하는 메서드와 인스턴스를 인자로 받는 메서드를 함께 사용한다면 클래스의 사용성을 향상시키면서도 다양한 컨텍스트에서 유연하게 사용될 수 있는 여지를 제공할 수 있다.
public class Movie {
private DiscountPolicy discountPolicy;
public Movie(String title, Duration runnintTime, Money fee) {
this(title, runningTime, fee, new AmountDiscountPolicy(...));
}
public Movie(String title, Duration runningTime, Money fee, DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
}
- 단 이건 결합도와 사용성의 트레이드오프 이다. 구체 클래스에 의존하게 되더라도 클래스의 사용성이 더 중요하다면 결합도를 높이는 방향으로 작성할 수 있다.
- 종종 모든 결합도가 모이는 FACTORY를 추가함으로써 사용성과 유연성 두 마리 토끼를 잡을 수 있는 경우도 있다. (9장에 나옴)
- ☆ 클린코드에 생성자 오버로딩이 필요할 시 정적 팩토리 메소드를 쓰라는 말이 있다. 위 예시도 저 방식보다는 정적 팩토리 메소드로 withAmountDiscountPolicy()로 구현하는게 더 좋을 것 같다.
⚈ 표준 클래스에 대한 의존은 해롭지 않다.
- 변경될 확률이 거의 없는 클래스라면 의존성이 문제되지 않는다.
- 이런 클래스들에 대해서는 구체 클래스에 의존하거나 직접 인스턴스를 생성하더라도 문제가 없다. -> 물론 추상적인 타입을 사용하는 것이 확장성 측면에서 유리하다.
Movie의 결합도를 낮춤으로써 무엇이 가능해졌는가?
⚈ Movie를 DiscountPolicy라는 추상화에 의존하게 했고, 생성자를 통해 DiscountPOolicy에 대한 의존성을 명시적으로 드러냈으며, new와 같이 구체 클래스를 직접적으로 다뤄야 하는 책임을 Movie 외부로 옮겼다. -> 유연한 설계 ! -> Movie를 수정하지 않고도 새로운 기능을 추가하는 것이 쉬워진다.
⚈ 할인 혜택을 제공하지 않는 영화의 예매 요금을 계산 -> Movie는 바뀔 필요가 없다.
public class NoneDiscountPolicy extends DiscountPolicy {
@Override
protected Money getDiscountAmount(Screening screening) {
return Money.ZERO;
}
}
...
Movie avatar = new Movie("아바타",
Duration.ofMinutes(120),
Money.wons(10000),
new NoneDiscountPolicy());
⚈ 중복 적용이 가능한 할인 정책 구현 -> Movie는 바뀔 필요가 없다.
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));
}
}
}
...
Movie avatar = new Movie("아바타",
Duration.ofMinutes(120),
Money.wons(10000),
new OverlappedDiscountPolicy(
new AmountDiscountPolicy(...),
new PercentDiscountPolicy(...)));
⚈ 결합도를 낮춤으로써 얻게 되는 컨텍스트의 확장이라는 개념이 유연하고 재사용 가능한 설계를 만드는 핵심이다.
⚈ 유연하고 재사용 가능한 설계는 객체가 어떻게(how) 하는지를 장황하게 나열하지 않고도 객체들의 조합을 통해 무엇(what)을 하는지를 표현하는 클래스들로 구성된다.
'Study > 오브젝트' 카테고리의 다른 글
[오브젝트] 10장. 상속과 코드 재사용 (0) | 2023.01.03 |
---|---|
[오브젝트] 9장. 유연한 설계 (0) | 2022.12.22 |
[오브젝트] 7장. 객체 분해 (0) | 2022.12.08 |
[오브젝트] 6장. 메시지와 인터페이스 (2) | 2022.12.08 |
[오브젝트] 5장. 책임 할당하기 (0) | 2022.12.02 |
댓글