8장 의존성 관리하기

객체지향 설계의 핵심은

협력을 위해 필요한 의존성은 유지하면서 변경을 방해하는 의존성은 제거하는데 있다.

이런 관점에서 객체지향 설계란?

  • 의존성을 관리하는 것
  • 객체가 변화를 받아들일 수 있게 의존성을 정리하는 기술

변경과 의존성

어떤 객체가 협력하기 위해 다른 객체를 필요로 할 때 두 객체 사이에 의존성이 존재하게 된다.

이러한 의존성은 실행 시점과 구현시점에 서로 다른 의미를 가진다.

  • 실행 시점 - 의존하는 객체가 정상적으로 동작하기 위해선 실행 시에 의존 대상 객체가 반드시 존재해야 한다.
  • 구현 시점 - 의존 대상 객체가 변경될 경우 의존하는 객체도 함께 변경한다.
public class PeriodCondition implements DiscountCondition {
    private DayOfWeek dayOfWeek;
    private LocalTime startTime;
    private LocalTime endTime;

    ...

    @Override
    public boolean isSatisfiedBy(Screening screening) {
        return dayOfWeek.equals(screening.getWhenScreened().getDayOfWeek())
                && startTime.compareTo(screening.getWhenScreened().toLocalTime()) <= 0
                && endTime.compareTo(screening.getWhenScreened().toLocalTime()) >= 0;
    }
}

실행시점 - PeriodCondition의 인스턴스가 제대로 동작하기 위해선 Screening의 인스턴스가 존재해야 한다. 의존성은 방향성을 가지는데 항상 단뱡향이다.

따라서 Screening이 변경될 때 PeriodCondition이 영향을 받게 되지만 그 반대는 성립되지 않는다.

 

두 요소 사이에 의존되는 요소가 변경될 때 의존하는 요소도 함께 변경될 수 있다는 것을 의미하기에 의존성은 변경에 의한 영향의 전파 가능성을 암시한다.

 

위의 코드를 살펴보면 어떤 형태로든 DayOfWeek, LocalTime, Screening, DiscountCondition이 변경된다면 PeriodCondition도 함께 변경될 수 있다.[의존성을 가진다.]

  • 인스턴스 변수: DayOfWeek, LocalTime
  • 메서드 인자: Screening
  • 실체화[구현] : DiscountCondition <interface>

의존성 전이

의존성은 전이될 수 있다.

의존성 전이(transitice dependency): 자신이 어떤 객체에 의존하고 있을 때, 그 의존하는 객체가 가지고 있는 또 다른 객체와의 의존성까지 전이된다.

다만, 의존성은 함께 변경될 수 있는 가능성을 의미하기에 항상 의존성이 전이되는 것은 아니다.

의존성 전이는 변경에 의해 영향이 널리 전파될 수도 있다는 경고일 뿐이다.

  • 직접 의존성: 한 요소가 다른 요소에 직접 의존하는 경우
  • 간접 의존성: 직접적인 관계는 아니지만 의존성 전이에 의해 영향이 전파되는 경우

의존성은 클래스뿐만 아니라 객체, 모듈, 큰 규모의 시스템에서도 같은 개념으로 적용하며, 의존하고 있는 대상의 변경에 영향을 받을 수 있는 가능성이다.

런타임 의존성과 컴파일타임 의존성

  • 런타임 : 실행되는 시점
  • 컴파일타임: 작성된 코드를 컴파일하는 시점 혹은 코드 그 자체
    • 컴파일타임 의존성은 자바의 경우 작성한 코드의 구조를 중요시한다.

객체지향 애플리케이션에서

런타임의 주인공은 객체이며, 그렇기에 런타임 의존성이 다루는 주제는 객체 사이의 의존성이다.

컴파일[=코드]의 주인공은 클래스이며, 그렇기에 컴파일타임 의존성이 다루는 주제는 클래스 사이의 의존성이다.

 

런타임 의존성과 컴파일타임 의존성이 다를 수 있다는 사실이 중요하다. 더불어 유연하고 재사용 가능한 코드를 설계하기 위해서는 두 종류의 의존성을 서로 다르게 만들어야 한다.

 

