Ch8 의존성 관리

본문


들어가며

6장까지 이르며 자율적인 객체, 캡슐화, 응집도, 결합도에 대한 개념과 책임 주도 설계 에 따른 책임 할당 방법을 학습했다.

또한 책임 주도 설계에 더하여 품질 높은 퍼블릭 인터페이스를 얻기 위한 기법으로 협력의 경로를 통제함으로써 결합도를 낮추는 디미터 법칙 , 디미터 법칙을 위한 스타일인 묻지말고 시켜라 원칙 , 협력에 어울리는 오퍼레이션을 얻기 위한 클라이언트의 의도를 드러내는 인터페이스 , 예측 가능한 코드를 위한 명령-쿼리 분리 원칙 에 대하여 배웠다.

이번 장에서는 객체지향 설계에서 아주 중요한 의존성 관리에 대해서 학습한다.

이번 장에서 반드시 기억해야 할 점은 컨텍스트 독립성이다. 즉, 특정한 문맥에 종속적인 컨텍스트에 의존하는 것이 아니라 충분히 추상적인 컨텍스트에 의존함으로써 동일한 소스코드로 다양한 컨텍스트의 협력이 가능하다.

여기서 다양한 컨텍스트란 단순히 한 가지 행동이 다른 객체정도로 생각하면 오산이다.

말 그대로 컨텍스트실행 문맥을 의미한다. 물론 추상적인 컨텍스트라는 의미가 리스코프 치환 원칙을 위배한다는 의미는 아니지만, 서로 다른 컨텍스트서로 다른 협력을 구성할 수 있다는 것은 상당히 의미가 크다.

이를테면 DB에 저장하는 컨텍스트와 File에 저장하는 컨텍스트는 저장이라는 동일한 행위를 수행하지만 협력 구조는 현저히 다르다. 단순히 save() 라는 하나의 메서드 내의 구현만 조금 다른것이 아니라 해당 객체들이 협력하는 대상까지 생각해본다면 짐작이 될 것이다.

그리고 이러한 추상적인 컨텍스트에 의존함으로써 다양한 컨텍스트의 협력이 가능하게 해주는 것은 바로 런타임 의존성컴파일타임 의존성을 서로 다르게 구성하는 것이다.

자세한 내용은 본문을 확인하자.


