TL;DR

  • 의존성의 개념을 자세히 설명하고 결합도를 느슨하게 유지할 수 있는 다양한 설계 방법들을 설명합니다.
  • 의존성의 관리가 곧 변경의 관리이고 유연한 설계를 낳는 기반이라는 사실을 이해하게 될 것입니다.
  • 잘 설계된 객체지향 애플리케이션은 작고 응집도 높은 객체들로 구성됩니다. 작고 응집도 높은 객체란 책임의 초점이 명확하고 한 가지 일만 잘 하는 객체를 의미합니다. 이런 작은 객체들이 단독으로 수행할 수 있는 작업은 거의 없기 때문에 일반적인 애플리케이션의 기능을 구현하기 위해서는 다른 객체에게 도움을 요청해야 합니다. 이런 요청이 객체 사이의 협력을 낳습니다.
  • 협력은 필수적이지만 과도한 협력은 설계를 곤경에 빠드릴 수 있습니다. 협력은 객체가 다른 객체에 대해 알 것을 강요합니다. 다른 객체와 협력하기 위해서는 그런 객체가 존재한다는 사실을 알고 있어야 합니다. 객체가 수신할 수 있는 메시지에 대해서도 알고 있어야 합니다. 이런 지식이 객체 사이의 의존성을 낳습니다.
  • 협력을 위해서는 의존성이 필요하지만 과도한 의존성은 애플리케이션을 수정하기 어렵게 만듭니다. 객체지향 설계의 핵심은 협력을 위해 필요한 의존성은 유지하면서도 변경을 방해하는 의존성을 제거하는 데 있습니다. 이런 관점에서 객체지향 설계란 의존성을 관리하는 것이고 객체가 변화를 받아 들일 수 있게 의존성을 정리하는 기술이라고 할 수 있습니다.
  • 8장에서는 충분히 협력적이면서도 유연한 객체를 만들기 위해 의존성을 관리하는 방법을 살펴봅니다.

01. 의존성 이해하기

변경과 의존성

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

UML과 의존성

  • UML(Unified Modeling Language)에서는 두 요소 사이의 관계로 실체화 관계(realization), 연관 관계(association), 의존 관계(dependency), 일반화/특수화 관계(generaliztion/specialization), 합성 관계(composition), 집합 관계(aggregation) 등을 정의합니다.
  • 이번장에서 다루고 있는 ‘의존성’은 UML의 의존 관계와는 다릅니다. UML은 두 요소 사이에 존재할 수 있는 다양한 관계의 하나로 ‘의존 관계’를 정의합니다. 의존성은 두 요소 사이에 변경에 의해 영향을 주고받는 힘의 역학관계가 존재한다는 사실에 초점을 맞춥니다. 따라서 UML에 정의된 모든 관계는 의존성이라는 개념을 포함합니다.
  • 이번 장에서 말하는 의존성을 단순히 UML에서 이야기하는 의존 관계로 해석해서는 안 됩니다. UML에서 정의하는 모든 관계가 가지는 공통적인 특성으로 바라봐야 합니다.