예시 코드를 보면서 추가적인 설명

public class Movie {
    ...
    public Movie(String title, Duration runningTime, Money fee, DiscountPolicy discountPolicy) {
				...
        this.discountPolicy = discountPolicy;
    }

    public Money calculateMovieFee(Screening screening) {
        return fee.minus(discountPolicy.calculateDiscountAmount(screening));
    }
}  

영화 예매 시스템에서 Movie는 가격을 계산하기 위해 비율 할인 정책(AmountDiscountPolicy)과 금액 할인 정책(PercentDiscountPolicy) 모두를 적용할 수 있게끔 설계해야 한다.

즉, 둘 다 협력할 수 있어야 한다.

이를 위해 추상 클래스인 DiscountPolicy를 상속받게 한 후 Movie가 해당 추상 클래스에 의존하도록 클래스 관계를 설계했다.

이때 중요한 것은 클래스 관계에선 Movie 클래스가 추상클래스에만 의존할 뿐 하위클래스(AmountDiscountPolicy, PercentDiscountPolicy)에는 의존하지 않는다는 것이다.

하지만 런타임 의존성을 살펴보면 상황이 달라진다. 각 할인 정책을 적용하기 위해선 하위 클래스의 인스턴스와 협력해야 한다.

 

정리하면 코드를 작성하는 시점의 Movie 클래스는 하위 클래스(AmountDiscountPolicy, PercentDiscountPolicy)의 존재에 대해 알지 못하지만 실행 시점에서는 하위 클래스의 인스턴스와 협력할 수 있어야 한다.

 

다시 말해 두 클래스를 모두 포괄하는추상 클래스(DiscountPolicy)에 의존하게끔 하여 컴파일타임 의존성을 실행 시에 하위 클래스(AmountDiscountPolicy, PercentDiscountPolicy) 인스턴스에 대한 런타임 의존성으로 대체하는 것이 핵심이다.

유연하고 재사용 가능한 설계를 위해서는 동일한 소스코드 구조를 가지고 다양한 실행 구조를 만들어야 한다.

 

어떤 클래스의 인스턴스가 다양한 클래스의 인스턴스와 협력하기 위해서는 협력할 인스턴스의 구체적인 클래스를 알아서는 안 되며, 실제로 협력할 객체가 어떤 것인지는 런타임에서 해결해야 한다.

클래스가 협력할 객체의 클래스를 명시적으로 드러낸다면 다른 클래스의 인스턴스와 협력할 가능성이 아예 없어지기에 런타임 구조와 컴파일타임 구조 사이의 거리가 멀수록 설계가 유연해지고 재사용 가능해진다.

 

컨텍스트 독립성

컨텍스트 독립성 : 클래스가 사용될 특정한 문맥에 대해 최소한의 가정으로만 이뤄져 있다면 다른 문맥에 재사용하기 수월해진다.

컨텍스트 독립적: 각 객체가 해당 객체를 실행하는 시스템에 관해 아무것도 알지 못한다.

유연하고 확장 가능한 설계를 만드려면 컴파일타임 의존성과 런타임 의존성이 달라야한다는 걸 알 수 있었다. 클래스는 자신과 협력할 객체의 구체적인 클래스에 대해 알면 알수록 해당 클래스가 사용되는 특정한 문맥에 강하게 결합되기에 구체적인 클래스에 알면 안된다.

구체 클래스에 대해 의존하는 것은 클래스의 인스턴스가 어떤 문맥에 사용되는지를 구체적으로 명시하는 것과 같다.

public class Movie {
    ...
    public Movie(String title, Duration runningTime, Money fee) {
				...
        this.discountPolicy = **new AmountDiscountPolicy(...)**; // 구체적인 명시
    }
}  

이렇게 특정 문맥에 강하게 결합될수록 다른 문맥에서 재사용하기 더 어려워진다.

따라서 설계를 유연하게 만들기 위해선 가능한 자신이 실행될 컨텍스트에 대한 구체적인 정보를 최대한 적게 알아야 한다.

