[Object] 08. 의존성 관리
in Programming on Java
잘 설계된 객체지향 어플리케이션은 작고 응집도높은 객체들로 구성된다. 때문에 객체들간 협력은 필수적이다. 협력하기 위해서는 객체끼리 서로 어떤 기능인지 알고있어야한다. 이런 다른 객체에 대한 지식은 객체간 의존성을 낳는다.
협력을 위해서는 의존성이 필수적이지만 과도한 의존성은 유지보수를 어렵게 만든다.
따라서 객체지향의 핵심은 필요한 의존성은 유지하고 변경을 방해하는 의존성은 제거하는것이다.
충분히 협력적이면서 변경을 방해하지않는 유연한 객체를 만ㄷ르기 위한 의존성 관리 방법을 알아보자.
1. 의존성 이해하기
위에서 말했듯 협력을 위해서는 객체 사이의 지식이 필요하게되고 서로 알게되는 순간 의존성이 발생한다.
public class PeriodCondition implements DiscountCondition{
...
public boolean isStatisfiedBy(Screening screening){
...
return screening.isSatisfied();
}
}
PeriodCondition은 Screening객체에 의존하고있고, Screening객체가 변경되면 PeriodCondition객체도 변경된다.
즉 의존성은 변경에 의한 영향의 전파 가능성을 암시한다.
의존성 전이
의존하는 대상이 의존하고있는 객체들에게도 자동으로 의존하게 되는것.
예를들면, Screening이 Movie에 의존하고있다고 하자. 그럼 Screening이 의존하고있는 PeriondCondition도 Movie에게 자동으로 의존하게된다.
PeriondCondition - Screening 처럼 직접 의존하는경우를 직접의존성 이라고하고 Periond - Movie처럼 의존성 전이에 의하 간접적으로 의존하는 경우 간접 의존성 이라한다.
의존성 전이에 의해 영향이 전파된것이다.
런타임 의존성과 컴파일타임 의존성
런타임 : 어플리케이션이 실행되는 시점.
컴파일타임 : 코드를 컴파일하는 시점. (즉, 직접 짠 소스코드 그 자체)
런타임의존성 : 객체 사이의 의존성.
컴파일타임 : 클래스 사이의 의존성.
런타임 의존성과 컴파일 의존성은 다를 수 있다.
abstract class DiscountPolicy{
...
}
public class AmountDiscountPolicy extends DiscountPolicy{
...
}
public class PercentDiscountPolicy extends DiscountPolicy{
...
}
public class Movie{
private DiscountPolicy discountPolicy;
public Movie(String title, Money fee, DiscountPolicy discountPolicy){
...
}
}
Movie 비율할인과 금액할인 모두 가능해야하기때문에 어느 한쪽에만 의존하면 안되고 둘 다와 협력해야한다. 그렇다고 두 클래스 모두에게 의존하는것은 좋은 방법이 아니다. 가장 좋은방법은 위와 같이 Movie가 둘다 알지 못하게 하고 DiscountPolicy라는 추상클래스에 의존하게 한 뒤, 실행시점(런타임)에 구체적인 클래스에 의존하도록 만드는것이다.
이것이 핵심이다. 유연하고 재사용 가능한 설계를 위해서는 동일 소스코드구조로 다양한 실행구조를 만들수 있어야한다.
컴파일구조와 런타임 구조 사이의 거리가 멀수록 설계가 유연하고 재사용 가능해진다.
컨텍스트 독립성
위에서 봤듯이 객체는 자신과 협력할 구체적인 클래스에 대해 알아서는 안된다. 구체적인 클래스를 알면 알수록 특정 문맥에 강하게 결합되기 때문이다.
클래스가 사용될 특정 문맥에 최소한의 가정만으로 이루어지면 다른 문맥에서 재사용하기가 수월해진다.
이것이 바로 컨텍스트 독립성이다.
의존성 해결
컴파일타임의 의존성을 실행 컨텍스트에 맞는 적절한 런타임 의존성으로 교체하는것을 의존성 해결 이라고한다.
- 생성자를 통한 의존성해결
Movie avatar = new Movie("아바타", 3000, new AmountDiscountPolicy());
- setter메소드를 통한 의존성해결
avatar.setDiscountPolicy(new AmountDiscountPolicy());
- 메서드 인자를 이용한 의존성해결
public Money calculateFee(DiscountPolicy discountPolicy){) }
(= 의존성 주입 방법…)
2. 유연한 설계
의존성과 결합도
객체지향의 근간은 협력이다. 따라서 의존성이 나쁜것은 아니다. 다만 의존성이 과하면 문제라는것이다. 의존성을 바람직하게 만들어야 한다.
바람직한 의존성이란 무엇인가? 재사용성과 관련이 있다. 다양한 환경에서 재사용할 수 있다면 그 의존성은 바람직한것이다. 컨텍스트에 독립적인 의존성은 바람직한 의존성이다.
느슨한 결합. 약한 결합일때가 바람직한 의존성일때다.
단단한결합. 강한 결합일때가 바람직하지 못한 의존성일때다.
지식이 결합을 낳는다
결합도는 결국 한 요소가 자신이 의존하고있는 다른 요소에 대해 얼마나 알고있냐에 따라 결정된다. 적게 알수록 결합도가 느슨해진다.
결합도를 느슨하게 만들기 위해서는 협력 대상에 대해 필요한 정보 외에는 최대한 감춰야한다.
이를 위한 가장 효과적인 방법이 바로 추상화이다.
추상화에 의존하라
추상화 : 특정 절차나 물체를 의도적으로 생략하거나 감춰서 복잡도를 극복하는 방법.
추상화를 하여 불필요한 정로블 감춰 지식의 양을 줄여 결합도를 느슨하게 유지할수있다.
명시적 의존성
public class Movie{
public Movie(String title, Money fee, DiscountPolicy discountPolicy){
...
}
}
이렇게 public 인터페이스 생성자에 명시적으로 할인정책을 설정할 수 있도록 노출시킨다면, 이는 명시적 의존성이다.
숨겨진 의존성
public class Movie{
private DiscountPolicy discountPolicy;
public Movie(String title, Money fee){
this.discountPolicy = new AmountDiscountPolicy(800, 10, 20, 30, 40);
}
}
이러한 방식은 Movie가 DiscountPolicy에 의존한다는 사실을 감춰준다. 이를 숨겨진 의존성이라고 한다.
의존성이 명시적이지 않으면 의존성을 파악하기 위해 내부 구현을 직접 살펴볼수밖에 없다.
따라서 의존성은 명시적으로 표현되어야한다.
new는 해롭다
new연산자를 사용하려면 구체 클래스를 직접 구현해야함. 따라서 클래스 사이 결합도가 극단적으로 높아진다.
예를들면 위 숨겨진 의존성 케이스만 봐도, Movie를 선언하기 위해 AmountDiscountPolicy의 모든 파라미터를 다 알아야한다.
따라서 이를 피하기 위해 인스턴스 생성 로직과 사용 로직을 분리해야한다.
사용로직
public class Movie{
public Movie(String title, Money fee, DiscountPolicy discountPolicy){
...
}
}
생성로직(client)
Movie avatar = new Movie("아바타", 3000, new AmountDiscountPolicy());
생성의 책임을 클라이언트로 옮겨서 Movie는 모든 할인정책을 사용할 수 있게되었다. 설계가 유연해진것이다.
가끔은 생성해도 괜찮다
대부분 AmountDiscountPolicy이고 가끔만 PercentDiscountPolicy 라면? 아래가 더 효율적이다..
public class Movie{
public Movie(String title, Money fee){
this(title, fee, new AmountDiscountPolicy());
}
public Movie(String title, Money fee, DiscountPolicy discountPolicy){
...
this.discountPolicy = discountPolicy;
}
}
생성자가 체인처럼 연결되는.. 대부분의 라이브러리/오픈소스에서 볼수있는 패턴 이 방법은 당근 메서드를 오버로딩하는 경우에도 사용 가능.
보면 알겠지만 설계는 트레이드 오프(trade-off) 다.
표준클래스 의존은 괜찮다
표준클래스는 변경이 거의 없으므로 의존해도 괜찮다.
조합 가능한 행동
Movie avatar = new Movie("아바타", 3000, new AmountDiscountPolicy());
이것만 봐도 아바타는 3천원이고 금액할인정책을 적용한다는것을 알 수있음.
유연하고 재사용한 설계는 이렇게 작은 객체들의 행동을 조합하고 새로운 행동을 이끌어 낼 수 있는 설계다.
객체들의 조합을 선언적으로 표현하여 객체들이 무엇을 하는지 표현하는 설계다.
이런 설계의 핵심은 의존성을 관리하는것이다.