• 잘 설계된 객체지향 애플리케이션은 작고 응집도 높은 객체들로 구성됩니다.
  • 이런 작은 객체들이 단독으로 수행할 수 있는 작업은 거의 없기 때문에 객체 사이의 협력을 낳습니다.
  • 협력을 위해서는 의존성이 필요하지만 과도한 의존성은 애플리케이션을 수정하기 어렵게 만듭니다.
  • 객체지향 설계의 핵심은 협력을 위해 필요한 의존성은 유지하면서도 변경을 방해하는 의존성을 제거하는 데 있습니다.
    • 이런 관점에서 객체지향 설계란 의존성을 관리하는 것이고 객체가 변화를 받아 들일 수 있게 의존성을 정리하는 기술이라고 할 수 있습니다.
  • 8장에서는 충분히 협력적이면서도 유연한 객체를 만들기 위해 의존성을 관리하는 방법을 살펴봅니다.

01. 의존성 이해하기

변경과 의존성

  • 어떤 객체가 협력하기 위해 다른 객체를 필요로 할 때 두 객체 사이에 의존성이 존재하게 됩니다.
  • 의존성은 실행 시점과 구현 시점에 서로 다른 의미를 가집니다.
    • 실행 시점 : 의존하는 객체가 정상적으로 동작하기 위해서는 실행 시에 의존 대상 객체가 반드시 존재해야 합니다.
    • 구현 시점 : 의존 대생 객체가 변경 될 경우 의존하는 객체도 함께 변경됩니다.
  • 어떤 객체가 예정된 작업을 정상적으로 수행하기 위해 다른 객체를 필요로 하는 경우 두 객체 사이에 의존성이 존재한다고 말합니다.
  • 의존성은 방향성을 가지며 항상 단방향입니다.
  • 두 요소 사이의 의존성은 의존되는 요소가 변경될 때 의존하는 요소도 함께 변경될 수 있다는 것을 의미합니다.
    • 의존성은 변경에 의한 영향의 전파 가능성을 암시합니다.
  • 의존성이란 의존하고 있는 대상의 변경에 영향을 받을 수 있는 가능성입니다.

의존성 전이(transitive dependency)

  • 의존성은 전이될 수 있습니다.
  • 의존성은 함께 변경될 수 있는 가능성을 의미하기 때문에 모든 경우에 의존성이 전이되는 것은 아닙니다.
  • 의존성이 실제로 전이 될지 여부는 변경의 방향과 캡슐화의 정도에 따라 달라집니다.
  • 의존성 전이는 변경에 의해 영향이 널리 전파 될 수도 있다는 경고일 뿐입니다.
  • 의존성은 전이될 수 있기 때문에 의존성의 종류를 직접 의존성(direct dependency)간접 의존성(indirect dependency)으로 나누기도 합니다.
    • 직접 의존성이란 말 그대로 한 요소가 다른 요소에 직접 의존하는 경우를 가리킵니다.
    • 간접 의존성이란 직접적인 관계는 존재하지 않지만 의존성 전이에 의해 영향이 전파되는 경우를 가리킵니다.

런타임 의존성(rum-time dependency)과 컴파일타임 의존성(compile-time dependency)

  • 코드 관점에서 주인공은 클래스입니다. 따라서 컴파일타임 의존성이 다루는 주제는 클래스 사이의 의존성 입니다.
  • 객체지향 애플리케이션에서 런타임의 주인공은 객체입니다. 따라서 런타임 의존성을 다루는 주제는 객체 사이의 의존성입니다.
  • 런타임 의존성과 컴파일타임 의존성이 다를 수 있습니다. 사실 유연하고 재사용 가능한 코드를 설계하기 위해서는 두 종류의 의존성을 서로 다르게 만들어야 합니다.
  • 유연하고 재사용 가능한 설계를 창조하기 위해서는 동일한 소스코드 구조를 가지고 다양한 실행 구조를 만들 수 있어야 합니다.
  • 어떤 클래스의 인스턴스가 다양한 클래스의 인스턴스와 협력하기 위해서는 협력할 인스턴스의 구체적인 클래스를 알아서는 안 됩니다.
  • 컴파일타임 구조와 런타임 구조 사이의 거리가 멀면 멀수록 설계가 유연해지고 재사용 가능해집니다.

