오브젝트(조영호) 책 정리 -8-
협력을 위해 의존성이 필요하지만 과도한 의존성은 애플리케이션을 수정하기 어렵게 만든다. 협력을 위해 필요한 의존성을 유지하면서도 변경을 방해하는 의존성은 제거해야 한다.
어떤 객체가 예정된 작업을 정상적으로 수행하기 위해 다른 객체를 필요로 하는 경우 두 객체 사이에 의존성이 존재한다고 말한다. 의존성은 방향성을 가진다. 의존성은 실행 시점과 구현 시점에 다른 의미를 가진다
- 실행 시점 : 의존하는 객체가 정상적으로 동작하기 위해서는 실행 시에 의존 대상 객체가 반드시 존재해야한다
- 구현 시점 : 의존 대상 객체가 변경될 경우 의존하는 객체도 함께 변경된다.
의존성은 런타임 의존성과 컴파일 타임 의존성이 다를 수 있다.
- 컴파일 타임 의존성 : 코드를 작성하는 시점에서 의존 관계 (클래스 사이의 의존성)
- 런타임 의존성 : 애플리케이션이 실행되는 시점에서 의존 관계 (객체 사이의 의존성)
유연하고 재사용 가능한 설계를 창조하기 위해서는 동일한 소스코드 구조를 가지고 다양한 실행 구조를 만들 수 있어야 한다. 실제로 협력할 객체가 어떤 것인지는 런타임에 해결해야 한다. 클래스가 협력할 객체의 클래스를 명시적으로 들어내고 있다면 다른 클래스 인스턴스와 협력할 가능성 자체가 없어진다. 따라서 컴파일 타임 구조와 런타임 구조 사이의 거리가 멀수록 설계가 유연해지고 재사용 가능해진다.
클래스는 자신과 협력할 객체의 구체적인 클래스에 대해 알아서는 안된다. 구체 클래스에 대해 의존하는 것은 클래스의 인스턴스가 어떤 문맥에 사용될 것인지 구체적으로 명시하는 것과 같다. 구체적인 클래스를 알수록 그 클래스가 사용하는 특정한 문맥에 강하게 결합된다. 클래스가 특정 문맥에 강하게 결합될수록 다른 문맥에서 사용하기 어려워진다.
설계가 유연해지기 위해서는 가능한 자신이 실행될 문맥에 대한 구체적인 정보를 최대한 적게 알아야 한다. 이를 컨텍스트 독립성이라고 한다. 컨텍스트에 대한 정보가 적으면 적을수록 더 다양한 문맥에서 재사용 가능하다.
컴파일 타임 의존성은 실행 컨텍스트에 맞는 적절한 런타임 의존성으로 교체되어야 한다. 이를 의존성 해결이라고 한다.
의존성을 해결하기 위해 세 가지 방법을 사용한다
- 객체 생성하는 시점에 생성자를 통해 의존성 해결
- 객체 생성 후 setter를 통해 의존성 해결
- 매서드 실행 시 인자를 이용해 의존성 해결
클래스의 메서드를 호출하는 대부분의 경우에 매번 동일한 객체를 인자로 전달하고 있다면 생성자와 setter를 혼합한 방식으로 의존성을 지속적으로 유지하는 방식이 좋다. 메서드를 이용하는 방법은 협력 대상에 지속적으로 의존 관계를 맺을 필요 없이 메서드가 실행되는 동안만 일시적인 의존 관계가 필요하거나 매번 의존 관계가 바뀌는 경우 좋다.
추상화에 의존해라
바람직한 의존성을 느슨한 결합을 가진다고 말하며 바람직하지 못할 때 강한 결합을 가진다고 말한다. 결합도를 느슨하게 유지하려면 협력하는 대상에 대해 더 적게 알아야 한다. 따라서 필요한 정보 외에는 추상화하여 최대한 감춘다.
추상화와 결합도 관점에서 다음과 같이 구분한다(아래로 내려 갈수록 결합도 느슨해진다).
- 구체 클래스 의존성
- 추상 클래스 의존성
- 인터페이스 의존성
명시적인 의존성
// 명시적인 의존성
public class Movie{
private DiscountPolicy discountPolicy;
public Movie(..., DiscountPolicy discountPolicy){
this.dicountPolicy = discountPolicy;
}
}
// 명시적이지 않은 의존성
public class Movie{
private DiscountPolicy discountPolicy;
public Movie(...){
this.discountPolicy = new AmountDiscountPolicy(...);
}
}
의존성을 명시적으로 퍼블릭 인터페이스에 노출해라. 의존성이 명시적이지 않으면 의존성을 파악하기 위해 내부 구현을 직접 살펴볼 수밖에 없다. 또한 의존성이 명시적이지 않으면 클래스를 다른 컨텍스트에서 재사용하기 위해 내부 구현을 직접 변경해야 한다. 따라서 의존성은 명시적으로 표현돼야 한다. 의존성을 구현 내부에 숨기지 말아야 한다.
new는 해롭다
결합도 측면에서 new가 해로운 이유는 두 가지이다. 따라서 의존성을 주입받는 방식으로 설계하는 것이 이롭다.
- new 연산자를 사용하기 위해서는 구체 클래스의 이름을 직접 기술해야 한다. 따라서 new를 사용하면 구체 클래스에 의존할 수밖에 없기 때문에 결합도가 높아진다.
- new 연산자는 생성하려는 구체 클래스뿐만 아니라 어떤 인자를 이용해 클래스의 생성자를 호출해야 하는 지도 알아야 한다. 따라서 new를 사용하면 알아야 하는 지식의 양이 늘어난다.
*협력하는 기본 객체를 설정하고 싶을 때, 직접 생성하는 방식이 유용할 수 있다. 설계는 트레이드오프를 가지고 있기 때문에 구체 클래스를 의존하더라도 클래스의 사용성이 더 중요하다면 결합도를 높이는 방향으로 설계할 수 있다.
표준 클래스에 대한 의존은 해롭지 않다
의존성이 불편한 이유는 변경에 민감하기 때문이다. 하지만 변경될 확률이 거의 없는 클래스라면 의존성이 문제가 되지 않는다
훌륭한 객체지향 설계란 객체가 어떻게를 표현하는 것이 아니라 객체들이 협력을 통해 무엇을 하는지 표현하는 설계이다.