그렇다면?! 클래스가 실행 컨텍스트에 독립적인데도 불구하고 어떻게 런타임에 실행 컨텍스트에 적절한 객체들과 협력이 가능한 것일까??

의존성 해결하기

컴파일타임 의존성은 구체적인 런타임 의존성으로 대체돼야 한다고 계속 언급했다. 이를 의존성 해결이라고 한다.

의존성 해결: 컴파일타임 의존성을 실행 컨텍스트에 맞는 적절한 런타임 의존성으로 교체하는 것

의존성을 해결하기 위해선 일반적으로 세 가지 방법을 사용한다.

  1. 객체를 생성하는 시점에 생성자를 통한 의존성 해결 [클래스 내에서 계속 사용할 경우]
  2. 객체 생성 후 setter 메서드를 통한 의존성 해결 [클래스 내에서 계속 사용할 경우]
  3. 메서드 실행 시 인자를 이용해 의존성 해결 [메서드 내에서만 해당 객체를 사용할 경우]
  1. 생성자를 통한 의존성 해결
// 생성자를 통한 의존성 해결 
public class Movie {
    ...
    public Movie(..., DiscountPolicy discountPolicy) {
				...
        this.discountPolicy = discountPolicy;
    }
}  

// 금액 할인 정책을 적용하고 싶을 때
Movie movie = new Movie(..., new AmountDiscountPolicy(...));

// 비율 할인 정책을 적용하고 싶을 때
Movie movie = new Movie(..., new PercentDiscountPolicy(...));

이처럼 Movie 클래스가 각각의 인스턴스를 선택적으로 전달받을 수 있도록 두 클래스의 상위 클래스인 DiscountPolicy 타입의 인자를 받는 생성자를 정의하는 방법이 생성자를 통한 의존성 해결이다.

 

 2. setter 메서드를 통한 의존성 해결

public class Movie {
	public void setDiscountPolicy (DiscountPolicy discountPolicy) {
		this.discountPolicy = discountPolicy;
	}
}

Moive movie = new Movie(...);
movie.setDiscountPolicy(new AmountDiscountPolicy(...));

실행시점에 의존 대상을 변경할 수 있기 때문에 설계를 좀 더 유연하게 만들 수 있다.

객체가 생성된 후에 협력에 필요한 의존 대상을 설정하기 때문에 객체를 생성하고 의존 대상을 설정하기 전까지는 객체의 상태가 불완전할 수 있다.

Movie movie = new Moive(...);
movie.calculateFee(...); // NullPointerException 예외 발생 [객체의 상태가 불완전]
avator.setDiscountPolicy(new AmountDiscountPolicy(...));

항상 객체를 생성할 때 의존성을 해결해서 완전한 상태의 객체를 생성한 후[:생성자 의존성 해결],

필요에 따라 setter 메서드를 이용해 의존 대상을 변경할 수 있게끔 하여[:setter 의존성 해결] 시스템의 상태를 안정적으로 유지하면서 유연성을 향상시킬 수 있는 생성자 방식과 setter를 혼합하는 방법이 의존성 해결에 가장 선호되는 방법이다.

 

 3. 메서드 인자를 이용한 의존성 해결

public class Movie {
	public Money calculateMovieFee(Screening screening, DiscountPolicy discountPolicy) {
		return fee.minus(discountPolicy.calculateDiscountAmount(screening);
	}
}

항상 클래스 내에서 의존성이 필요하지 않고 메서드 내에서만 의존할 필요가 있을 경우에 사용해도 되는 방법이다.

협력 대상에 대해 계속 의존 관계를 맺을 필요가 없고 메서드 내에서만 일시적으로 의존 관계가 존재해도 무방하거나, 메서드가 실행될 때마다 의존 대상이 매번 달라져야 하는 경우에 유용하다.


의존성과 결합도

객체지향 패러다임의 핵심은 협력이다.

객체들이 협력하기 위해서는 서로의 존재와 수행가능한 책임을 알아야 한다.

이렇게 알아야 하는 지식들이 객체 사이의 의존성을 낳는다. 의존성이 무조건 나쁜 것은 아니며 객체들의 협력을 가능하게 하는 매개체라는 관점에서는 좋다. 하지만 과하면 문제가 된다.

public class Movie {
    ...
    private PercentDiscountPolicy percentDiscountPolicy;