의존성 전이(transitive dependency)

  • 의존성은 전이될 수 있습니다.
  • 의존성은 함께 변경될 수 있는 가능성을 의미하기 때문에 모든 경우에 의존성이 전이되는 것은 아닙니다. 의존성이 실제로 전이 될지 여부는 변경의 방향과 캡슐화의 정도에 따라 달라집니다.
  • 내부 구현을 효과적으로 캡슐화하고 있다면 변경이 전파되지 않을 것입니다. 의존성 전이는 변경에 의해 영향이 널리 전파 될 수도 있다는 경고일 뿐입니다.
  • 의존성은 전이될 수 있기 때문에 의존성의 종류를 직접 의존성(direct dependency)간접 의존성(indirect dependency)으로 나누기도 합니다.
    • 직접 의존성이란 말 그대로 한 요소가 다른 요소에 직접 의존하는 경우를 가리킵니다.
    • 간접 의존성이란 직접적인 관계는 존재하지 않지만 의존성 전이에 의해 영향이 전파되는 경우를 가리킵니다.
  • 변경과 관여 있는 어떤 것에도 의존성이라는 개념을 적용 할 수 있습니다. 의존성의 대상은 객체일 수도 있고 모듈이나 더 큰 규모의 실행 스스템일 수도 있습니다. 하지만 의존성의 본질은 변하지 않습니다. 의존성이란 의존하고 있는 대상의 변경에 영향을 받을 수 있는 가능성입니다.

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

  • 의존성과 관련해서 다뤄야 하는 또 다른 주제는 런타임 의존성(rum-time dependency)컴파일타임 의존성(compile-time dependency)의 차이입니다.
  • 런타임은 말 그대로 애플리케이션이 실행되는 시점을 가리킵니다.
  • 컴파일타임은 일반적으로 작성ㄷ괸 코드를 컴파일하는 시점을 가리키지만 문맥에 따라서는 코드 그 자체를 가리키기도 합니다. 컴파일 타임 의존성이라는 용어가 중요하게 생각하는 것은 시간이 아니라 우리가 작성항 코드의 구조입니다. 또한 동적타입 언어의 경우에는 컴파일타임이 존재하지 않기 때문에 컴파일 의존성이라는 용어를 실제로 컴파일이 수행되는 시점으로 이해하면 의미가 모호해질 수 있습니다. 따라서 어딘가에서 컴파일이라는 용어를 보게 된다면 그것이 정말 컴파일이 진행되는 시점을 가리키는 것인지 아니면 코드를 작성하는 시점을 가리키는 것인지를 파악하는 것이 중요합니다.
  • 객체지향 애플리케이션에서 런타임의 주인공은 객체입니다. 따라서 런타임 의존성을 다루는 주제는 객체 사이의 의존성입니다. 반면 코드 관점에서 주인공은 클래스입니다. 따라서 컴파일타임 의존성이 두르는 주제는 클래스 사이의 의존성 입니다.
  • 런타임 의존성과 컴파일타임 의존성이 다를 수 있습니다. 사실 유연하고 재사용 가능한 코드를 설계하기 위해서는 두 종류의 의존성을 서로 다르게 만들어야 합니다.
  • 유연하고 재사용 가능한 설계를 창조하기 위해서는 동일한 소스코드 구조를 가지고 다양한 실행 구조를 만들 수 있어야 합니다.
  • 어떤 클래스의 인스턴스가 다양한 클래스의 인스턴스와 협력하기 위해서는 협력할 인스턴스의 구체적인 클래스를 알아서는 안 됩니다. 실제로 협력할 객체가 어떤 것인지는 런타임에 해결해야 합니다. 클래스가 협력할 객체의 클래스를 명시적으로 드러내고 있다면 다른 클래스 인스턴스와 협력할 가능성 자체가 없어집니다. 따라서 컴파일타임 구조와 런타임 구조 사이의 거리가 멀면 멀수록 설계가 유연해지고 재사용 가능해집니다.

컨텍스트 독립성

  • 유연하고 확장 가능한 설계를 마들기 위해서는 컴파일타임 의존성과 런타임 의존성이 달라야 합니다.
  • 클래스는 자신과 협력할 객체의 구체적은 클래스에 대해 알아서는 안 됩니다. 구체적은 클래스를 알면 알수록 그 클래스가 사용되는 특정한 문맥에 강하게 결합되기 때문입니다. 구체 클래스에 대해 의존하는 것은 클래스의 인스턴스가 어떤 문맥에서 사용될 것인지를 구체적으로 명시하는 것과 같습니다.
  • 클래스가 특정한 문맥에 강하게 결합될수록 다른 문맥에서 사용하기는 더 어려워집니다. 클래스가 사용될 특정한 문맥에 대해 최소한의 가정만으로 이뤄져 있다면 다른 문맥에서 재사용하기가 더 수월해집니다. 이를 컨텍스트 독립성이라고 부릅니다.
  • 설계가 유연해지기 위해서는 가능한 한 자신이 실행될 컨텍스트에 대해 구체적인 정보를 최대한 적게 알아야 합니다. 컨텍스트에 대한 정보가 적으면 적을수록 더 다양한 컨텍스트에서 재사용될 수 있기 때문입니다. 결과적으로 설계는 더 유연해지고 변경에 탄력적으로 대응할 수 있게 될 것입니다.
  • 시스템을 구성하는 객체가 컨텍스트 독립적이라면 해당 시스템은 변경하기 쉽습니다. 여기서 컨텍스트 독립적이라는 말은 각 객체가 해당 객체를 실행하는 시스템에 관해 아무것도 알지 못한다는 의미입니다. 이렇게 되면 행위의 단위(객체)를 가지고 새로운 상황에 적용할 수 있습니다. 컨텍스트 독립성을 따르면 다양한 컨텍스트에 적용할 수 있는 응집력 있는 객체를 만들 수 있고 객체 구성 방법을 재설정해서 변경 가능한 시스템으로 나아갈 수 있습니다.