컨텍스트 독립성

  • 클래스가 사용될 특정한 문맥에 대해 최소한의 가정만으로 이뤄져 있다면 다른 문맥에서 재사용하기가 더 수월해지는 것을 컨텍스트 독립성이라고 부릅니다.
  • 설계가 유연해지기 위해서는 가능한 한 자신이 실행될 컨텍스트에 대해 구체적인 정보를 최대한 적게 알아야 합니다.
  • 컨텍스트 독립성을 따르면 다양한 컨텍스트에 적용할 수 있는 응집력 있는 객체를 만들 수 있고 객체 구성 방법을 재설정해서 변경 가능한 시스템으로 나아갈 수 있습니다.

의존성 해결하기

  • 컴파일타임 의존성은 구체적인 런타임 의존성으로 대체돼야 합니다.
  • 컴파일타임 의존성을 실행 컨텍스트에 맞는 적절한 런타임 의존성으로 교체하는 것을 의존성 해결이라고 부릅니다.
  • 의존성 해결하는 3가지 방법
    • 객체를 생성하는 시점에 생성자를 통해 의존성을 해결
    • 객체를 생성 후 setter 메서드를 통해 의존성 해결
    • 메서드 실행 시 인자를 이용해 의존성 해결

02. 유연한 설계

의존성과 결합도

  • 모든 의존성이 나쁜 것은 아닙니다. 의존성은 객체들의 협력을 가능하게 만드는 매개체라는 관점에서는 바람직한 것입니다.
  • 의존성이 과하면 문제가 될 수 있습니다.
  • 문제는 의존성의 존재가 아니라 의존성의 정도 입니다.
  • 바람직한 의존성은 재사용성과 관련이 있습니다.
  • 바람직한 의존성이란 컨텍스트에 독립적인 의존성을 의미하며 다양한 환경에서 재상용될 수 있는 가능성을 열어놓은 의존성을 의미합니다.
  • 바람직하지 못한 의존성을 가리키는 좀더 세련된 용어가 결합도 입니다.
    • 어떤 두 요소 사이에 존재하는 의존성이 바람직할 때 두 요소가 느슨한 결합도(loose coupling) 또는 약한 결합도(weak coupling)를 가진다고 말합니다.
    • 반대로 두 요소 사이의 의존성이 바람직하지 못할 때 단단한 결합도(tight coupling) 또는 강한 결합도(strong coupling)를 가진다고 말합니다.
  • 바람직한 의존성이란 설계를 재사용하기 쉽게 만드는 의존성 입니다.

의존성과 결합도

  • 의존성은 두 요소 사이의 관계 유무를 설명합니다.
    • 따라서 의존성의 관점에서는 ‘의존성이 존재한다’ 또는 ‘의존성이 존재하지 않는다’라고 표현해야 합니다.
  • 결합도는 두 요소 사이엥 존재하는 의존성의 정도를 상대적으로 표현합니다.
    • 따라서 결합도의 관점에서는 ‘결합도가 강하다’ 또는 ‘결합도가 느슨하다’라고 표현 합니다.

지식이 결합을 낳는다

  • 서로에 대해 알고 있는 양이 결합도를 결정합니다.
  • 결합도를 느슨하게 만들기 위해서는 협력하는 대상에 대해 필요한 정보 외에는 최대한 감추는 것이 중요합니다.
  • 이를 해결하기 위해서 추상화를 사용합니다.

추상화에 의존하라

  • 일반적으로 추상화와 결합도의 관점에서 의존 대상을 다음과 같이 구분하는 것이 유용합니다.
    • 구체 클래서 의존성(concreate class dependency)
    • 추상 클래스 의존성(abstract class dependency)
    • 인터페이스 의존성(interface dependency)
  • 결합도를 느슨하게 만들기 위해서는 구체적인 클래스보다 추상클래스에, 추상 클래스보다 인터페이스에 의존하도록 만드는 것이 더 효과적입니다.
  • 의존하는 대상이 더 추상적일수록 결합도는 더 낮아집니다.