    public Movie(String title, Duration runningTime, Money fee, PercentDiscountPolicy percentDiscountPolicy) {
        ...
				this.percentDiscountPolicy = percentDiscountPolicy
    }

    public Money calculateMovieFee(Screening screening) {
        return fee.minus(percentDiscountPolicy.calculateDiscountAmount(screening));
    }
}

Moive가 현재 PercentDiscountPolicy를 의존하고 있다는 걸 코드에서 명시적으로 보여준다.

의존성이 무조건 나쁜 것이 아니라고 했듯이 Movie와 PercentDiscountPolicy 사이에 의존성이 존재하는 것이 문제가 되지 않는다. 문제는 의존성의 존재가 아닌 의존성의 정도이다.

 

구체적인 클래스에 의존하게 만들었기에 다른 종류의 구체 클래스가 필요한 문맥에서 재사용할 수 없게 된다.이를 해결할 방법은 의존성을 바람직하게 만드는 것이다. 구체 클래스가 아닌 할인 정책인 추상 클래스 DiscountPolicy에 각각의 구체클래스가 메시지를 이해할 수 있는 타입을 정의하는 것이다.

그렇게 하여 Movie 클래스는 오직 DiscountPolicy 추상 클래스와 의존하게 만들어서 컴파일타임 의존성을AmountDiscountPolicy 인스턴스와 PercentDiscountPolicy 인스턴스에 대한 런타임 의존성으로 대체할 수 있다.

 

결국 의존성의 존재 자체가 나쁜 것이 아니며, 협력을 위해선 반드시 의존성이 필요하지만, 의존성의 정도가 문제가 되는 것이다.

따라서 다양한 환경에서 클래스를 재사용할 수 있도록 바람직한 의존성을 만드는 것이 중요하다.

 

특정한 컨텍스트에 강하게 의존하는 클래스를 다른 컨텍스트에서 재사용할 수 있는 유일한 방법은 구현을 변경하는 것 뿐이다. 이는 코드를 수정해야만 하며 바람직하지 못한 의존성을 또다른 바람직하지 못한 의존성으로 바뀌는 것에 불과하다.

 

바람직한 의존성: 컨텍스트에 독립적인 의존성을 의미, 다양한 환경에서 재사용될 수 있는 가능성을 열어놓는 의존성을 의미

 

의존성 vs 결합도

서로 다른 관점에서 관계의 특성을 설명한다.

의존성: 두 요소 사이의 관계 유무

의존성이 존재한다. 존재하지 않는다.

 

결합도: 두 요소 사이에 존재하는 의존성의 정도를 상대적으로 표현한다.

결합도가 강하다. 느슨하다.

 

바람직한 의존성 = 느슨한 결합도(loose coupling), 약한 결합도(weak coupling)

바람직하지 못한 의존성 = 단단한 결합도(tight coupling), 강한 결합도(strong coupling)

이렇게 연결지어 생각하자.

 

자식이 결합을 낳는다.

결합도의 정도는 한 요소가 자신이 의존하고 있는 다른 요소에 대해 알고 있는 정보의 양으로 결정된다. 한 요소가 다른 요소에 대해 더 많은 정보를 알수록 두 요소는 강하게 결합된다.

 

더 많이 알수록 더 많이 결합된다는 것은 더 적은 컨텍스트에서 재사용 가능하다는 것을 의미한다. 기존 지식에 어울리지 않는 컨텍스트에다가 클래스의 인스턴스를 사용하기 위해선 클래스를 수정할 수 밖에 없게 된다.

 

결합도를 느슨하게 만들기 위해서는 협력하는 대상에 대해 필요한 정보외에는 최대한 감추는 것이 중요하다. 가장 효과적인 방법은 바로 추상화다!

추상화에 의존하라

추상화: 어떤 양상, 세부사항, 구조를 좀 더 명확하게 이해하기 위해 특정 절차나 물체를 의도적으로 생략하거나 감춤으로써 복잡도를 극복하는 방법이다.

불필요한 정보를 감출 수 있기에 대상에 대한 알아야 하는 지식의 양이 줄어들고 이는 결합도를 느슨하게 한다.

