• oop
  • design
  • java
  • book

오브젝트 Ch.8 — 의존성 관리하기

런타임·컴파일타임 의존성의 차이와 의존성 해결 방법, 추상화에 의존해야 하는 이유와 new의 해로움을 정리한다.

시리즈 · BackEnd 서적14 / 17
  1. SQL 레벨업 Ch.1 — DBMS 아키텍처
  2. SQL 레벨업 Ch.3 — SQL의 조건 분기
  3. SQL 레벨업 Ch.4~5 — 집약과 자르기, 반복문
  4. SQL 레벨업 Ch.6 — 결합
  5. SQL 레벨업 Ch.7 — 서브쿼리
  6. SQL 레벨업 Ch.8 — SQL의 순서
  7. SQL 레벨업 Ch.9 — 갱신과 데이터 모델
  8. SQL 레벨업 Ch.10 — 인덱스 사용
  9. 오브젝트 Ch.1 — 객체, 설계
  10. 오브젝트 Ch.2 — 객체지향 프로그래밍
  11. 오브젝트 Ch.3~4 — 역할, 책임, 협력 / 설계 품질과 트레이드오프
  12. 오브젝트 Ch.5 — 책임 할당하기
  13. 오브젝트 Ch.6 — 메시지와 인터페이스
  14. 오브젝트 Ch.8 — 의존성 관리하기
  15. 오브젝트 Ch.9 — 유연한 설계
  16. 오브젝트 Ch.10, 12 — 상속과 코드 재사용, 다형성
  17. 오브젝트 Ch.13 — 서브클래싱과 서브타이핑

잘 설계된 객체지향 애플리케이션은 책임의 초점이 명확한, 작고 응집도 높은 객체들로 구성된다. 이런 객체는 단독으로 할 수 있는 일이 거의 없어 다른 객체와 협력해야 하고, 협력하려면 상대 객체가 존재한다는 사실과 그 객체가 수신할 수 있는 메시지를 알아야 한다. 이 지식이 객체 사이의 의존성을 낳는다.

과도한 의존성은 애플리케이션을 수정하기 어렵게 만든다. 객체지향 설계의 핵심은 협력에 필요한 의존성은 유지하면서 변경을 방해하는 의존성은 제거하는 것이다.

의존성이란

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

  • 실행 시점: 의존하는 객체가 정상적으로 동작하려면 실행 시에 의존 대상 객체가 반드시 존재해야 한다.
  • 구현 시점: 의존 대상 객체가 변경되면 의존하는 객체도 함께 변경된다.

의존성에는 방향이 있다.

public class A {
    private B b;
}

B가 변경되면 A도 영향을 받지만 그 역은 성립하지 않는다.

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

  • 런타임: 애플리케이션이 실행되는 시점
  • 컴파일타임: 일반적으로 컴파일하는 시점을 가리키지만, 문맥에 따라 코드 그 자체를 가리키기도 한다.

앞 장들에서 계속 다룬 예제를 다시 보면, 코드 상에서 Movie는 DiscountPolicy에 의존하지만 런타임에는 그 구현인 AmountDiscountPolicy나 PercentDiscountPolicy의 인스턴스에 의존한다. 컴파일타임 의존성과 런타임 의존성은 다를 수 있고, 동일한 소스코드 구조로 다양한 실행 구조를 만들 수 있어야 한다.

의존성 해결하기

추상화에 대한 컴파일타임 의존성을 적절한 런타임 의존성으로 교체하는 방법은 세 가지다.

1. 생성자

Movie starWars = new Movie("스타워즈", new PercentDiscountPolicy(...));

2. setter 메서드

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

Movie avatar = new Movie(...);
avatar.setDiscountPolicy(new AmountDiscountPolicy(...));

객체를 생성한 뒤 의존 대상을 설정하기 전까지는 객체의 상태가 불완전할 수 있다는 것이 단점이다. 더 좋은 방법은 생성자 방식과 setter 방식을 혼합하는 것이다. 생성 시점에 완전한 상태를 만들고, 필요할 때 교체한다.

Movie avatar = new Movie(..., new PercentDiscountPolicy(...));
// ...
avatar.setDiscountPolicy(new AmountDiscountPolicy(...));

3. 메서드 인자

특정 행동을 할 때만 일시적으로 알면 되는 대상이라면 메서드의 인자로 전달한다.

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

바람직한 의존성

의존성은 협력을 위해 반드시 필요하다. 모든 의존성이 나쁜 것은 아니고, 바람직하지 못한 의존성이 문제일 뿐이다.

기준은 재사용성이다. 어떤 의존성이 클래스를 다양한 환경에서 재사용할 수 없게 제한한다면, 즉 다른 환경에서 쓰기 위해 내부 구현을 변경하게 만든다면 바람직하지 않은 의존성이다. 바람직한 의존성은 컨텍스트에 독립적이어서 다양한 환경에서 재사용될 가능성을 열어 둔다.

추상화에 의존하라

한 요소가 다른 요소에 대해 더 많이 알수록 둘은 강하게 결합된다. Movie가 PercentDiscountPolicy보다 DiscountPolicy에 의존할 때 결합도가 느슨해지는 것은 알아야 하는 지식의 양이 적기 때문이다.

협력 대상에 대해 필요한 정보 외에는 최대한 감추는 것이 중요하고, 이를 달성하는 가장 효과적인 방법이 추상화다. 다음 순서로 갈수록 알아야 하는 정보가 적어진다.

  1. 구체 클래스 의존성
  2. 추상 클래스 의존성
  3. 인터페이스 의존성

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

new의 해로움

  • new를 사용하려면 구체 클래스의 이름을 직접 기술해야 한다. 클라이언트가 추상화가 아닌 구체 클래스에 의존하게 된다.
  • 어떤 인자로 생성자를 호출해야 하는지도 알아야 한다. 클라이언트가 알아야 하는 지식의 양이 늘어난다.

두 경우 모두 결합도가 높아진다. 해결 방법은 인스턴스를 생성하는 로직과 사용하는 로직을 분리하는 것이다. 사용하는 쪽은 앞에서 본 것처럼 생성자, setter, 메서드 인자로 인스턴스를 전달받기만 한다.

유연하고 재사용 가능한 설계는 객체가 어떻게(how) 하는지를 장황하게 나열하지 않고, 객체들의 조합을 통해 무엇(what)을 하는지를 표현하는 클래스들로 구성된다.