명시적인 의존성

  • 모든 경우에 의존성은 명시적으로 퍼블릭 인터페이스에 노출하는 것을 명시적 의존성(explicit dependency)이라고 부릅니다.
  • 의존성이 인터페이스에 표시되지 않는 것을 숨겨진 의존성(hidden dependency)이라고 부릅니다.
  • 명시적인 의존성을 사용해야만 퍼블릭 인터페이스를 통해 컴파일타임 의존성을 적절한 런타임 의존성을로 교체할 수 있습니다.
  • 클래스가 다른 클래스에 의존하는 것은 부끄러운 일이 아닙니다. 의존성은 다른 객체와의 협력을 가능하게 해주기 때문에 바람직한 것입니다. 경계해야 할 것은 의존성 자체가 아니라 의존성을 감추는 것입니다. 숨겨져 있는 의존성을 밝은 곳으로 드러내서 널리 알리면 설계가 유연하고 재사용 가능해 집니다.

new는 해롭다

  • new를 잘못 사용하면 클래스 사이의 결합도가 극단적으로 높아집니다.
  • 해결 방법은 인스턴스를 생성하는 로직과 생성된 인스턴스를 사용하는 로직을 분리하는 것입니다. 필요한 인스턴스를 생성자의 인자로 전달받아 내부의 인스턴스 변수에 할당합니다.
  • 사용과 생성의 책임을 분리하고, 의존성을 생성자에 명시적으로 드러내고, 구체 클래스가 아닌 추상 클래스에 의존하게 함으로써 설계를 유연하게 만들 수 있습니다. 그리고 그 출발은 객체를 생성하는 책임을 객체 내부가 아니라 클라이언트로 옮기는 것에서 시작했습니다.

가끔은 생성해도 무방하다

  • 협력하는 기본 객체를 설정하고 싶은 경우는 클래스 안에서 객체의 인스턴스를 직접 생성하는 방식이 유용합니다.
  • 여기서 트레이드오프의 대상은 결합도와 사용성입니다. 구체 클래스에 의존하게 되더라도 클래스의 사용성이 더 중요하다면 결합도를 높이는 방향으로 코드를 작성할 수 있습니다.
  • 가급적 구체 클래스에 대한 의존성을 제거할 수 있는 방법을 찾기를 권장합니다.
  • 종종 모든 결합도가 모이는 새로운 클래스를 추가함으로써 사용성과 유연성이라는 두 마리 토끼를 잡을 수 있느 경우도 있습니다.

표준 클래스에 대항 의존은 해롭지 않습니다

  • 의존성이 불편한 이유는 그것이 항상 변경에 대한 영향을 암시하기 때문입니다. 따라서 변경될 확률이 거의 없는 클래스라면 의존성이 문제가 되지 않습니다.
  • 비록 클래스를 직접 생성하더라도 가능한 한 추상적인 타입을 사용하는 것이 확장성 측면에서 유리합니다. 의존성에 의한 영향이 적은 경우에도 추상화와 의존성을 명시적으로 드러내는 것은 좋은 설계 습관입니다.

컨텍스트 확장하기

  • 어떤 경우든 코드 내부를 직접 수정하는 것은 버그의 발생 가능성을 높입니다. 그렇기 때문에 예외 케이스도 새로운 정책으로 간주합니다.
  • 결합도를 낮춤으로써 얻게 되는 컨텍스트의 확장이라는 개념이 유연하고 재사용 가능한 설계를 만드는 핵심입니다.

조합 가능한 행동

  • 다양한 종류의 정책이 필요한 컨텍스트에서 클래스를 재사용할 수 있는 이유는 코드를 직접 수정하지 않고도 협력 대상인 인스턴스를 교체할 수 있기 때문입니다.
  • 유연하고 재사용 가능한 설계는 작은 객체들의 행동을 조합함으로써 새로운 행동을 이끌어 낼 수 있는 설계입니다.
  • 훌륭한 객체지향 설계란 객체가 어떻게 하는지를 표현하는 것이 아니라 객체들의 조합을 선언적으로 표현함으로써 객체들이 무엇을 하는지를 표현하는 설계입니다. 그리고 이런 설계를 창조하는 데 있어서의 핵심은 의존성을 관리하는 것입니다.