핵심.

  • 일반적인 애플리케이션의 기능을 구현하기 위해서는 다른 객체에게 도움을 요청해야 한다. 이런 요청이 객체사이의 협력을 낳는다.

  • 다른 객체와 협력하기 위해서는 해당 객체가 존재한다는 사실을 알고있어야 한다. 또한 객체가 수신할 수 있는 메시지에 대해서도 알고있어야 한다. 이런 지식이 객체 사이의 의존성을 낳는다.

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

  • 의존성은 방향성을 가지며 항상 단방향이다.

  • 두 요소 사이의 의존성의존되는 대상(서버)가 변경될 때 의존하는 요소(클라이언트)도 변경될 수 있는 “가능성”을 의미한다.

  • 의존성은 변경에 의한 영향의 전파 가능성(파급 효과가 있을 수 있음)을 의미한다.

  • 의존성은 여러 형태(인스턴스 변수, 메서드 인자, 구현 및 상속 대상, 아예 별개의 시스템 등)로 나타날 수 있지만 근본적인 특성은 동일하다. 의존하는 대상이 변경될 때 함께 변경될 수 있다는 것이다.

  • 변경과 관련있는 어떤 것이라도 의존성이라는 개념을 적용할 수 있다. 의존성의 대상은 객체일 수도, 모듈이나 더 큰 규모의 실행 시스템일 수도 있다. 하지만 의존성의 본질은 변하지 않는다.

  • 런타임 의존성컴파일타임 의존성을 구분해야 한다.

  • 런타임 의존성이 다루는 주제는 객체 사이의 의존성이고 컴파일타임 의존성이 다루는 주제는 클래스 사이의 의존성이다.

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

  • 클래스는 자신과 협력할 객체의 구체적인 클래스를 알아서는 안된다. 구체적인 클래스를 알면 알수록(더 자세히 알수록) 그 클래스가 사용되는 “특정한 문맥”에 강하게 결합되기 때문이다.

  • 클래스가 특정 문맥에 강하게 결합될수록 다른 문맥에서 사용하기는 더 어려워진다.

  • 클래스가 사용될 특정한 문맥에 대해 “최소한의 가정”만으로 이뤄져 있다면 다른 문맥에서 “재사용”하기 더 수월해지는데 이를 “컨텍스트 독립성”이라 부른다.

  • 설계가 유연해지기 위해서는 가능한 자신이 실행 될 컨텍스트에 대한 구체적인 정보를 최대한 적게알아야 한다. 컨텍스트에 대한 정보가 적으면 적을수록 더 다양한 컨텍스트에서 재사용 가능하다.

  • 컴파일타임 의존성은 구체적인 런타임 의존성으로 대체돼야 한다.

  • 컴파일타임 의존성을 실행 컨텍스트에 맞는 적절한 런타임 의존성으로 교체하는 것을 의존성 해결이라고 부른다.

  • 의존성 해결을 위해서는 일반적으로 생성자 주입, setter 메서드 주입, 메서드 실행 시 인자로 주입하는 세 가지 방법을 사용한다.

  • 바람직한 의존성이란 재사용성과 관련이 있다. 어떤 의존성이 다양한 환경에서 클래스를 재사용할 수 없도록 제한한다면 바람직하지 못한 의존성이다. 어떤 의존성이 다양한 환경에서 재사용할 수 있다면 그 의존성은 바람직한 것이다.

  • 특정한 컨텍스트에 강하게 결합된 의존성은 바람직하지 않은 의존성이다.

  • 특정한 컨텍스트에 강하게 의존하는 클래스를 다른 컨텍스트에서 재사용 할 수 있는 유일한 방법은 구현을 변경하는 것 뿐이다.

  • 다른 환경에서 재사용하기 위해 내부 구현을 변경하게 만드는 모든 의존성은 바람직하지 않은 의존성이다.

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

  • 지식이 결합을 낳는다.

  • 결합도는 상대적이다. 추상적인 인터페이스에 의존하는 것과 세부 구현이 드러나는 인터페이스에 의존하는 것, 구체적인 클래스에 의존하는것과 추상화에 의존하는것 모두 상대적으로 결합도를 비교할 수 있다.

  • 결합도의 정도는 한 요소가 자신이 의존하고 있는 대상에 대해 알고있는 정보의 양으로 결정된다.

  • 더 많은 정보를 알고 있을수록 두 요소는 강하게 결합된다.

  • 결합도를 느슨하게 유지하려면 협력하는 대상에 대해 더 적게 알아야 한다.

  • 결합도를 느슨하게 만들기 위해서는 협력하는 대상에 대해 필요한 정보 외에는 최대한 감추는 것이 중요하다.

  • 추상화에 의존하면 결합도를 느슨하게 유지할 수 있다.

  • 추상화란 구체적인 절차나 세부내용을 의도적으로 감추고 공통적이고 일반적인 부분만을 드러냄으로써 복잡도를 극복하는 방법이다.

  • 추상화를 사용하면 현재 다루고 있는 문제를 해결하는 데 불필요한 정보를 감출 수 있다. 따라서 대상에 대해 알아야 하는 지식의 양을 줄일 수 있기 때문에 결합도를 느슨하게 유지할 수 있다.

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

  • 의존성명시적으로 표현되어야 한다. 의존성을 구현 내부에 숨겨두지 마라

  • 유연하고 재사용 가능한 설계란 퍼블릭 인터페이스를 통해 의존성이 명시적으로 드러나는 설계다.

  • 명시적인 의존성을 사용해야만 퍼블릭 인터페이스를 통해 컴파일 타임 의존성을 적절한 런타임 의존성으로 교체할 수 있다.

  • 사용과 생성의 책임을 분리하고 의존성을 생성자에 명시화하고 구체 클래스가 아닌 추상화에 의존하게 함으로써 설계를 유연하게 할 수 있다. 그 출발은 객체를 생성하는 책임을 객체 내부가 아니라 클라이언트로 옮기는 것이다.

  • 어떤 경우든 코드 내부를 직접 수정하는 것은 버그의 발생 가능성을 높이는 것이다.

  • 어떤 객체와 협력하느냐에 따라 객체의 행동이 달라지는 것은 유연하고 재사용 가능한 설계가 가진 특징이다.

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

  • 유연하고 재사용 가능한 설계는 작은 객체들의 행동을 조합함으로써 새로운 행동을 이끌어낼 수 있는 설계다.