  • 구체 클래스 의존성
  • 추상 클래스 의존성
  • 인터페이스 의존성

(점점 알아야 하는 지식이 줄어들기에 결합도가 느슨해진다.)

추상 클래스의 클라이언트는 협력하는 대상이 속한 클래스 상속 계층이 무엇인지 알고 있어야 한다.

인터페이스 의존성은 협력하는 객체가 어떤 메시지를 수신할 수 있는지에 대한 지식만을 남기기 때문에 추상 클래스 의존성보다 결합도가 낮다.

실행 컨텍스트에 대해 알아야 하는 정보를 줄일수록 결합도가 낮아진다는 것이 중요하다.

즉, 의존하는 대상이 더 추상적일수록 결합도는 낮아진다.

명시적인 의존성

결합도를 느슨하게 만들기 위해서 단순히 인스턴스 변수의 타입을 추상 클래스나 인터페이스로 선언한다고 끝나는 것이 아니다.

클래스 안에서 구체 클래스에 대한 모든 의존성을 제거해야만 한다. 이를 앞서 말했듯 의존성 해결이라하고 생성자, setter 메서드, 메서드 인자를 이용하는 세가지 방식이 있다고 했다.

public class Movie {
    ...
		private DiscountPolicy discountPolicy; // 인스턴스 변수 타입 => 추상클래스 

    public Movie(..., DiscountPolicy discountPolicy) { // 생성자의 인자 추상클래스타입
				...
        this.discountPolicy = discountPolicy; // 생성자를 통한 의존성 해결  
    }
}  

이 때 인스턴스 변수의 타입은 추상 클래스나 인터페이스로 정의하고, 의존성 해결할 때는 추상 클래스를 상속받거나 인터페이스를 실체화한 구체 클래스를 전달하는 것이다.

 

의존성의 대상을 생성자의 인자로 전달받는 방법과 생성자 안에서 직접 생성하는 방법 사이의 가장 큰 차이점은 퍼블릭 인터페이스를 통해 할인 정책을 설정할 수 있는 방법을 제공하는지 여부이다.

 

생성자의 인자로 선언하는 방법은 Movie가 DiscountPolicy에 의존한다는 사실을 Movie의 퍼블릭 인터페이스에 드러내는 것이다. 의존성이 명시적으로 퍼블릭 인터페이스에 노출된다. 이를 **명시적인 의존성(explicit dependency)**라고 한다.

 

Movie의 내부에서 AmountDiscountPolicy의 인스턴스를 직접 생성하는 방식은 Movie가 DiscountPolicy에 의존한다는 사실을 감추고 이는 의존성이 퍼블릭 인터페이스에 표현되지 않게 된다. 이는 **숨겨진 의존성(hidden dependency)**이라고 한다.

 

의존성을 명시적으로 표현하지 않은[=숨겨진 의존성]경우 문제점들은 다음과 같다.

의존성이 명시적이지 않으면 의존성을 파악하기 위해 내부 구현 코드를 직접 살펴볼 수 밖에 없다.

클래스를 다른 컨텍스트에서 재사용하기 위해서 내부 구현을 직접 변경해야 한다.

 

따라서 의존성을 구현 내부에 숨기지말고 퍼블릭 인터페이스를 통해 의존성이 명시적으로 드러내도록 설계해야한다. 더불어 명시적 의존성을 사용해야만 퍼블릭 인터페이스를 통해 컴파일타임 의존성을 런타임의존성으로 교체할 수 있다.

new는 해롭다

이유는 구체 클래스를 알아야 하기 때문이다.

new를 잘못 사용하면 클래스 사이의 결합도가 극단적으로 높아진다.

이유는?

