✏️, 💡,❓ 해당 이모지는 저의 생각임을 나타냅니다.
과도한 협력은 설계를 곤경에 빠트릴 수 있다. 협력은 객체가 다른 객체에 대해 알 것을 강요한다. 다른 객체와 협력하기 위해서는 그 객체가 존재한다는 사실을 알고 있어야 한다.
01 의존성 이해하기
변경과 의존성
협력할 때 두 객체 사이에 의존성이 존재하게 된다. 의존성은 1) 실행 시점과 2) 구현 시점에 서로 다른 의미를 가진다.
1) 실행 시점: 의존하는 객체가 정상적으로 동작하기 위해서는 실행 시에 의존 대상 객체가 반드시 존재해야 한다.
2) 구현 시점: 의존 대상 객체가 변경될 경우 의존하는 객체도 함께 변경된다.
의존성 전이
의존성은 전이될 수 있다. 이전 'Screening'의 코드를 보았을 때 Screening이 Movie, LocalDateTIme, Customer등에 의존한다는 사실을 알 수 있다.
PeriodCondition이 Screening에 의존할 경우 Screening이 의존하는 대상에 대해서도 자동적으로 의존하게 된다. 물론 Screening이 내부 구현을 효과적으로 캡슐화하고 있다면 Screening에 의존하고 있는 PeriodCondition까지는 변경이 전파되지 않을 것이다. 의존성 전이는 변경에 의해 영향이 널리 전파될 수도 있다는 경고다.
의존성의 종류를 직접 의존성(direct dependency)와 간접 의존성(indirect dependency)로 나누기도 한다.
의존성의 대상은 객체일 수도, 모듈이나 더 큰 규모의 실행 시스템일 수도 있다. 하지만 의존성의 본질은 변하지 않으며, 의존성이랑 의존하고 있는 대상의 변경에 영향을 받을 수 있는 가능성이다.
런타임 의존성과 컴파일타임 의존성
의존성에서 사용하는 런타임과 컴파일타임의 의미를 이해할 필요가 있다.
런타인은 말 그대로 애플리케이션이 실행되는 시점을 가리킨다.
컴파일 타임은 일반적으로 작성된 코드를 컴파일하는 시점이지만, 문맥에 따라서는 코드 그 자체를 가리키기도 한다. 어딘가에서 컴파일타임이라는 용어를 보게 된다면 그것이 정말 컴파일이 진행되는 시점인지 코드를 작성하는 시점인지 파악하는 것이 중요하다.
런타임에서 주인공은 객체다. 런타임 의존성이 다루는 주체는 객체 사이의 의존성이다. 하지만 코드 관점에서 주인공은 클래스다. 따라서 컴파일타임 의존성이 다루는 주체는 클래스 사이의 의존성이다.
중요한 것은 런타임 의존성과 컴파일 의존성이 다를 수 있다는 것이다. (사실 유연하고 재사용 가능한 코드를 위해선 두 종류의 의존성을 다르게 만들어야 한다.)
위에서 Movie 클래스와 AmountDiscountPoliy 클래스는 어떤 의존성도 존재하지 않는다. 하지만 런타임 의존성을 보았을 때, 금액 할인 정책을 적용하기 위해서는 AmountDiscountPoliy와 협력해야 한다.
코드 작성 시점의 Movie 클래스는 할인 정책을 구현한 두 클래스의 존재를 모르지만, 실행 시점의 Movie 객체는 두 클래스의 인스턴스와 협력할 수 있게 된다. 이것이 핵심이다. 유연한 설계를 위해선 동일한 소스코드 구조를 가지고 다양한 실행 구조를 만들 수 있어야 한다.
어떤 클래스의 인스턴스가 다양한 클래스의 인스턴스와 협력하기 위해서는 협력할 인스턴스의 구체적인 클래스를 알아서는 안 된다. 실제로 협력할 객체가 어떤 것인지는 런타임에 해결해야 한다.
컨텍스트 독립성
클래스는 자신과 협력할 객체의 구체적인 클래스에 대해 알면 알수록 그 클래스가 사용하는 특정 문맥에 강하게 결합된다. 구체 클래스에 대해 의존하는 것은 클래스의 인스턴스가 어떤 문맥에서 사용될 것인지를 구체적으로 명시하는 것과 같다.
클래스가 특정 문맥에 강하게 결합될수록 다른 문맥에서 사용하기 어려워진다. 반대로 문맥에 대해 최소한의 가정으로만 있다면 다른 문맥에서 재사용하기가 더 수월해진다. 이를 컨텍스트 독립성이라 부른다.
설계가 유연해지기 위해선 가능한 자신이 실행될 컨텍스트에 대한 구체적 정보를 최대한 적게 알아야 한다.
그럼 클래스가 실행 컨텍스트에 독립적인데도 어떻게 런타임에 실행 컨텍스트에 적절한 객체들과 협력할 수 있을까?
의존성 해결하기
컴파일타임 의존성은 구체적인 런타임 의존성으로 대체되어야 한다. 컴파일타임 의존성을 실행 컨텍스트에 맞는 적절한 런타임 의존성으로 교체하는 것을 의존성 해결이라고 부른다. 의존성을 해결하기 위해선 일반적으로 세 가지 방법을 사용한다.
- 객체를 생성하는 시점에 생성자를 통해 의존성 해결
- 객체 생성 후 setter 메서드를 통해 의존성 해결
- 메서드 실행 시 인자를 이용해 의존성 해결
// 생성자를 통해 의존성 해결
Movie avatar = new Movie("아바타", ..., new AmountDiscountPolicy(...));
publi Movie(String title, ..., DiscountPolicy discountPolicy) {
...
this.discountPolicy = discountPolicy;
}
// 메서드를 통해 의존성 해결
// setter 메서드 이용 방식은 객체를 생성한 이후에도 의존하고 있는 대상을 변경할 수 있는 가능성을 열어놓고 싶은 경우 유용하다.
Movie avatar = new Movie(...);
avatar.setDiscountPolicy(new AmountDiscountPolicy(...));
// 메서드 실행 시 인자를 이용해 의존성 해결
// 항상 할인 정책을 알 필요는 없고 가격을 계산할 때만 일시적으로 알아도 무방하다면 메서드 인자를 이용해 의존성을 해결할 수도 있다.
public class Movie {
public Money calculateMovieFee(..., DiscountPolicy discountPolicy) {
return fee.minus(discountPolicy.calculateDiscountAmount(screening));
}
}
02 유연한 설계
설계를 유연하고 재사용 가능하게 만들기로 결정했다면 의존성을 관리하는 데 유용한 몇 가지 원칙과 기법을 익히자. 먼저 의존성과 결합도의 관계를 살펴보자.
의존성과 결합도
모든 의존성이 나쁜 것은 아니다. 의존성은 객체들의 협력을 가능하게 만드는 매개체라는 관점에서 바람직하다. 하지만 과하면 문제가 되는 것이다.
예시로 Movie가 비율 할인 정책을 구현하는 PercentDiscountPolicy에 직접 의존한다고 해보자. 코드는 Movie가 PercentDiscountPolicy에 의존하고 있다는 사실을 코드를 통해 명시적으로 드러낸다. 이는 문제가 아니다. 오히려 이 의존성이 객체 사이의 협력을 가능하게 만들기 때문에 존재 자체는 바람직하다.
문제는 의존성의 정도다.
이렇게 되면 Movie가 구체적인 클래스를 의존함으로써 다른 종류의 할인 정책이 필요한 문맥에서 Movie를 재사용할 수 있는 가능성이 없어진다.
해결 방법은 의존성을 바람직하게 만드는 것이다. Movie가 협력하고 싶은 대상이 반드시 PercentDiscountPolicy의 인스턴스일 필요는 없다는 사실에 주목하라. 사실 Movie는 자신이 전송하는 calculateDiscountAmount 메시지를 이해할 수 있다면 어떤 타입의 객체와 협력하더라도 상관 없다. 그리고 이는 추상 클래스 타입을 정의함으로써 해결된다.
💡 챕터 8에서는 의존성이 나쁘다가 아닌, 의존성을 잘 만드는 법을 알려주고 있다.
위는 의존성 자체가 나쁜 것이 아니라는 것을 보여준다. 의존성은 협력을 위해 반드시 필요하다. 단지 바람직하지 못한 의존성이 문제인 것이다.
💡 내 생각에 바람직하지 못한 의존성이란, 너무 하나의 상황에만 대응 가능한 의존성이다. 유연성이 낮은 의존성들. 하나의 상황을 위해 만들어진 의존성들.
바람직한 의존성은 재사용성과 관련이 있다. 어떤 의존성이 다양한 환경에서 클래스를 재사용할 수 없도록 한다면 그 의존성은 바람직하지 못한 것이다. 다시 말해 컨텍스트에 독립적인 의존성은 바람직한 의존성이고 특정한 컨텍스트에 강하게 결합된 의존성은 바람직하지 않은 의존성이다. 특정한 컨텍스트에 강하게 의존하는 클래스를 다른 컨텍스트에서 재사용할 수 있는 유일한 방법은 구현을 변경하는 것 뿐이다.
바람직한 의존성과 바람직하지 못한 의존성을 가리키는 더 세련된 용어가 있는데, 바로 결합도이다. 의존성이 바람직할 때 두 요소는 느슨한 결합도(loose coupling / weak coupling)을 가진다고 하고, 의존성이 바람직하지 못할 때 강한 결합도(strong coupling / tight coupling)을 가진다고 한다.
일반적으로 의존성과 결합도를 동의어로 사용하지만 둘인 서로 다른 관점에서 관계의 특성을 설명하는 용어다.
'의존성'은 두 요소 사이의 관계 유무를(의존성이 존재한다) 설명하고, 결합도는 의존성의 정도를 상대적으로 표현(결합도가 강하다) 표현한다.
지식이 결합을 낳는다
결합도의 정도는 한 요소가 자신이 의존하고 있는 다른 요소에 대해 알고 있는 정보의 양으로 결정된다.
더 많이 알수록 더 많이 결합된다. 더 많이 알고 있다는 것은 더 적은 컨텍스트에서 재사용 가능하다는 것이다. 결합도를 느슨하게 만들려면 협력하는 대상에 대해 필요한 정보 외에는 최대한 감추는 것이 중요하다.
우리는 이를 달성하는 가장 효과적인 방법에 대해 알고 있는데, 바로 추상화이다.
추상화에 의존하라
추상화란 세부사항, 구조를 더 명확하게 이해하기 위해 특정 절차나 물체를 의도적으로 생략하거나 감춤으로써 복잡도를 극복하는 방법이다. 추상화를 사용하면 불필요한 정보를 감출 수 있다. (정보를 줄일수록 결합도가 낮아진다. 의존하는 대상이 더 추상적일 수록 결합도는 더 낮아진다.)
일반적으로 추상화와 결합도의 관점에서 의존 대상을 다음과 같이 구분하는 것이 유용하다.
- 구체 클래스 의존성
- 추상 클래스 의존성: 내부 구현과 자식 클래스 종류를 숨길 수 있다.
- 인터페이스 의존성: 상속 계층을 모르더라도 협력이 가능해진다.
명시적인 의존성
생성자의 인자로 의존성의 대상을 전달받는 방법은 Movie가 DiscountPolicy에 의존한다는 사실을 Movie의 퍼블릭 인터페이스에 드러내는 것이다. (setter 메서드를 사용하는 방식과 메서드 인자를 사용하는 방식도 동일하다.) 이를 명시적인 의존성(explicit dependency)라고 부른다.
반면 숨겨진 의존성도 있는데, Movie에서 AmountDiscountPolicy를 직접 생성하는 것을 말한다. (좋지 않다.)
의존성이 명시적이지 않으면 의존성을 파악하기 위해 내부 구현을 직접 살펴볼 수밖에 없다. 그리고 클래스를 다른 컨텍스트에서 재사용하기 위해 내부 구현을 직접 변경해야 한다.
그러므로 의존성을 명시적으로 표현돼야 한다. 의존성을 밝은 곳으로 드러내서 널리 알려라.
new는 해롭다
new를 잘못 사용하면 클래스 사이의 결합도가 극단적으로 높아지는데, 결합도 측면에서 new가 해로운 이유는 크게 두 가지다.
- new 연산자를 사용하려면 구체 클래스의 이름을 직접 기술해야 한다.
- new 연산자는 어떤 인자를 이용해 클래스의 생성자를 호출해야 하는지 알아야 한다. 클라이언트가 알아야 하는 지식의 양이 늘어난다.
❓ 그런데 이건 인스턴스를 생성하려면 어쩔 수 없는 거 아닌가...?
💡 아하 여기서 말하는 건 사용하는 곳에서 new를 호출하면 안된다는 말이었다.
인스턴스를 생성하는 로직과 생성된 인스턴스를 사용하는 로직을 분리해야 한다. AmountDiscountPolicy를 사용하는 Movie는 인스턴스를 생성해서는 안 된다. Movie는 외부로부터 이미 생성된 AmountDiscountPlicy의 인스턴스를 전달받아야 한다.
가끔씩은 생성해도 무방하다
클래스 안에서 객체의 인스턴스를 직접 생성하는 방식이 유용한 경우도 있다. 주로 협력하는 기본 객체를 설정하고 싶은 경우이다. 예를 들어 Movie가 대부분은 AmountDiscountPolicy 인스턴스와 협력하고 가끔씩만 PercentDiscountPolicy의 인스턴스와 협력한다고 가정해보자. 이 상황에서 모든 경우 인스턴스를 생성하는 책임을 클라이언트로 옮기면 클라이언트들 사이에 중복 코드가 늘어나고 Movie의 사용성도 나빠질 것이다.
이 문제를 해결하는 방법은 기본 객체를 생성하는 생성자를 추가하고 이 생성자에서 DiscountPolicy의 인스턴스를 인자로 받는 생성자를 체이닝 하는 것이다.
public class Movie {
...
private DiscountPolicy discountPolicy;
public Movie(String title, Duration runningTime) {
this(title, runningTime, fee, new AmountDiscountPolicy(...));
}
public Movie(String title, Duration runningTime, Money fee, DiscountPolicy discountPolicy) {
...
this.discountPolicy = discountPolicy;
}
}
이 예는 설계가 트레이드오프 활동이라는 사실을 상기시킨다. 여기서 트레이드 오프의 대상은 결합도와 사용성이다. 이렇게 된다면 결합도가 높아지지만 사용성도 높아진다.
표준 클래스에 대한 의존은 해롭지 않다
의존성이 불편한 이유는 그것이 항상 변경에 대한 영향을 암시하기 대문이다. 변경될 확률이 거의 없는 클래스라면 의존성이 문제가 되지 않는다. 자바라면 JDK에 포함된 표즌 클래스가 이 부류에 속한다. 예를 들어 ArrayList의 경우 인스턴스를 직접 생성해도 문제가 되지 않는다. (변경될 확률이 매우 적기 때문에)
조합 가능한 행동
다양한 종류의 할인 정책이 필요한 컨텍스트에서 Movie를 재사용할 수 있는 이유는 코드를 직접 수정하지 않고도 협력 대상인 DiscountPolicy 인스턴스를 교체할 수 있었기 때문이다.
유연하고 재사용가능한 설계는 객체가 어떻게(how) 하는지를 장황하게 나열하지 않고도 객체들의 조합을 통해 무엇(what)을 하는지를 표현하는 클래스들로 구성된다. 따라서 클래스의 인스턴스를 생성하는 코드를 보는 것만으로 객체가 어떤 일을 하는지 쉽게 파악할 수 있다.
위 코드를 읽는 것만으로도 객체의 행동을 이해할 수 있다. 선언적으로 객체의 행동을 정의할 수 잇는 것이다. 그리고 인자를 변경하는 것만으로도 새로운 할인 정책과 할인 조건을 적용할 수 있다는 것 역시 알 수 있다.
new Movie("아바타",
...,
new AmountDiscountPolicy(Money.wons(800),
new SequenceCondition(1),
new SequenceCondition(10),
new PeriodCondition(DayOfWeek.MONDAY...));
훌륭한 객체지향 설계란 객체가 어떻게 하는지를 표현하는 것이 아니라 객체들의 조합을 선언적으로 표현함으로써 객체들이 무엇을 하는지 표현하는 설계다.
마무리하며
8챕터는 교과서를 읽는 느낌이었다. 의존성과 결합도에 대해 확실히 알 수 있었다. (둘의 명확하게 차이를 짚어준 부분이 좋았다.)
9챕터는 팩토리 패턴에 대해 알려준다고 하는데 공감되는 코드가 많이 나올 것 같아서 기대가 된다.
'Book' 카테고리의 다른 글
[오브젝트] 챕터 07: 객체 분해 (2) | 2025.06.04 |
---|---|
[오브젝트] 챕터 06: 메시지와 인터페이스 (0) | 2025.05.21 |
[오브젝트] 챕터 05: 책임 할당하기 (0) | 2025.05.21 |
[오브젝트] 챕터 04: 설계 품질과 트레이드오프 (0) | 2025.05.04 |
[오브젝트] 챕터 03: 역할, 책임, 협력 (0) | 2025.05.02 |