의존성 이해하기

변경과 의존성

시작에 앞서 먼저 코드를 살펴보자.

어떤 객체가 예정된 작업을 정상적으로 수행하기 위해 다른 객체를 필요로 하는 경우 두 객체 사이에 의존성이 존재한다고 말한다. 의존성은 방향을 가지며 항상 단방향이다.

두 요소 사이의 의존성은 의존되는 대상(서버)가 변경될 때 의존하는 요소(클라이언트)도 함께 변경될 수 있다는 것을 의미한다. 따라서 의존성은 변경에 의한 영향의 전파 가능성을 암시한다.

위 코드에서 PeriodConditionDiscountCondition, DayOfWeek, LocalTime, Screening에 대한 의존성을 가진다. 다시말해 어떤 형태로든 DiscountCondition, DayOfWeek, LocalTime, Screening이 변경된다면 PeriodCondition도 함께 변경될 수 있다는 것이다.

DiscountCondition, DayOfWeek, LocalTime, Screening이 각각 인터페이스, 인스턴스 변수, 메서드 인자로 그 형태들은 다르지만 의존성이 가지는 근본적인 특성은 동일하다. 의존하는 대상이 변경될 경우 그 영향이 전파 될 수 있다는 것이다.

여기서 의존성을 코드 상의 클래스(인터페이스), 인스턴스 변수 등으로 예를 들었지만 변경과 관련이 있는 어떤 것에도 의존성이라는 개념을 적용할 수 있다.

의존성의 대상은 객체일 수도 있고 모듈이나 더 큰 규모의 실행 시스템일 수 도 있다. 하지만 의존성의 본질은 변하지 않는다. 의존성이란 의존하고 있는 대상의 변경에 영향을 받을 수 있는 가능성이다.

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

의존성과 관련하여 다루어야 할 중요한 주제는 런타임 의존성컴파일타임 의존성의 구분이다. 런타임 의존성은 말 그대로 코드가 실행되고 있는 중의 의존성을 의미한다. 하지만 컴파일타임 의존성이란 굳이 컴파일 중의 의존성이 아니라 코드 작성 시점의 의존성을 의미한다.

런타임의 주인공은 객체이다. 따라서 런타임 의존성이 다루는 주제는 객체 사이의 의존성이다.

컴파일타임의 주인공은 클래스이다. 따라서 컴파일타임 의존성이 다루는 주제는 클래스 사이의 의존성이다.

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

아래 DiscountPolicy와 협력하는 Movie 코드를 살펴보자.

위의 코드 상에서 Movie추상클래스인 DiscountPolicy에만 의존한다. 하지만 런타임 의존성을 살펴보면 상황이 완전히 달라진다.

금액 할인 정책을 적용하기 위해서는 AmountDiscountPolicy의 인스턴스와 협력하고 비율 할인 정책을 적용하기 위해서는 PercentDiscountPolicy의 인스턴스와 협력해야 한다. 코드를 작성하는 시점의 Movie 클래스AmountDiscountPolicy 클래스PercentDiscountPolicy 클래스에 대해 전혀 알지 못하지만 런타임 시점의 Movie 인스턴스AmountDiscountPolicy 인스턴스PercentDiscountPolicy 인스턴스와 협력할 수 있어야 한다.

코드 작성 시점의 Movie 클래스는 할인 정책을 구현한 두 클래스의 존재를 모르지만 실행 시점의 Movie 객체는 두 클래스의 인스턴스와 협력할 수 있어야 한다. 유연하고 재사용 가능한 설계를 창조하기 위해서는 동일한 소스코드 구조를 가지고 다양한 실행 구조를 만들 수 있어야 한다.

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

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

코드 구조는 컴파일 시점에 확정되는 것이고 이 구조에는 고정된 클래스 상속 관계들이 포함된다. 그러나 프로그램의 실행 시점 구조는 협력하는 객체에 따라서 달라질 수 있다. 즉, 두 구조는 전혀 다른 별개의 독립성을 갖는다.

컨텍스트 독립성