  • new 연산자를 사용하기 위해 구체 클래스의 이름을 직접 써야한다. 이는 new를 사용하는 클라이언트가 추상화가 아닌 구체 클래스에 의존할 수 밖에 없기에 결합도가 높아진다.
  • new 연사나자는 생성하려는 구체 클래스뿐만 아니라 어떤 인자를 이용해 클래스의 생성자를 호출해야하는지도 알아야 한다. 따라서 new를 사용하면 클라이언트가 알아야 하는 지식의 양이 늘어나기에 결합도가 높아진다.

결론은 구체 클래스에 직접 의존하면 결합도가 높아진다는 점을 기억하고 있어야 한다.

결합도 관점에서 구체 클래스는 협력자에게 많은 지식을 알게끔 강요한다. 더불어 new는 문제를 더 크게 만든다. 클라이언트는 구체 클래스를 생성하는데 어떤 정보가 필요한지도 알아야 하기 때문이다.

public class Movie {
    ...
		private DiscountPolicy discountPolicy;

    public Movie(..., DiscountPolicy discountPolicy) { 
				...
        this.discountPolicy = new AmountDiscountPolicy(Money.wons(800),
															new SequenceCondition(1)
															...);
    }
}  

Movie 클래스가 AmountDiscountPolicy의 인스턴스를 생성하기 위해 생성자에 전달되는 인자를 전부 알고 있어야 한다. 이는 당연히 Movie 클래스가 알아야하는 지식을 늘리며 결국 AmountDiscountPolicy에게 더 강하게 결합되게 한다. 더불어 SequenceCondition 구체 클래스도 의존하게 만든다. 이 또한 Movie를 결합시킨다.

 

단순히 AmountDiscountPolicy의 생성자의 인자 목록이나 순서를 바꾸는 경우에도 Movie의 코드를 변경해야한다. 즉, Movie가 더 많은 것에 의존할수록 점점 더 변경에 취약해진다. 그렇기에 높은 결합도를 피해야한다.

 

결국 new는 구체 클래스에 결합시키는 것만으로 해로움이 끝나지 않고 협력할 클래스의 인스턴스를 생성하기 위해 어떤 인자들이 필요하고 사용해야하는지에 대한 정보도 노출시키며 인자로 사용되는 구체 클래스에 대한 의존성을 추가시킨다. 이 모든 것은 결국 결합도를 높이기에 좋지 않다.

 

해결방법은 인스턴스를 생성하는 로직과 생성된 인스턴스를 사용하는 로직을 분리하는 것이다.

AmountDiscountPolicy를 사용하는 Movie는 인스턴스를 생성하지 않고 단순히 사용만 하는 것이다.

이를 위해 Movie는 외부로부터 이미 생성된 AmountDiscountPolicy의 인스턴스를 전달받아야 한다.

외부에서 인스턴스를 전달받는 방법은 의존성 해결 방법과 같다.

3가지 방법 중 어떤 방법을 사용하든 간에 Movie클래스에는 AmountDiscountPolicy의 인스턴스에 메시지를 전송하는 코드만 남아있게끔 한다.

 

그렇다면 누가 AmountDiscountPolicy의 인스턴스를 생성해야할까?

이것은 Movie의 클라이언트가 처리한다. AmountDiscountPolicy의 인스턴스를 생성하는 책임을 Movie의 클라이언트로 옮겨서 단순히 Movie는 사용하는 책임만 남게끔하여 사용과 생성의 책임을 분리하고 결합도를 낮춰 설계를 유연하게 만들 수 있다.

이때 주의할 점은, Movie의 생성자가 구체클래스가 아닌 추상클래스(DiscountPolicy)를 인자로 받아들이게끔 해야한다. 그래야만 생성의 책임을 클라이언트에 옮기고 Movie는 DiscountPolicy 추상클래스의 모든 자식 클래스와 협력할 수 있게 된다.

 

정리하면, 사용과 생성의 책임을 분리하고, 의존성을 생성자에 명시적으로 드러내며, 구체 클래스가 아닌 추상클래스에 의존하게 하여 결합도를 낮추고 설계를 유연하게 만들 수 있다. 더불어 핵심은 객체를 생성하는 책임을 객체 내부가 아니라 클라이언트로 옮겨야 한다는 점이다.

가끔은 생성해도 무방하다.

클래스 안에 객체의 인스턴슬ㄹ 직접 생성하는 방식이 유용할 때도 있다.

주로 협력하는 기본 객체를 설정하고 싶을 때다.

 

방법은 기본 객체를 생성하는 생성자를 추가하고 이 생성자에서 기본 객체를 인자로 받는 생성자를 체이닝하는 것이다.

public class Movie {
    ...
		private DiscountPolicy discountPolicy;