의존성 해결하기

  • 컴임 의존성은 구체적인 런타임 의존성으로 대체돼야 합니다.
  • 컴파일타임 의존성을 실행 컨텍스트에 맞는 적절한 런타임 의존성으로 교체하는 것을 의존성 해결이라고 부릅니다.
  • 의존성 해결하는 방법
    • 객체를 생성하는 시점에 생성자를 통해 의존성을 해결
    • 객체를 생성 후 setter 메서드를 통해 의존성 해결
      • setter 메서드를 이용하는 방식은 객체를 생성한 이후에도 의존하고 있는 대상을 변경할 수 있는 가능성을 열어 놓고 싶은 경우에 유용합니다.
      • setter 메서드를 이용하는 방법은 실행 시점에 의존 대상을 변경할 수 있기 때문에 설계를 좀 더 유연하게 만들 수 있습니다.
      • 단점은 객체가 생성된 후에 협력에 필요한 의존 대상을 설정하기 때문에 객체를 생성하고 의존 대상을 설정하기 전까지는 객체의 상태가 불완전할수 있다는 점입니다.
      • 더 좋은 방법은 생성자 방식과 setter 방식을 혼합하는 것입니다. 항상 객체를 생성할 때 의존성을 해결해서 완전한 상태의 객체를 생성한 후, 필요에 따라 setter 메서드를 이용해 의존 대상을 변경할 수 있게 할 수 있습니다. 이 방법은 시스템의 상태를 안정적으로 유지하면서도 유연성을 향상시킬 수 있기 때문에 의존성 해결을 위해 가장 선호되는 방법입니다.
    • 메서드 실행 시 인자를 이용해 의존성 해결
      • 메서드 인자를 사용하는 방식은 협력 대상에 대해 지속적으로 의존 관계를 맺을 필요 없이 메서드가 실행되는 동안만 일시적으로 의존 관계가 존재해도 무방하거나, 메서드가 실행될 때마다 의존 대상이 매번 달라져야 하는 경우에 유용합니다.
      • 하지만 클래스의 메서드를 호출하는 대부분의 경우에 매번 동일한 객체를 인자로 전달하고 있다면 생성자를 이용하거나 setter 메서드를 이용해 의존성을 지속적으로 유지하는 방식으로 변경하는 것이 좋습니다.

02. 유연한 설계