우리는 이제 유연하고 확장 가능한 설계를 위해서 컴파일타임 의존성과 런타임 의존성이 달라야 한다는 사실을 이해했다. 클래스는 자신과 협력할 객체의 구체적인 클래스에 대해서 알아서는 안된다. 구체적인 클래스를 알면 알수록 그 클래스가 사용되는 특정한 문맥에 강하게 결합되기 때문이다. (여기서 클래스라고 표현했음에 유의하자. 클래스는 코드 상에 표현되는, 즉 컴파일 타임 시점의 코드 구조에 대한 이야기다.)

코드 구조(컴파일 타임 시점)에서 구체 클래스에 대해 의존하는 것은 어떤 문맥에서 사용될 것인지를 구체적으로 명시하는 것과 같다. 만일 위 코드에서 Movie 클래스가 의존하는 대상이 AmountDiscountPolicy라는 구체적인 클래스였다면, Movie가 금액 할인 정책이 적용된 영화의 요금을 계산하는 문맥에서 사용될 것이라는 것을 가정하는 것이다.

반면 DiscountCondition과 같이 추상클래스에 의존하는 것은 Movie가 할인 정책에 따라 요금을 계산하지만 구체적으로 어떤 정책을 따르지는 결정하지 않았음을 의미한다. 이 경우 구체적인 문맥은 컴파일타임 의존성을 어떤 런타임 의존성으로 대체하는지에 따라 달라진다.

코드 구조상의 클래스가 특정한 문맥에 강하게 결합될수록 다른 문맥에서 사용되기는 더 어려워진다. 클래스가 사용될 특정한 문맥에 대해 최소한의 가정만으로 이뤄져 있다면 다른 문맥에서 재사용되기가 더 수월해진다. 이를 컨텍스트 독립성이라 말한다.

컨텍스트 독립성에 대한 사색 start

컨텍스트 독립성에 대해서 조금 더 깊게 생각해보자. 독립성은 상대적인 개념으로 해석하자. 이게 무슨말이냐면, saveToTarget()saveToDB()saveToFile()는 비교하여 컨텍스트 독립적이다. 다시말해 어딘가에 저장한다.라는 컨텍스트에서 얼마든지 재사용이 가능하고 유연하게 확장할 수도 있다.(DB, File, other server 등)

하지만 saveToTarget()do()를 비교해보자. 어떤게 더 컨텍스트 독립적일까? 당연히 do()가 훨씬 더 컨텍스트에 독립적이다. 사실상 do()저장을 뛰어 넘어 어떤 컨텍스트에서든 무한히 재활용 할 수 있다.

그렇다면 saveToTarget()보다 do()에 의존하는 것이 더 좋은 선택일까?

상황에 따라 다르겠지만 대부분의 경우에서, 아니 정말 특이한 상황이 아니라면 saveToTarget()에 의존하는 것이 더 좋은 선택이라고 할 수 있을 것 같다.

즉, 어느정도의 컨텍스트 경계선은 필요하다. 라는 생각이다. 물론 그 경계선이 적당한지 적당하지 않은지는 변경의 정도를 식별하는 것이 가장 중요할 것 같다.

여기서 변경의 정도를 식별한 충분히 추상적인 컨텍스트란 무엇일까? 본문의 예시 에서는 구체적인 할인 조건, 구체적인 할인 정책이 변경되는 요소라고 판단하였기 때문에 이를 추상화 하여 DiscountCondition, DiscountPolicy로 표현하였다. 즉, "할인" 조건, "할인" 정책이라는 개념 자체는 유지가 필요한, 변경되지 않는 컨텍스트의 경계선 이라고 판단한 것이다.

만일 할인이라는 개념 자체가 변경될 수 있는 대상이라고 판단되는 경우(ex: 영화 과금 조건, 영화 과금 정책)에는 할인 조건, 할인 정책조건, 정책에 비해 구체적인 컨텍스트가 된다.

즉, 과금 조건, 과금 정책이라는 컨텍스트에서 재사용 할 수 없게 되어버리는 것이다.

이를 해결하기 위한 "할인"이라는 컨텍스트에 독립적인 개념은

  • DiscountCondition -> Condition
  • DiscountPolicy -> Policy

가 될 것이다.