    public Movie(..., DiscountPolicy discountPolicy) {  // 기본 객체를 생성하는 생성자 추가
				this(title, runningTime, fee, new AmountDiscountPolicy(...)); // 체이닝			
    }

    public Movie(..., DiscountPolicy discountPolicy) { 
				...
        this.discountPolicy = discountPolicy,					
    }
}  

메서드를 오버로딩할 때도 사용할 수 있다.

public class Movie {
    public Money calculateMovieFee(Screening screening) {
        return calculateMovieFee(screening, new AmountDiscountPolicy);
    }

    public Money calculateMovieFee(Screening screening, DiscountPolicy discountPolicy) {
        return fee.minus(percentDiscountPolicy.calculateDiscountAmount(screening));
    }
}

다만 이 방법들은 설계가 트레이드오프가 있다는 걸 알려준다. 트레이드 오프의 대상은 결합도와 사용성이다. 구체 클래스에 의존하더라도 클래스의 사용성이 더 중요하다면 결합도를 높이는 방법으로 코드를 작성할 수 있다.

표준 클래스에 대한 의존은 해롭지 않다.

의존성이 불편한 것은 하상 변경에 대한 영향을 암시하기 때문이다.

따라서 변경될 확률이 거의 없는 클래스라면 의존성이 문제가 되지 않는다. 자바의 경우 JDK에 포함된 표준 클래스이에 해당된다.

public abstract class DiscountPolicy {
	private List<DiscountPolicy> conditions = new ArrayList<>();
}

클래스를 직접 생성하더라도 가능한 추상적인 타입을 사용하는 것이 확장성 측면에서 유리하다.

인터페이스 List를 사용한 이유다.

public abstract class DiscountPolicy {
	private List<DiscountPolicy> conditions = new ArrayList<>();

	public void switchConditions(List<DiscountPolicy> conditions) { // 의존성 명시
		this.condtions = conditions;
	}
}

의존성에 영향이 적은 경우에도 추상화에 의존하고 명시적으로 드러낸 것이 좋은 설계 습관이다.

조합 가능한 행동

어떤 객체와 협력하느냐에 따라 객체의 행동이 달라지는 것은 유연하고 재사용가능한 설계가 가진 특징이다. 응집도 높인 책임들을 가진 작은 객체들을 다양한 방식으로 연결함으로써 애플리케이션의 기능을 쉽게 확장할 수있다.

 

유연하고 재사용 가능한 설계는 객체가 어떻게(how)하는지를 나열하지 않고도 객체들의 조합을 통해 무엇(what)을 하는지를 표현하는 클래스들로 구성된다. 따라서 클래스의 인스턴스를 생성하는 코드를 보는 것만으로도 객체가 어떤 일을 하는지 파악할 수 있다. 선언적으로 객체의 행동을 정의할 수 있는 것이다.

 

이러한 설계를 만드는데 핵심은 의존성을 관리하는 것이다.

 

 

참고

https://product.kyobobook.co.kr/detail/S000001766367

 

오브젝트 | 조영호 - 교보문고

오브젝트 | 역할, 책임, 협력을 향해 객체지향적으로 프로그래밍하라!객체지향으로 향하는 첫걸음은 클래스가 아니라 객체를 바라보는 것에서부터 시작한다. 객체지향으로 향하는 두번째 걸음

product.kyobobook.co.kr

 

' > 오브젝트' 카테고리의 다른 글

6장 메시지와 인터페이스  (0) 2023.11.29