희존성과 결합도

  • 객체지향 패러다임의 근간은 협력 입니다. 객체들은 협력을 통해 애플리케이션에 생명력을 불어 넣습니다. 객체들이 협력하기 위해서는 서로의 존재와 수행 가능한 책임을 알아야 합니다. 이런 지식들이 객체 사이의 의존성을 낳습니다. 따라서 모든 의존성이 나쁜 것은 아닙니다. 의존성은 객체들의 협력을 가능하게 만드는 매개체라는 관점에서는 바람직한 것입니다. 하지만 의존성이 과하면 문제가 될 수 있습니다.
  • 문제는 의존성의 존재가 아니라 의존성의 정도 입니다. 재사용할 수 있는 가능성을 없애 버릴수 있습니다. 해결방법은 의존성을 바람직하게 만드는 것입니다. 협력할 객체의 클래스를 고정할 필요가 없습니다. 추상 클래스의 메시지를 이해할 수 있는 타입을 정의함으로써 이 문제를 해결합니다. 컴파일타임 의존성을 런타임 의존성으로 대체할 수 있습니다.
  • 의존성은 협력을 위해 반드시 필요한 것입니다. 단지 바람직하지 못한 의존성이 문제입니다.
  • 바람직한 의존성은 재사용성과 관련이 있습니다. 어떤 의존성이 다양한 환경에서 클래스를 재사용할 수 없도록 제한한다면 그 의존성은 바람직하지 못한 것입니다. 어떤 의존성이 다양한 환경에서 재사용할 수 있다면 그 의존성은 바람직한 것입니다. 다시말해 컨텍스트에 독립적인 의존성은 바람직한 의존성이고 특정한 컨텍스트에 강하게 결합된 의존성은 바람직하지 않은 의존성 입니다.
  • 특정한 컨텍스트에 강하게 의존하는 클래스를 다른 컨텍스트에서 재사용할 수 있는 유일한 방법은 구현을 변경하는 것뿐입니다. 하지만 이것은 바람직하지 못한 의존성을 바람직하지 못한 의존성으로 대체하는 것 뿐입니다. 다른 환경에서 재사용하기 위해 내부 구현을 변경하게 만드는 의존성은 바람직하지 않은 의존성입니다.
  • 바람직한 의존성이란 컨텍스트에 독립적인 의존성을 의미하며 다양한 환경에서 재상용될 수 있는 가능성을 열어놓은 의존성을 의미합니다.
  • 바람직하지 못한 의존성을 가리키는 좀 더 세련된 용어가 결합도 입니다. 어떤 두 요소 사이에 존재하는 의존성이 바람직할 때 두 요소가 느슨한 결합도(loose coupling) 또는 약한 결합도(weak coupling)를 가진다고 말합니다. 반대로 두 요소 사이의 의존성이 바람직하지 못할 때 단단한 결합도(tight coupling) 또는 강한 결합도(strong coupling)를 가진다고 말합니다.
  • 바람직한 의존성이란 설계를 재사용하기 쉽게 만드는 의존성 입니다. 바람직하지 못한 의존성이란 설계를 재사용하기 어렵게 만드는 의존성입니다. 어떤 의존성이 재사용을 방해한다면 결합도가 강하다고 표현합니다. 어떤 의존성이 재사용을 쉽게 허용한다면 결합도가 느슨하다고 표현합니다.

의존성과 결합도

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

지식이 결합을 낳는다

  • 결합도의 정도는 한 요소가 자신이 의존하고 있는 다른 요소에 대해 알고 있는 정보의 양으로 결정됩니다. 한 요소가 다른 요소에 대해 더 많은 정보를 알고 있을수록 두 요소는 강하게 결합됩니다. 반대로 한 요소가 다른 요소에 대해 더 적은 정보를 알고 있을수록 두 요소는 약하게 결합됩니다.
  • 서로에 대해 알고 있는 양이 결합도를 결정합니다.
  • 더 많이 알수록 더 많이 결합됩니다. 더 많이 알고 있다는 것은 더 적은 컨텍스트에서 재사용 가능하다는 것을 의미합니다. 기존 지식에 어울리지 않는 컨테스트에서 클래스의 인스턴스를 사용하기 위해서 할 수 있는 유일한 방법은 클래스를 수정하는 것뿐입니다. 결합도를 느슨하게 유지하려면 협력하는 대상에 대해 더 적게 알아야 합니다. 결합도를 느슨하게 만들기 위해서는 협력하는 대상에 대해 필요한 정보 외에는 최대한 감추는 것이 중요합니다. 이를 해결하기 위해서 추상화를 사용합니다.

추상화에 의존하라

  • 추상화란 어떤 양상, 세부사항, 구조를 좀 더 명확하게 이해하기 이해 특정 절차나 물체를 의도적으로 생략하거나 감춤으로써 복잡도를 극복하는 방법입니다.
  • 추상화를 사용하면 현재 다루고 있는 문제를 해결하는 데 불필요한 정보를 감출 수 있습니다. 따라서 대상에 대해 알아야 하는 지식의 양을 줄일 수 있기 때문에 결합도를 느슨하게 유지할 수 있습니다.
  • 일반적으로 추상화와 결합도의 관점에서 의존 대상을 다음과 같이 구분하는 것이 유용합니다. 목록에서 아래로 갈수록 클라이언트가 알아야 하는 지식의 양이 적어지기 때문에 결합도가 느슨해집니다.
    • 구체 클래서 의존성(concrete class dependency)
    • 추상 클래스 의존성(abstract class dependency)
    • 인터페이스 의존성(interface dependency)
  • 구체 클래스에 비해 추상 클래스는 메서딍 내부 구현과 자식 클래스의 종류에 대한 지식을 클라이언트에게 숨길 수 있습니다. 따라서 클라이언트가 알아야 하는 지식의 양이 더 적기 때문에 구체 클래스보다 추상 클래스에 의존하는 것이 결합도가 더 낮습니다. 하지만 추상 클래스의 클라이언트는 여전히 협력하는 대상이 속한 클래스 상속 계층이 무엇인지에 대해서는 알고 있어야 합니다.
  • 인터페이스에 의존하면 상속 계층을 모르더라도 협력이 가능해집니다. 인터페이스 의조성은 협력하는 객체가 어떤 메시지를 수신할 수 있는지에 대한 지식만을 남기기 때문에 추상 클래스 의존성보다 결합도가 낮습니다. 이것은 다양한 클래스 상속 계층에 속한 객체들이 동일한 메시지를 수신할 수 있도록 컨텍스트를 확장하는 것을 가능하게 합니다.
  • 실행 컨텍스트에 대해 알아야 하는 정보를 줄일수록 결합도가 낮아진다는 것입니다. 결합도를 느슨하게 만들기 위해서는 구체적인 클래스보다 추상클래스에, 추상 클래스보다 인터페이스에 의존하도록 만드는 것이 더 효과적입니다. 다시말해 의존하는 대상이 더 추상적일수록 결합도는 더 낮아집니다.