조금 장황하게 설명한 감이 없지않아 있지만 내가 사색한 부분은 간단하다.

컨텍스트 독립성에서 “독립”적이다 라는 것은 상대적이다. 필요한 것은 “충분히 독립적”인 컨텍스트 이다.

“충분히 독립적”인 컨텍스트를 얻기 위한 방법은 이미 앞에서 모두 배웠다. ‘변경되는 불안정한 부분’과 ‘변경되지 않는 안정적인 부분’을 식별하고 변경되는 부분을 추상화/캡슐화 함으로써 변경되지 않는 안정적인 부분을 경계선으로 삼는다.

사실 사색할 땐 잘 몰랐는데 이렇게 적고 나니까 캡슐화/추상화/변경 식별에서 배운 내용과 흐름이 일치해서 조금 놀랐다.

사색 end

설계가 유연해지기 위해서는 가능한 한 자신이 실행될 컨텍스트에 대한 구체적인 정보를 충분히 적게 알아야 한다. 컨텍스트에 대한 정보가 충분히 적을수록(너무 적어도 안된다.) 더 다양한 컨텍스트에서 재사용 될 수 있다.

컨텍스트 독립적이라는 말은 각 객체가 해당 객체를 실행하는 시스템에 대해 아무것도 알지 못한다는 의미다.

그렇다면 클래스가 실행 컨텍스트에 독립적(구체적으로 실행할 컨텍스트를 모름)인데도 어떻게 런타임에 실행 컨텍스트에 적절한 객체들과 협력할 수 있을까?

의존성 해결

추상적인 컴파일타임 의존성은 구체적인 런타임 의존성으로 대체되어야 한다.

컴파일타임 의존성을 실행 컨텍스트에 맞는 적절한 런타임 의존성으로 대체하는 것을 의존성 해결이라고 부른다. 의존성 해결을 위해서 보통 세 가지 방법을 사용한다.

  1. 객체 생성 시점에 생성자를 통해 의존성 해결
  2. 객체 생성 후 setter 메서드를 통해 의존성 해결
  3. 메서드 실행 시 메서드 인자를 활용해 의존성 해결

말 그대로 의존성 주입 을 통해 실행 시점에 적절한 컨텍스트에 의존하도록 만드는 것이다.

유연한 설계

유연하고 재사용 가능한 설계를 위해서 충분히 독립적인(추상적인) 컨텍스트에 의존해야 한다는 점을 학습했다. 그렇다면 이제 의존성을 관리하기 위한 몇 가지 기법에 대해 알아보자.

의존성과 결합도

객체들이 협력하기 위해서는 서로의 존재와 수행 가능한 책임을 알아야 한다. 이런 지식들이 객체 사이의 의존성을 낳는다. 따라서 모든 의존성이 나쁜 것은 아니다. 의존성은 객체들의 협력을 가능하게 만드는 매개체라는 관점에서 바람직한 것이다. 하지만 의존성이 과하면 문제가 될 수 있다.

문제는 의존성이 아니라 의존성의 정도이다. 바람직한 의존성은 재사용성과 관련이 있다. 어떤 의존성이 다양한 환경에서 클래스를 재사용할 수 없도록 제한한다면 그 의존성은 바람직하지 못한 것이다. 어떤 의존성이 다양한 환경에서 재사용 할 수 있다면 그 의존성은 바람직 한 것이다.

결국 특정한 컨텍스트에 강하게 결합된 의존성은 바람직하지 않은 의존성이다. 라는 말로 귀결되는데 이는 앞에서 학습한 바와 같이 충분히 독립적인(추상적인) 컨텍스트에 의존하는 것이 바람직한 의존성이다.

다른 환경에서 재사용하기 위해서 내부 구현을 변경하게 만드는 모든 의존성은 바람직하지 않은 의존성이다. 바람직한 의존성이란 컨텍스트에 독립적인 의존성을 의미하며 다양한 환경에서 재사용될 수 있는 가능성을 열어놓은 의존성을 의미한다.

바람직한 의존성이란 느슨한 결합도를 의미하고 바람직하지 않은 의존성이란 단단한 결합도를 의미한다.

결합도재사용성과도 관련이 깊은데, 어떤 의존성이 재사용성이 낮다면 높은 결합도를, 재사용성이 높다면 낮은 결합도를 의미한다.

여기서 잠깐, 우리는 앞서 결합도지식의 정도와 관련이 깊다고 배웠다. 더 많이 알 수록 결합도가 강하고 더 적게 알 수록 결합도가 느슨하다 라고 배웠다.

근데 여기서는 재사용성결합도를 논하는데 그렇다면 결합도, 지식의 양, 재사용성은 서로 무슨 관계가 있을까?

결론부터 이야기 하자면 의존하는 대상에 대해 더 많은 정보를 알 수록(지식의 양이 많을 수록) 재사용성은 낮아지며 높은 결합도를 갖게 된다.

지식이 결합을 낳는다.

앞에서 MovieAmountDiscountPolicy에 의존하는 경우 강한 결합도를 갖는다고 표현하였다. 반면 MovieDiscountPolicy에 의존하는 경우 느슨한 결합도를 갖는다고 표현하였다.

결합도의 정도는 자신이 의존하고 있는 대상에 대해 알고있는 정보의 으로 결정된다. 다시말해 어떤 대상에 대해 많은 정보를 알고 있을수록 두 요소는 강하게 결합된다.

서로에 대해 알고 있는 지식의 양이 결합도를 결정한다.

Movie 클래스가 구체 클래스인 PercentDiscountPolicy에 직접 의존한다고 가정해보자. 이 경우 Movie는 협력할 객체가 비율 할인 정책에 따라 할인 요금을 계산할 것이라는 사실을 알고있다.

반면 Movie 클래스가 추상 클래스인 DiscountPolicy에 의존하는 경우에는 구체적인 계싼 방법이ㅔ 대해 알 필요가 없다. 그저 할인 요금을 계산한다는 사실만 알고 있을 뿐이다. 따라서 Movie가 PercentDiscountPolicy에 의존하는 것 보다 DiscountPolicy에 의존하는 경우 알아야 하는 지식의 양이 적기 때문에 결합도가 느슨해지는 것이다.

더 많이 알 수록 더 많이 결합된다.

더 많이 알고 있따는 것은 더 적은 컨텍스트에서 재사용 가능하다는 것을 의미한다. 기존 지식에 어울리지 않는 컨텍스트에서 클래스의 인스턴스를 사용하기 위해 할 수 있는 유일한 방법은 클래스를 수정하는 것 뿐이다.

결합도를 느슨하게 유지하려면 협력하는 대상에 대해 더 적게 알아야 한다. 결합도를 느슨하게 만들기 위해서는 협력하는 대상에 대해 필요한 정보 외에는 최대한 감추는 것이 중요하다.

결합도와 지식, 변경에 대한 사색 start

결합도는 세 가지 관점에서 이야기 할 수 있다.

  1. 변경과 관련한 결합도
  2. 지식과 관련된 결합도
  3. 재사용성과 관련된 결합도

세 가지 관점은 서로 긴밀한 관련이 있다. 다만 어느 부분에 초점을 맞추느냐의 차이다.

먼저 변경과 관련한 결합도는 앞에서 충분히 학습하였다. 어떤 클래스를 수정했을 때 그 파급효과로 인해 다른 클래스까지 수정해야 한다면 높은 결합도를 갖는다고 이야기 한다. 즉, 변경의 여파가 외부까지 미친다면 결합도가 높은 것이다.

물론 의존하는 대상의 내부 구현(상태 등)에 대한 정보를 앎으로써(지식이 많음) 재사용성이 떨어지고 변경에 대한 파급효과가 크다 는 점에서는 지식, 재사용성, 변경과 모두 관련이 있다.

그리고 지식과 관련하여서는 많은 정보를 안다라는 것이 상대적임을 이해하여야 한다. 이 지식구체적인 클래스의 내부 구현(상태 등)까지 미치는 것과 구체적인 클래스까지 미치는 것을 비교한다면 당연히 전자가 더 많은 정보를 알고 있기 때문에 결합도가 더 높은 것임을 짐작 할 수 있다.

반면 구체적인 클래스에 대해 아는것과 추상적인 역할에 대해 아는 것을 비교한다면 구체적인 클래스를 아는 것이 더 결합도가 높은 것임을 짐작 할 수 있다.