명시적인 의존성

  • 결합도를 느슨하게 만들기 위해서는 인스턴스 변수의 타입을 추상 클래스나 인터페이스로 선언하는 것만으로는 부족합니다. 클래스 안에서 구체 클래스에 대한 모든 의존성을 제거해야만 합니다. 런타임에 구체 클래스의 인스턴스와 협력해야 하기 때문에 어떤 인스턴스인지 알려줄 수 있는 방법이 필요합니다.
  • 인스턴스 변수의 타입은 추상 클래스나 인터페이스 정의하고 생성자, setter 메서드, 메서드 인자로 의존성을 해결할 때는 추상 클래스를 상속받거나 인터페이스를 실체화한 구체 클래스를 전달하는 것입니다.
  • 생성자 안에서 인스턴스를 직접 생성하지 않고 생성자의 인자로 선언할 수 있습니다. 의존성의 대상을 생성자의 인자로 전달 받는 방법과 생성자 안에서 직접 생성하는 방법 사이의 가장 큰 차이점은 퍼블릭 인터페이스를 통해 정책을 설정할 수 있는 방법을 제공하는지 여부 입니다. 이것은 setter 메서드를 사용하는 방식과 메서드 인자를 사용하는 방식의 경우에도 동일합니다.
  • 모든 경우에 의존성은 명시적으로 퍼블릭 인터페이스에 노출 됩니다. 이를 명시적 의존성(explicit dependency)이라고 부릅니다.
  • 내부에서 인스턴스를 직접 생성하는 방식은 의존한다는 사실을 감춥니다. 의존성이 인터페이스에 표시되지 않는 것을 숨겨진 의존성(hidden dependency)이라고 부릅니다.
  • 의존성이 명시적이지 않으면 의존성을 파악하기 위해 내부 구현을 직접 살펴볼 수밖에 없습니다. 커다란 클래스에 정의된 긴 메서드 내부 어딘가에서 인스턴스를 생성하는 코드를 파악하는 것은 쉽지 않을뿐더러 심지어 고통스러울 수도 있습니다.
  • 더 커다란 문제는 의존성이 명시적이지 않으면 클래스를 다른 컨텍스트에서 재사용하기 위해 내부 구현을 직접 변경해야 한다는 것입니다. 코드 수정은 언제나 잠재적으로 버그 발생을 내포합니다. 의존성을 명시적으로 드러내면 코드를 직접 수정해야 하는 위험을 피할 수 있습니다. 실행 컨텍스트에 적절한 의존성을 선택할 수 있기 때문입니다.
  • 의존성을 명시적으로 표현돼야 합니다. 의존성을 구혀 내부에 숨겨두면 안됩니다. 유연하고 재상용 가능한 설계란 퍼블릭 인터페이스를 통해 의존성이 명시적으로 드러나는 설계입니다. 명시적인 의존성을 사용해야만 퍼블릭 인터페이스를 통해 컴파일타임 의존성을 적절한 런타임 의존성으로 교체할 수 있습니다.
  • 클래스가 다른 클래스에 의존하는 것은 부끄러운 일이 아닙니다. 의존성은 다른 객체와의 협력을 가능하게 해주기 때문에 바람직한 것입니다. 경계해야 할 것은 의존성 자체가 아니라 의존성을 감추는 것입니다. 숨겨져 있는 의존성을 박은 곳으로 드러내서 널리 알리면 설계가 유연하고 재사용 가능해 집니다.