이 역시도 마찬가지로 변경재사용성을 기준으로 설명이 가능한데, 구체적인 클래스의 내부 구현에 대한 정보 > 구체적인 클래스 > 추상적인 역할 순으로 변경에 취약하다. (재사용성은 사실 구체적인 클래스를 아는 것과 내부 구현까지 아는 것이 거의 비슷한 것 같다.)

이번 사색에서 정리하고 싶은 것은 의존성의 정도와 결합도는 서로 관련이 있다는 것이고, 이 결합도변경, 지식, 재사용성 측면에서 살펴볼 수 있다는 점이다.

다만 이전에는 변경에 주목하여 학습했다면 이번 챕터에서는 재사용성에 주목했다는 점이다.

어떤 관점에서 살펴보든 결합도를 낮추기 위한 가장 효과적인 방법은 바로 ‘추상화’에 의존하는 것이다.

사색 end

추상화에 의존하라

추상화란 구체적인 절차나 세부내용을 의도적으로 감추고 공통적이고 일반적인 부분만을 드러냄으로써 복잡도를 극복하는 방법이다.

추상화를 사용하면 현재 다루고 있는 문제를 해결하는 데 불필요한 정보를 감출 수 있다.

여기서 이야기하는 추상화란 단순히 추상클래스, 인터페이스만을 의미하는 것이 아니다. 보다 충분히 추상적인 오퍼레이션과 같이 오퍼레이션 레벨에서도 추상화에 의존함으로써 불필요한 정보를 감추고 결합도를 낮춤으로써 변경의 여파를 통제할 수 있다.

클래스 레벨에서 살펴본다면 아래로 갈수록 클라이언트가 알아야 할 지식의 양이 적어지기 때문에 결합도가 느슨해진다.

  • 구체 클래스 의존성

  • 추상 클래스 의존성

  • 인터페이스 의존성

구체 클래스에 비해 추상 클래스는 내부 구현과 자식 클래스의 종류에 대한 지식 을 숨길 수 있다. 하지만 클라이언트는 여전히 협력하는 대상이 속한 클래스 상속 계층이 무엇인지에 대해서는 알고 있어야 한다.

인터페이스에 의존하면 클래스 상속 계층을 모르더라도 협력이 가능해진다. 이것은 다양한 클래스 상속 계층의 각 객체들이 동일한 메시지를 수신할 수 있도록 컨텍스트를 확장하는 것을 가능하게 한다.

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

명시적인 의존성

의존성은 명시적으로 퍼블릭 인터페이스(생성자든, setter든)에 드러내야 한다. 퍼블릭 인터페이스에 의존성이 명시적이지 않으면 의존성을 파악하기 위해 내부 구현을 직접 살펴볼 수 밖에 없다.

더 커다란 문제는 의존성이 명시적이지 않으면 클래스를 다른 컨텍스트에서 재사용하기 위해 내부 구현을 직접 변경해야 한다는 것이다.

따라서 의존성은 명시적으로 표현돼야 한다. 의존성을 구현 내부에 숨겨두지 마라. 유연하고 재사용 가능한 설계란 퍼블릭 인터페이스를 통해 의존성이 명시적으로 드러나는 설계다. 명시적인 의존성을 사용해야만 퍼블릭 인터페이스를 통해 컴파일타임 의존성을 적절한 런타임 의존성으로 교체할 수 있다.

인스턴스 생성과 사용을 분리하라.(의존성을 주입하라)

이는 명시적인 의존성과도 관련이 깊다.

만일 인스턴스를 사용하는 클라이언트가 생성을 직접 한다면 이는 구체적인 클래스에 의존하는 것과 다를 바와 없다.

해결 방법은 인스턴스를 생성하는 로직과 생성된 인스턴스를 사용하는 로직을 분리 하는 것이다. AmountDiscountPolicy를 사용하는 Movie는 인스턴스를 생성해서는 안된다. 단지 해당하는 인스턴스를 사용하기만 해야한다. 이를 위해 Movie는 외부로부터 이미 생성된 AmountDiscountPolicy의 인스턴스를 전달받아야 한다.

사용과 생성의 책임을 분리하고, 퍼블릭 인터페이스에 의존성을 명시화하고, 구체 클래스가 아닌 추상화에 의존하게 함으로써 설계를 유연하고 재사용 가능하게 만들 수 있다. 그리고 그 출발은 객체를 생성하는 책임을 객체 내부가 아니라 외부로부터 주입받는 것이다.

컨텍스트 확장하기

이제 Movie가 재사용 가능하고 유연할 설계를 갖는다는 것을 입증해보자. 즉, 다른 컨텍스트에서 Movie를 확장해서 재사용 하는 두 가지 예를 살펴보자.

  1. 할인 혜택을 제공하지 않는 영화
  2. 다수의 할인 정책을 중복해서 적용하는 영화

첫 번째는 할인 혜택을 제공하지 않는 영화의 예매 요금을 계산하는 경우이다. 쉽게 생각할 수 있는 방법은 discountPolicy에 어떤 객체도 할당하지 않는 것이다. 아래의 코드를 살펴보자.

위 코드는 정삭적으로 작동하지만 한 가지 문제가 있다. 지금까지의 Movie와 DiscountPolicy 사이의 협력 방식에 어긋나는 예외 케이스가 추가된 것이다. 그리고 이 예외 케이스를 처리하기 위해 Movie의 내부 코드를 직접 수정 해야 했다. 어떤 경우든 코드 내부를 직접 수정하는 것은 버그의 발생 가능성을 높이는 것이라는 점을 기억하자.

해결책은 할인 정책이 존재하지 않는다는 사실을 예외 케이스로 처리하지 말고 기존에 Movie와 DiscountPolicy가 협력하던 방식을 그대로 따르도록 만드는 것이다. 다시말해 할인 정책이 존재하지 않는다는 사실을 할인 정책의 한 종류로 간주하는 것이다.

이제 Movie에 예외 케이스를 위해 if문을 추가하지 않아도 할인 혜택을 제공하지 않는 영화를 구현할 수 있게 되었다. 간단히 NoneDiscountPolicy의 인스턴스를 Movie 생성자에 전달해주기만 하면 된다.

두 번째는 중복 적용이 가능한 할인 정책을 구현하는 것이다.

가장 간단한 방법은 MovieDiscountPolicy의 인스턴스들로 구성된 List를 인스턴스 변수로 갖게하는 것이다. 하지만 이 방법은 중복 할인 정책을 구현하기 위해 기존의 할인 정책의 협력 방식과는 다른 예외 케이스를 추가하게 만든다.

이 문제 역시 NoneDiscountPolicy와 같은 방법을 해결할 수 있다. 중복 할인 정책을 할인 정책의 한 가지로 간주하는 것이다. 이렇게 하면 기존의 MovieDiscountPolicy 사이의 협력 방식을 수정하지 않고도 여러개의 할인 정책을 적용할 수 있다.

이 예제는 Movie를 수정하지 않고도 컨텍스트를 확장하는 것이 얼마나 간단한지를 잘 보여준다.

Movie가 협력해야 하는 객체를 변경하는 것만으로도 Movie를 새로운 컨텍스트에서 재사용할 수 있기 때문에 Movie는 유연하고 재사용 가능하다.

설계를 유연하게 만들 수 있었던 이유는 MovieDiscountPolicy라는 추상화에 의존하고 생성자를 통해 의존성을 명시화하였으며 생성의 책임을 Movie 외부로 옮겼기 때문이다.

조합 가능한 행동

다양한 종류의 할인 정책이 필요한 컨텍스트에서 Movie를 재사용할 수 있었던 이유는 코드를 직접 수정하지 않고도 협력 대상인 DiscountPolicy 인스턴스를 교체할 수 있기 때문이다.

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

유연하고 재사용 가능한 설계는 객체가 어떻게(how) 하는지를 장황하게 나열하지 않고도 객체들의 조합을 통해서 무엇(what)을 하는지를 표현하는 클래스들로 구성된다. 다시말해 선언적으로 객체의 행동을 정의할 수 있는 것이다.

유연하고 재사용 가능한 설계는 작은 객체들의 행동을 조합함으로써 새로운 행동을 이끌어낼 수 있는 설계다. 훌륭한 객체지향 설계란 객체가 어떻게 하는지를 표현하는 것이 아니라 객체들의 조합을 선언적으로 표현함으로써 객체들이 무엇을 하는지를 표현하는 설계다.

참고 및 출처

  • 오브젝트