new는 해롭다

  • new를 잘못 사용하면 클래스 사이의 결합도가 극단적으로 높아집니다.
    • new 연산자를 사용하기 위해서는 구체 클래스의 이름을 직접 기술해야 합니다. 따라서 new를 사용하는 클라이언트는 추상화가 아닌 구체 클래스에 의존할 수밖에 없기 때문에 결합도가 높아집니다.
    • new 연산자는 생성하려는 구체 클래스뿐만 아니라 어떤 인자를 이용해 클래스의 생성자를 호출해야 하는지도 알아야 합니다. 따라서 new를 사용하면 클라이언트가 알아야 하는 지식의 양이 늘어나기 때문에 결합도가 높아집니다.
  • 구체 클래스에 직접 의존하면 결합도가 높아진다는 사실을 기억해야 합니다. 결합도의 관점에서 구체 클래스는 협력자에게 너무 많은 지식을 알도록 강요합니다. 여기서 new는 문제를 더 크게 만듭니다. 클라이언트는 구체 클래스를 생성하는 데 어떤 정보가 필요한지에 대해서도 알아야 하기 때문입니다.
  • 결합도 높으면 변경에 의해 영향을 받기 쉬워집니다. 더 많은 것에 의존하면 의존할수록 점점 더 변경에 취약해집니다. 이것은 높은 결합도를 피해야 하는 이유입니다.
  • new는 여러분의 클래스를 구체 클래스에 겨합시키는 것만으로 끝나지 않습니다. 협력할 클래스의 인스턴스를 생성하기 위해 어떤 인자들이 필요하고 그 인자들을 어떤 순서로 사용해야 하는지에 대한 정보도 노출시킬뿐만 아니라 인자로 사용되는 구체 클래스에 대한 의존성을 추가합니다.
  • 해결 방법은 인스턴스를 생성하는 로직과 생성된 인스턴스를 사용하는 로직을 분리하는 것입니다. 단지 해당하는 인스턴스를 사용하기만 해야 합니다. 어떤 방법을 사용하건 인스턴스에 메시지를 전송하는 코드만 남아 있어야 합니다. 인스턴스에 메시지를 전송하는 코드만 남아 있어야 합니다.
  • 사용과 생성의 책임을 분리하고, 의존성을 생성자에 명시적으로 드러내고, 구체 클래스가 아닌 추상 클래스에 의존하게 함으로써 설계를 유연하게 만들 수 있습니다. 그리고 그 출발은 객체를 생성하는 책임을 객체 내부가 아니라 클라이언트로 옮기는 것에서 시작했습니다. 올바른 객체가 올바른 책임을 수행하게 하는 것이 훌륭한 설계를 창조하는 기반이라는 사실을 잘 보여줍니다.

가끔은 생성해도 무방하다

  • 협력하는 기본 객체를 설정하고 싶은 경우는 클래스 안에서 객체의 인스턴스를 직접 생성하는 방식이 유용합니다. 대부분의 경우 하나의 인스턴스만 의존하고 가끔씩만 다른 인스턴스와 협력하는 경우 인스턴스를 생성하는 책임을 클라이언트로 옮긴다면 클라이언트들 사이에 중복 코드가 늘어나고 사용성도 나빠질 것입니다.
    • 기본 객체를 생성하는 생성자를 추가하고 이 생성자에게 인스턴스를 인자로 받는 생성자를 체이닝 합니다.
    // 자바스크립트에서는 생성자를 체이닝 할 수 없어서 옵셔널하게 처리해아 할 듯 합니다.
    class Movie {
      constructor(
        title: string,
        runningTime: Duration,
        discountPolicy?: DiscountPolicy,
      ) {
        if (!discountPolicy) {
          this.discountPolicy = new AmountDiscountPolicy();
        } else {
          this.discountPolicy = discountPolicy;
        }
      }
    }
    • 메서드를 오버로딩하는 경우에도 사용할 수 있습니다. 자바스크립트에서는 메소드의 구현 역시 오버로딩 할 수 없어 if문을 활용해야 할 거 같습니다.
  • 설계는 트레이드오프입니다. 여기서 트레이드오프의 대상은 결합도와 사용성입니다. 구체 클래스에 의존하게 되더라도 클래스의 사용성이 더 중요하다면 결합도를 높이는 방향으로 코드를 작성할 수 있습니다. 가급적 구체 클래스에 대한 의존성을 제거할 수 있는 방법을 찾기를 권장합니다.
  • 종종 모든 결합도가 모이는 새로운 클래스를 추가함으로써 사용성과 유연성이라는 두 마리 토끼를 잡을 수 있느 경우도 있습니다.

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

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

컨텍스트 확장하기

  • 어떤 경우든 코드 내부를 직접 수정하는 것은 버그의 발생 가능성을 높입니다. 그렇기 때문에 예외 케이스도 새로운 정책으로 간주합니다.
  • 설계를 유연하게 만들 수 있었던 이유는 추상화에 의존하고, 생성자를 통해 의존성을 명시적으로 드러냈으며, new와 같은 구체 클래스를 직접적으로 다뤄야 하는 책임을 외부로 옮겼기 때문입니다. 의존하는 추상화 클래스에 자식 클래스를 추가함으로써 간단하게 사용될 컨텍스트를 확장할 수 있었습니다. 결합도를 낮춤으로써 얻게 되는 컨텍스트의 확장이라는 개념이 유연하고 재사용 가능한 설계를 만드는 핵심입니다.

조합 가능한 행동

  • 다양한 종류의 정책이 필요한 컨텍스트에서 클래스를 재사용할 수 있는 이유는 코드를 직접 수정하지 않고도 협력 대상인 인스턴스를 교체할 수 있기 때문입니다.
  • 어떤 객체와 협력하느냐에 따라 객체의 행동이 달라지는 것은 유연하고 재사용 가능한 설계가 가진 특징입니다. 유연하고 재사용 가능한 설계는 응집도 높은 책임들을 가진 작은 객체들을 다양한 방식으로 연결함으로써 애플리케이션의 기능을 쉽게 확장할 수 있습니다.
  • 유연하고 재사용 가능한 설계는 객체가 어떻게(how) 하는지를 장황하게 나열하지 않고도 객체들의 조합을 통해 무엇(what)을 하는지를 표현하는 클래스들로 구성됩니다. 따라서 클래스의 인스터스를 생성하는 코드를 보는 것만으로 객체가 어떤 일을 하는지를 쉽게 파악할 수 있습니다. 코드에 드러난 로직을 해석할 필요 없이 객체가 어떤 객체와 연결됐는지를 보는 것만으로도 객체의 행동을 쉽게 예상하고 이해할 수 있기 때문입니다. 다시 말해 선언적으로 객체의 행동을 정의할 수 있는 것입니다.
  • 유연하고 재사용 가능한 설계는 작은 객체들의 행동을 조합함으로써 새로운 행동을 이끌어 낼 수 있는 설계입니다. 훌륭한 객체지향 설꼐란 객체가 어떻게 하는지를 표현하는 것이 아니라 객체들의 조합을 선언적으로 표현함으로써 객체들이 무엇을 하는지를 표현하는 설계입니다. 그리고 이런 설계를 창조하는 데 있어서의 핵심은 의존성을 관리하는 것입니다.
  • 객체지향 시스템은 협력하는 객체들의 네트워크로 구성돼 있습니다. 시스템은 객체를 생성해 서로 메시지를 주고 받을 수 있게 조립하는 과정을 거쳐 만들어집니다. 시스템의 행위는 객체의 조합(객체의 선택과 연결 방식)을 통해 나타나는 특성입니다. 따라서 시스템에 포함된 객체의 구성을 변셩해(절차적인 코드를 작성하기보다는 인스턴스 추가나 제거 또는 조합을 달리해서) 시스템의 작동 방식을 바꿀 수 있습니다. 이러한 객체 구성을 관리할 목적으로 작성하는 코드를 객체 네트워크의 행위에 대한 선언적인 정의라고 합니다. 시스템을 이런 방식으로 구축하면 방법(how)이 아니라 목적(what)에 집중할 수 있어 시스쳄의 행위를 변경하기가 더 쉽습니다.

참고