오브젝트

의존성 관리하기

객체지향 설계에 있어서 가장 많이 들어본 원칙들이 있을것이다. 그리고 그 중 하나가 바로 느슨한 결합도가 아닐까 싶다. 이번 장에서는 객체지향 설계에있어 중요한 부분인 느슨한 결합도를 위해 신경써야 하는 의존성 관리에 대해서 배워보려 한다. 의존성이란 무엇인지, 왜 의존성 관리가 중요한지, 그리고 어떻게 의존성을 해결하는지 차근차근 알아가보자.


들어가며

잘 설계된 객체지향 애플리케이션은 작고 응집도 높은 객체들로 구성된다. 작고 응집도 높은 객체란 책임의 초점이 명확하고 한 가지 일만 잘 하는 객체를 의미한다. 이런 작은 객체들이 단독으로 수행할 수 있는 작업은 거의 없기 때문에 일반적인 애플리케이션의 기능을 구현하기 위해서는 다른 객체에게 도움을 요청해야 한다. 이런 요청이 객체 사이의 협력을 낳는다.

협력은 필수적이지만 과도한 협력은 설계를 곤경에 빠뜨릴 수 있다.

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

협력을 위해서는 의존성이 필요하지만 과도한 의존성은 애플리케이션을 수정하기 어렵게 만든다.

객체지향 설계의 핵심은 협력을 위해 필요한 의존성을 유지하면서도 변경을 방해하는 의존성은 제거하는데 있다.

이런 관점에서 객체지향 설계란 의존성을 관리하는 것이고 객체가 변화를 받아들일 수 있게 의존성을 정리하는 기술이라고 할 수 있다.


의존성 이해하기

변경과 의존성

어떤 객체가 협력하기 위해 다른 객체를 필요로 할 때 두 객체 사이에 의존성이 존재하게 된다. 의존성은 실행 시점구현 시점에 서로 다른 의미를 갖는다.

실행 시점 : 의존하는 객체(클라이언트 객체)가 정상적으로 동작하기 위해서는 실행 시에 의존 대상 객체(서버, 의존 객체)가 반드시 존재해야 한다.

구현 시점 : 의존 대상 객체가 변경될 경우 의존하는 객체도 함께 변경된다.

설명을 잘 이해하기 위해 PeriodCondition 클래스를 이용해 의존성의 개념을 설명해보겠다.

실행 시점에 PeriodCondition의 인스턴스가 정상적으로 동작하기 위해서는 Screening의 인스턴스가 존재해야 한다. 만약 Screening의 인스턴스가 존재하지 않거나 getStartTime() 메시지를 이해할 수 없다면 PeriodConditionisSatisfiedBy() 메서드는 예상했던 대로 동작하지 않을 것이다.

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

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

Screening이 변경될 때 PeriodCondition이 영향을 받게 되지만 그 역은 성립하지 않는다. 이 경우 PeriodConditionScreening에 의존한다.

지금까지 주의깊게 살펴본 사람이라면 아마 설계와 관련된 대부분의 용어들이 변경과 관련이 있다는 사실을 눈치챘을 것이다. 두 요소 사이의 의존성은 의존 객체가 변경될 경우 클라이언트 객체도 함께 변경될 수 있다는 것을 의미한다.

의존성은 변경에 의한 영향의 전파 가능성을 암시한다.

PeriodCondition의 코드를 다시 살펴보자.

PeriodConditionDayOfWeekLocalTime의 인스턴스를 속성으로 포함한다. PeriodConditionisSatisfiedBy 메서드의 인자로 Screening의 인스턴스를 받는다. PeriodConditionDayOfWeek의 인스턴스에게 compareTo 메시지를 전송한다. PeriodConditionScreening의 인스턴스에게 getStartTime 메시지를 전송한다.

따라서 PeriodConditionDayOfWeek, LocalTime, Screening에 대해 의존성을 갖는다.

DayOfWeek의 클래스 명을 변경한다고 가정해보자. 이 경우 PeriodCondition 클래스에 정의된 인스턴스 변수의 타입 선언도 함께 수정해야 한다. 이것은 LocalTime, Screening에 대해서도 마찬가지다. 만일 DiscountCondition 인터페이스의 이름이 변경된다면 어떻게 될까? DiscountCondition에 선언된 isSatisfiedBy 오퍼레이션의 시그니처가 변경된다면 어떻게 될까? DiscountCondition의 인터페이스를 실체화 하고 있는 PeriodCondition 클래스 역시 어떤 식으로든 함께 수정해야 할 것이다. 여기서 요점은 어떤 형태로든 DayOfWeek, LocalTime, Screening, DiscountCondition이 변경된다면 PeriodCondition도 함께 변경될 수 있다는 것이다.

비록 위의 의존성들이 각자 인스턴스 변수, 메서드 인자, 인터페이스 타입과 같이 다른 성질을 갖고있지만 근본적으로 의존성이 갖는 특성은 동일하다. PeriodCondition은 자신이 의존하고 있는 대상이 변경될 때 함꼐 변경될 수 있다는 것이다.


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

의존성과 관련해서 다뤄야 하는 또 다른 주제는 런타임 의존성컴파일 타임 의존성의 차이다.

객체지향 애플리케이션에서 런타임의 주인공은 객체이다. 따라서 런타임 의존성이 다루는 주제는 객체 사이의 의존성이다.

반면 코드 관점에서의 주인공은 클래스이다. 따라서 컴파일 타임 의존성이 다루는 주제는 클래스 사이의 의존성이다.

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

영화 예매 시스템을 예로 들어보자 Movie는 가격을 계산하기 위해 비율 할인 정책금액 할인 정책 모두를 적용할 수 있게 설계해야 한다. 다시 말해서 MovieAmountDiscountPolicyPercentDiscountPolicy 모두와 협력할 수 있어야 한다. 이를 위해 AmountDiscountPolicyPercentDiscountPolicy가 추상 클래스인 DiscountPolicy를 상속받게 한 후 Movie가 이 추상 클래스에 의존하도록 클래스를 설계하였다.

여기서 중요한 것은 Movie클래스에서 AmountDiscountPolicy 클래스와 PercentDiscountPolicy 클래스로 향하는 어떤 의존성도 존재하지 않는다는 것이다. Movie 클래스는 오직 추상 클래스인 DiscountPolicy 클래스에만 의존한다. Movie 클래스의 코드를 보면 AmountDiscountPolicyPercentDiscountPolicy에 대해서는 언급조차 하지 않는다는 것을 알 수 있다.

하지만 런타임 의존성을 살펴보면 상황이 완전히 달라진다. 금액 할인 정책을 적용하기 위해서 AmountDiscountPolicy의 인스턴스와 협력해야 한다. 비율 할인 정책을 적용하기 위해서는 PercentDiscountPolicy의 인스턴스와 협력해야 한다. 코드를 작성하는 시점의 Movie 클래스는 AmountDiscountPolicy 클래스와 PercentDiscountPolicy에 대해서 전혀 알지 못하지만 실행 시점의 Movie 인스턴스는 AmountDiscountPolicy 인스턴스와 PercentDiscountPolicy 인스턴스와 협력할 수 있어야 한다.

만약 Movie 클래스가 AmountDiscountPolicy 클래스에 대해서만 의존한다면 PercentDiscountPolicy 인스턴스와 협력하는 것은 불가능 할 것이다. 물론 그 반대도 마찬가지 이다. Movie 클래스가 AmountDiscountPolicyPercentDiscountPolicy 클래스 둘 모두에 의존하도록 만드는 것은 좋은 방법이 아닌데, 이것은 Movie의 전체적인 결합도를 높일 뿐 아니라 새로운 할인 정책을 추가하기 위해서는 Movie 내부 구현 코드를 수정해야 하기 때문이다.

Movie의 인스턴스가 이 두 클래스의 인스턴스와 함께 협력할 수 있게 만드는 더 나은 방법은 Movie가 두 클래스 중 어떤 것도 알지 못하게 만드는 것이다. 대신 두 캘르스를 모두 포괄하는 DiscountPolicy라는 추상 클래스에 의존하도록 만들고 이 컴파일타임 의존성을 실행 시에 PercentDiscountPolicy 인스턴스나 AmountDiscountPolicy 인스턴스에 대한 런타임 의존성으로 대체해야 한다.

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

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

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


컨텍스트 독립성

이제 유연하고 확장 가능한 설계를 만들기 위해서는 컴파일타임 의존성런타임 의존성이 달라야 한다는 사실을 이해했을 것이다. 클래스는 자신과 협력할 객체의 구체적인 클래스에 대해 알아서는 안된다. 구체적인 클래스를 알면 알수록 그 클래스가 사용되는 특정한 문맥에 강하게 결합되기 때문이다.

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

Movie 클래스 안에 PercentDiscountPolicy 클래스에 대한 컴파일타임 의존성을 명시적으로 표현하는 것은 Movie비율 할인 정책이 적용된 영화의 요금을 계산하는 문맥에서 사용될 것이라는 것을 가정하는 것이다. 이와 달리 Movie 클래스에 추상 클래스인 DiscountPolicy에 대한 컴파일타임 의존성을 명시하는 것은 Movie가 할인 정책에 따라 요금을 계산하지만 구체적으로 어떤 정책을 따르는지는 결정하지 않았다고 선언하는 것이다. 이 경우 구체적인 문맥은 컴파일타임 의존성을 어떤 런타임 의존성으로 대체하느냐에 따라 달라질 것이다.

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

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

이제 마지막 연결고리만 남았다. 클래스가 실행 컨텍스트에 독립적인데도 어떻게 런타임에 실행 컨텍스트에 적절한 객체들과 협력할 수 있을까?


의존성 해결하기

컴파일타임 의존성은 구체적인 런타임 의존성으로 대체돼야 한다. 이처럼 컴파일타임 의존성을 실행 컨텍스트에 맞는 적절한 런타임 의존성으로 교체하는 것을 의존성 해결이라고 부른다. 의존성을 해결하기 위해서는 일반적으로 다음과 같은 세 가지 방법을 사용한다.

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

예를 들어, 어떤 영화의 요금 계산에 금액 할인 정책을 적용하고 싶다고 가정해보자. 다음과 같이 Movie 객체를 생성할 때 AmountDiscountPolicy의 인스턴스를 Movie의 생성자에 인자로 전달하면 된다.

Movie의 생성자에 PercentDiscountPolicy의 인스턴스를 전달하면 비율 할인 정책에 따라 요금을 계산하게 될 것이다.

이를 위해 Movie 클래스는 PercentDiscountPolicy 인스턴스와 AmountDiscountPolicy 인스턴스 모두를 선택적으로 전달받을 수 있도록 이 두 클래스의 부모 클래스인 DiscountPlicy 타입의 인자를 받는 생성자를 정의한다.

Movie의 인스턴스를 생성한 후에 메서드를 이용해 의존성을 해결하는 방법도 있다.

이 경우 Movie 인스턴스가 생성된 후에도 DiscountPolicy를 설정할 수 있는 setter 메서드를 제공해야 한다.

setter 메서드를 이용하는 방식은 객체를 생성한 이후에도 의존하고 있는 대상을 변경할 수 있는 가능성을 열어놓고 싶은 경우에 유용하다. 즉, 실행 시점에 의존 대상을 변경할 수 있기 때문에 설계를 좀 더 유연하게 만들 수 있다. 단점은 객체가 생성된 후에 협력에 필요한 의존 대상을 설정하기 때문에 객체를 생성하고 의존 대상을 설정하기 전 까지는 객체의 상태가 불완전할 수 있다는 것이다.

보다 좋은 방법은 생성자 방식과 setter 방식을 혼합하는 것이다. 항상 객체를 생성할 때 의존성을 해결해서 완전한 상태의 객체를 생성한 후, 필요에 따라 setter 메서드를 이용해 의존 대상을 변경할 수 있게 할 수 있다. 이 방법은 시스템의 상태를 안정적으로 유지하면서도 유연성을 향상시킬 수 있기 때문에 의존성 해결을 위해 가장 선호되는 방법이다.


유연한 설계

의존성과 결합도

객체지향 패러다임의 근간은 협력이다. 객체들은 협력을 통해 애플리케이션에 생명력을 불어넣는다. 객체들이 협력하기 위해서는 서로의 존재와 수행 가능한 책임을 알아야 한다. 이런 지식들이 객체 사이의 의존성을 낳는다. 따라서 모든 의존성이 나쁜것은 아니다. 의존성은 객체들의 협력을 가능하게 만드는 매개체라는 관점에서는 바람직한 것이다. 하지만 의존성이 과하면 문제가 될 수 있다.

Movie비율 할인 정책을 구현하는 PercentDiscountPolicy에 직접 의존한다고 가정해보자.

이 코드는 비율 할인 정책을 적용하기 위해 MoviePercentDiscountPolicy에 의존하고 있다는 사실을 코드를 통해 명시적으로 드러낸다. 문제는 의존성의 존재가 아니라 의존성의 정도이다. 이 코드는 MoviePercentDiscountPolicy라는 구체적인 클래스에 의존하게 만들었기 때문에 다른 종류의 할인 정책이 필요한 문맥에서 Movie를 재사용 할 수 있는 가능성을 없애버렸다. 만약 MoviePercentDiscountPolicy뿐만 아니라 AmountDiscountPolicy와도 협력해야 한다면 어떻게 해야할까?

해결 방법은 의존성을 바람직하게 만드는 것이다. Movie가 협력하고 싶은 대상이 반드시 PercentDiscountPolicy의 인스턴스일 필요는 없다는 사실에 주목하라. 사실 Movie의 입장에서는 협력할 객체의 클래스를 고정할 필요가 없다. 자신이 전송하는 calculateDiscountAmount 메시지를 이해할 수 있고 할인된 요금을 계산할 수만 있다면 어떤 타입의 객체와 협력하더라도 상관이 없다.

추상 클래스인 DiscountPolicycalculateDiscountAmount메시지를 이해할 수 있는 타입을 정의함으로써 이 문제를 해결한다. AmountDiscountPolicy 클래스와 PercentDiscountPolicy 클래스가 DiscountPolicy를 상속받고 Movie 클래스는 오직 DiscountPolicy에만 의존하도록 만듦으로써 DiscountPolicy 클래스에 대한 컴파일타임 의존성AmountDiscountPolicy 인스턴스와 PercentDiscountPolicy 인스턴스에 대한 런타임 의존성으로 대체할 수 있다.

바람직한 의존성이란 무엇인가? 바람직한 의존성은 재사용성과 관련이 있다.

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

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

특정한 컨텍스트에 강하게 의존하는 클래스를 다른 컨텍스트에서 재사용할 수 있는 유일한 방법은 구현을 변경하는 것 뿐이다. MoviePercentDiscountPolicy에 의존하고 있는 경우에 MovieAmountDiscountPolicy와 협력하도록 만들고 싶다면 어떻게 해야할까? 방법은 하나밖에 없다. percentDiscountPolicy의 타입을 PercentDiscountPolicy에서 AmountDiscountPolicy로 변경하는 것이다. 하지만 이 수정으로 인해 이번에는 MovieAmountDiscountPolicy에 강하게 결합된다. 결국 이것은 바람하지 못한 의존성을 또 다른 바람직하지 못한 의존성으로 대체한 것 뿐이다.

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

어떤 두 요소 사이에 의존성이 바람직 할 경우 느슨한 결합도라고 부르고 바람직하지 못할 경우 강한 결합도라고 부른다.

바람직한 의존성이란 설계를 재사용하기 쉽게 만드는 의존성이다. 바람직하지 못한 의존성이란 설계를 재사용하기 어렵게 만드는 의존성이다. 어떤 의존성이 재사용을 방해한다면 결합도가 강하다고 표현한다. 어떤 의존성이 재사용을 쉽게 허용한다면 결합도가 느슨하다고 표현한다.

Movie 클래스가 추상 클래스인 DiscountPolicy에 의존하면 AmountDiscountPolicyPercentDiscountPolicy 모두와 협력할 수 있다. 따라서 MovieDiscountPolicy는 느슨하게 결합된다.


지식이 결합을 낳는다.

앞에서 MoviePercentDiscountPolicy에 의존하는 경우에는 결합도가 강하다고 표현했다. 반면 MovieDiscountPolicy에 의존하는 경우에는 결합도가 느슨하다고 표현했다. 결합도의 정도는 한 요소가 자신이 의존하고 있는 다른 요소에 대해 알고있는 정보의 양으로 결정된다. 한 요소가 다른 요소에 대해 더 많은 정보를 알고 있을수록 두 요소는 강하게 결합된다. 반대로 한 요소가 다른 요소에 대해 더 적은 정보를 알고 있을수록 두 요소는 약하게 결합된다.

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

느슨한 결합도를 위한 방법으로 필요한 정보만을 제공하는 대상에 의존하는 방법은 어떤 것이 있을까? 바로 추상화이다.


추상화에 의존하라

추상화란 어떤 양상, 세부사항, 구조를 좀더 명확하게 이해하기 위해 특정 절차나 물체를 의도적으로 생략하거나 감춤으로써 복잡도를 극복하는 방법이다. 추상화를 사용하면 현재 다루고 있는 문제를 해결하는 데 불필요한 정보를 감출 수 있다. 따라서 대상에 대해 알아야 하는 지식의 양을 줄일 수 있기 때문에 결합도를 느슨하게 유지할 수 있다.

DiscountPolicy 클래스는 PercentDiscountPolicy 클래스가 비율 할인 정책에 따라 할인 요금을 계산한다는 사실을 숨겨주기 때문에 PercentDiscountPolicy의 추상화다. 따라서 Movie 클래스의 관점에서 협력을 위해 알아야 하는 지식의 양은 PercentDiscountPolicy보다 DiscountPolicy 클래스가 더 적다.

Movie와 DiscountPolicy 사이의 결합도가 더 느슨한 이유는 Movie가 구체적인 대상이 아닌 추상화에 의존하기 때문이다.

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

위 내용과 같이 구체 클래스가 가장 결합도가 강하고 그 다음으로 추상 클래스, 가장 느슨한 결합도를 얻을 수 있는 추상화는 인터페이스이다.

구체 클래스에 비해 추상 클래스는 메서드의 내부 구현과 자식 클래스의 종류에 대한 지식을 클라이언트에게 숨길 수 있다. 따라서 클라이언트가 알아야 하는 지식의 양이 더 적기 때문에 구체 클래스보다 추상 클래스에 의존하는 것이 결합도가 더 낮다. 하지만 추상 클래스의 클라이언트는 여전히 협력하는 대상이 속한 클래스 상속 계층이 무엇인지에 대해서는 알고 있어야 한다.

인터페이스에 의존하면 상속 계층을 모르더라도 협력이 가능해진다. 인터페이스 의존성은 협력하는 객체가 어떤 메시지를 수신할 수 있는지에 대한 지식만을 남기기 때문에 추상 클래스 의존성보다 결합도가 낮다. 이것은 다양한 클래스 상속 계층에 속한 객체들이 동일한 메시지를 수신할 수 있도록 컨텍스트를 확장하는 것을 가능하게 한다.

여기서 중요한 것은 실행 컨텍스트에 대해 알아야 하는 정보를 줄일수록 결합도가 더 낮아진다는 것이다. 결합도를 느슨하게 만들기 위해서는 구체적인 클래스보다는 추상 클래스에, 추상 클래스보다 인터페이스에 의존하도록 만드는 것이 더 효과적이다. 다시 말해 의존하는 대상이 더 추상적일수록 결합도는 더 낮아진다 이것이 핵심이다.


명시적인 의존성

아래 코드는 한 가지 실수로 인해 결합도가 불필요하게 높아졌다. 그 실수는 무엇일까?

Movie의 인스턴스 변수인 discountPolicy는 추상 클래스인 DiscountPolicy 타입으로 선언돼 있다. Movie는 추상화에 의존하기 때문에 이 코드는 유연하고 재사용 가능할 것처럼 보인다. 하지만 안타깝게도 생성자를 보면 그렇지 않다. discountPolicyDiscountPolicy 타입으로 선언돼 있지만 생성자에서 구체 클래스인 AmountDiscountPolicy의 인스턴스를 직접 생성해서 대입하고 있다. 따라서 Movie는 추상 클래스인 DiscountPlicy뿐만 아니라 구체 클래스인 AmountDiscountPolicy에도 의존하게 된다.

이 예제에서 알 수 있는 것처럼 결합도를 느슨하게 만들기 위해서는 인스턴스 변수의 타입을 추상 클래스나 인터페이스로 선언하는 것만으로는 부족하다.

클래스 안에서 구체 클래스에 대한 모든 의존성을 제거해야만 한다. 즉 의존성을 주입받으므로써 의존성 해결을 해야한다.

앞에서 설명했던 것처럼 의존성을 해결하는 방법에는 생성자, Setter, 메서드 인자를 사용하는 세 가지 방법이 존재한다. 여기서의 트릭은 인스턴스 변수의 타입은 추상 클래스나 인터페이스로 정의하고 생성자, Setter 메서드, 메서드 인자로 의존성을 해결할 때는 추상 클래스를 상속받거나 인터페이스를 실체화한 구체 클래스를 전달(주입) 하는 것이다.

다음은 생성자를 사용해 의존성을 해결하는 경우를 나타낸 것이다. 앞의 코드와 다른 점이라면 생성자 안에서 인스턴스를 직접 생성하지 않고 생성자의 읜자로 선언하고 있음을 알 수 있다. 여기서 눈여겨볼 부분은 인스턴스 변수의 타입과 생성자의 인자 타입 모두 추상 클래스인 DiscountPolicy로 선언돼 있다는 점이다.

생성자의 인자가 추상 클래스 타입으로 선언됐기 때문에 이제 객체를 생성할 때 생성자의 인자로 DiscountPolicy의 자식 클래스 중 어떤 것이라도 전달할 수 있다. 따라서 런타임에 AmountDiscountPolicy의 인스턴스나 PercentDiscountPolicy의 인스턴스를 선택적으로 전달할 수 있다. Movie 인스턴스는 생성자의 인자로 전달된 인스턴스에 의존하게 된다.

의존성의 대상을 생성자의 인자로 전달받는 방법과 생성자 안에서 직접 생성하는 방법 사이의 가장 큰 차이점은 퍼블릭 인터페이스를 통해 할인 정책을 설정할 수 있는 방법을 제공하는지 여부다. 생성자의 인자로 선언하는 방법은 MovieDiscountPolicy에 의존한다는 사실을 Movie퍼블릭 인터페이스에 드러내는 것이다. 이것은 Setter메서드를 사용하는 방식과 메서드 인자를 사용하는 방식의 경우에도 동일하다. 모든 경우에 의존성은 퍼블릭 인터페이스에 노출된다. 이를 명시적인 의존성이라고 부른다.

반면 Movie의 내부에서 AmountDiscountPolicy의 인스턴스를 직접 생성하는 방식은 MovieDiscountPolicy에 의존한다는 사실을 감춘다. 다시말해 의존성이 퍼블릭 인터페이스에 표현되지 않는다. 이를 숨겨진 의존성이라고 부른다.

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

커다란 클래스에 정의된 긴 메서드 내부 어딘가에서 인스턴스를 생성하는 코드를 파악하는 것은 쉽지 않다.

더 커다란 문제는 의존성이 명시적이지 않으면 다른 컨텍스트에서 재사용하기 위해 내부 구현을 직접 변경해야 한다. 코드 수정은 언제나 잠재적으로 버그의 발생 가능성을 내포한다. 의존성을 명시적으로 드러내면 코드를 직접 수정해야하는 위험을 피할 수 있다. 실행 컨텍스트에 적절한 의존성을 선택할 수 있기 때문이다.

의존성은 명시적으로 표현해야 한다. 의존성을 구현 내부에 숨겨두지 마라. 유연하고 재사용 가능한 설계란 퍼블릭 인터페이스를 통해 의존성이 명시적으로 드러나는 설계다.

다시한번 강조한다. 유연하고 재사용 가능한 설계란 퍼블릭 인터페이스를 통해 의존성이 명시적으로 드러나는 설게다. 명시적인 의존성을 사용해야만 퍼블릭 인터페이스를 통해 컴파일타임 의존성을 적절한 런타임 의존성으로 교체할 수 있다.


new는 해롭다.

대부분의 언어에서는 클래스의 인스턴스를 생성할 수 있는 new 연산자를 제공한다. 하지만 안타깝게도 new를 잘못 사용하면 클래스 사이의 결합도가 극단적으로 높아진다. 결합도 측면에서 new가 해로운 이유는 크게 두 가지다.

1. new 연산자를 사용하기 위해서는 구체 클래스의 이름을 직접 기술해야 한다. 따라서 new를 사용하는 클라이언트는 추상화가 아닌 구체 클래스에 의존할 수밖에 없기 때문에 결합도가 높아진다.

2. new 연산자는 생성하려는 구체 클래스뿐만 아니라 어떤 인자를 이ㅛㅇㅇ해 클래스의 생성자를 호출해야 하는지도 알아야 한다. 따라서 new를 사용하면 클라이언트가 알아야 하는 지식의 양이 늘어나기 때문에 결합도가 높아진다.

구체 클래스에 직접 의존하면 결합도가 높아진다는 사실을 기억하라. 결합도의 관점에서 구체 클래스는 협력자에게 너무 많은 지식을 알도록 강요한다. 여기에 new는 문제를 더 크게 만든다. 클라이언트는 구체 클래스를 생성하는 데 어떤 정보가 필요한지에 대해서도 알아야 하기 때문이다.

new는 결합도를 높이기 때문에 해롭다. new는 클래스를 구체 클래스에 결합시키는 것만으로 끝나지 않는다. 협력할 클래스의 인스턴스를 생성하기 위해 어떤 인자들이 필요하고 그 인자들을 어떤 순서로 사용해야 하는지에 대한 정보도 노출시킬뿐만 아니라 인자로 사용되는 구체 클래스에 대한 의존성을 추가한다.

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

외부에서 인스턴스를 전달받는 방법은 앞에서 살펴본 의존성 해결 방법과 동일하다. 생성자의 인자로 전달받거나, Setter 메서드를 사용하거나, 메서드의 인자로 전달하면 된다. 어떤 방법을 사용하건 Movie 클래스에는 AmountDiscountPolicy의 인스턴스에 메시지를 전송하는 코드만 남아있어야 한다.

그렇다면 인스턴스 생성의 책임은 누구에게 있는가? Movie의 클라이언트가 처리한다. 즉, Movie의 클라이언트가 AmountDiscountPolicy 인스턴스를 주입한다.

사용과 생성의 책임을 분리해서 Movie의 결합도를 낮추면 설계를 유연하게 만들 수 있다. Movie의 생성자가 구체 클래스인 AmountDiscountPolicy가 아니라 추상 클래스인 DiscountPolicy를 인자로 받아들이도록 선언돼 있다는 점에 주목하라. 생성의 책임을 클라이언트로 옮김으로써 이제 MovieDiscountPolicy의 모든 자식 클래스와 협력할 수 있게 됐다. 설계가 유연해진 것이다.

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

이 예제는 올바른 객체가 올바른 책임을 수행하는 것이 훌륭한 객체를 창조하는 기반이라는 사실을 잘 보여준다.


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

클래스 안에서 객체의 인스턴스를 직접 생성하는 방식이 유용한 경우도 있다. 주로 협력하는 기본 객체를 설정하고 싶은 경우가 여기에 속한다. 예를들어, Movie가 대부분의 경우에는 AmountDiscountPolicy의 인스턴스와 협력하고 가끔씩만 PercentDiscountPolicy와 협력한다고 가정해보자. 이런 상황에서 모든 경우에 인스턴스를 생성하는 책임을 클라이언트로 옮긴다면 클라이언트들 사이에 중복 코드가 늘어나고 Movie의 사용성도 나빠질 것이다.

이 문제를 해결하는 방법은 기본 객체를 생성하는 생성자를 추가하고 이 생성자에서 DiscountPolicy의 인스턴스를 인자로 받는 생성자를 체이닝 하는 것이다. 다음은 title과 runningTime을 인자로 받는 생성자를 추가한 Movie클래스를 나타낸 것이다.

추가된 생성자 안에서 AmountDiscountPolicy 클래스의 인스턴스를 생성한다는 것을 알 수 있다. 여기서 눈여겨볼 부분은 첫 번째 생성자의 내부에서 두 번째 생성자를 호출한다는 것이다. 다시 말해 생성자가 체인처럼 연결된다. 이제 클라이언트는 대부분의 경우에 추가된 간략한 생성자를 통해 AmountDiscountPolicy의 인스턴스와 협력하게 하면서도 컨텍스트에 적절한 DiscountPolicy의 인스턴스로 의존성을 교체할 수 있다.

이 방법은 메서드를 오버로딩하는 경우에도 사용할 수 있다. 다음과 같이 DiscountPolicy의 인스턴스를 인자로 받는 메서드와 기본값을 생성하는 메서드를 함께 사용한다면 클래스의 사용성을 향상시키면서도 다양한 컨텍스트에서 유연하게 사용될 수 있는 여지를 제공할 수 있다.

이 예는 설계가 트레이드오프 활동이라는 사실을 다시 한번 상기시킨다. 여기서 트레이드오프의 대상은 결합도와 사용성이다. 구체 클래스에 의존하더라도 클래스의 사용성이 더 중요하다면 결합도를 높이는 방향으로 코드를 작성할 수 있다. 그럼에도 가급적 구체 클래스에 대한 의존성을 제거할 수 있는 방법을 찾아보기 바란다. 종종 모든 결합도가 모이는 새로운 클래스를 추가함으로써 사용성과 유연성이라는 두 마리 토끼를 잡을 수 있는 경우도 있다. 이어지는 장에서 FACTORY를 살펴보길 바란다.


컨텍스트 확장하기

지금까지 Movie의 설계가 유연하고 재사용 가능한 이유에 대하여 장황하게 설명했다. 이제 실재로 Movie가 유연하다는 사실을 입증하기 위해 지금까지와는 다른 컨텍스트에서 Movie를 확장해서 재사용하는 두 가지 예를 살펴보겠다. 하나는 할인 혜택을 제공하지 않는 영화의 경우이고, 다른 하나는 다수의 할인 정책을 중복해서 적용하는 영화의 경우이다.

첫 번째는 할인 혜택을 제공하지 않는 영화의 예매 요금을 계산하는 경우다. 쉽게 생각할 수 있는 방법은 discountPolicy에 어떤 객체도 할당하지 않는 것이다. 다음과 같이 discountPolicynull 값을 할당하고 실제로 사용할 때는 null이 존재하는지 판단하는 방법을 사용할 수 있다.

앞에서 설명한 생성자 체이닝 기법을 이용해 기본값으로 null을 할당하고 있다는 점을 눈여겨 보기 바란다. discountPolicy의 값이 null인 경우에는 할인 정책을 적용해서는 안되기 때문에 calculateMovieFee 메서드 내부에서 discountPolicy의 값이 null인지 여부를 체크한다.

이 코드는 제대로 동작하지만 한 가지 문제가 있다. 지금까지의 MovieDiscountPolicy 사이의 협력 방식에 어긋나는 예외 케이스가 추가된 것이다. 그리고 이 예외케이스를 처리하기 위해 Movie의 내부 코드를 직접 수정해야 했다.

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

해결책은 할인 정책이 존재하지 않는다는 사실을 예외 케이스로 처리하지 말고 기존 Movie와 DiscountPolicy가 협력하던 방식을 따르도록 만드는 것이다. 다시 말해 할인 정책이 존재하지 않는다는 사실을 할인 정책의 한 종류로 간주하는 것이다. 방법은 간단하다. 할인할 금액을 0원으로 반환하는 NoneDiscountPolicy 클래스를 추가하고 DiscountPolicy의 자식 클래스로 만드는 것이다.

이제 Movie클래스에 if문을 추가하지 않고도 할인 혜택을 제공하지 않는 영화를 구현할 수 있다. 간단히 NoneDiscountPolicy의 인스턴스를 Movie의 생성자에 전달하면 되는 것이다.

두 번째 예는 중복 적용이 가능한 할인 정책을 구현하는 것이다. 여기서 중복 할인이란 금액 할인 정책과 비율 할인 정책을 혼합해서 적용할 수 있는 할인 정책을 의미한다. 할인 정책을 중복해서 적용하기 위해서는 Movie가 하나 이상의 DiscountPlicy와 협력할 수 있어야 한다.

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

이 문제 역시 NoneDiscountPolicy와 같은 방법을 사용해서 해결할 수 있다. 중복 할인 정책을 할인 정책의 한 종류로 간주하는 것이다. 중복 할인 정책을 구현하는 OverlappedDiscountPolicyDiscountPolicy의 자식 클래스로 만들면 기존의 MovieDiscountPolicy 사이의 협력 방식을 수정하지 않고도 여러개의 할인 정책을 적용할 수 있다.

이제 OverlappedDiscountPolicy의 인스턴스를 생성해서 Movie에 전달하는 것만으로도 중복할인을 쉽게 적용할 수 있다.

이 예제는 Movie를 수정하지 않고도 할인 정책을 적용하지 않는 새로운 기능을 추가하는 것이 얼마나 간단한지를 잘 보여준다. 우리는 단지 원하는 기능을 구현한 DiscountPolicy의 자식 클래스를 추가하고 이 클래스의 인스턴스를 Movie에 전달하기만 하면 된다. Movie가 협력해야 하는 객체를 변경하는 것만으로도 Movie를 새로운 컨텍스트에서 재사용 할 수 있기 때문에 Movie는 유연하고 재사용 가능하다.

설계를 유연하게 만들 수 있었던 이유는 아래와 같다.

  1. Movie가 DiscountPolicy라는 추상화에 의존하고
  1. 생성자를 통해 DiscountPolicy에 대한 의존성을 명시적으로 드러냈으며
  1. 구체 클래스를 직접적으로 생성하는 책임을 Movie의 외부(클라이언트)로 할당

우리는 Movie가 의존하는 추상화인 DiscountPolicy 클래스에 자식 클래스를 추가함으로써 간단하게 Movie가 사용될 컨텍스트를 확장할 수 있었다. 결합도를 낮춤으로써 얻게 되는 컨텍스트의 확장이라는 개념이 유연하고 재사용 가능할 설계를 만드는 핵심이다.


조합 가능한 행동

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

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

유연하고 재사용 가능한 설계는 응집도 높은 책임들을 가진 작은 객체들을 다양한 방식으로 연결함으로써 애플리케이션의 기능을 쉽게 확장할 수 있다.

유연하고 재사용 가능할 설계는 객체가 어떻게(how) 하는지를 장황하게 나열하지 않고도 객체들의 조합을 통해 무엇(what)을 하는지를 표현하는 클래스들로 구성된다. 따라서 클래스의 인스턴스를 생성하는 코드를 보는 것만으로 객체가 어떤 일을 하는지를 쉽게 파악할 수 있다. 코드에 드러난 로직을 해석할 필요 없이 객체가 어떤 객체와 연결됐는지를 보는 것만으로도 객체의 행동을 쉽게 예상하고 이해할 수 있기 때문이다. 다시말해 선언적으로 객체의 행동을 정의할 수 있는 것이다.

유연하고 재사용 가능한 설계는 작은 객체들의 행동을 조합함으로써 새로운 행동을 이끌어낼 수 있는 설계다. 훌륭한 객체지향 설계란 어떻게 하는지를 표현하는 것이 아니라 객체들의 조합을 선언적으로 표현함으로써 객체들이 무엇을 하는지를 표현하는 설계다. 그리고 지금까지 설명한 것 처럼 이런 설계를 창조하는 데 있어서의 핵심은 의존성을 관리하는 것이다.

참고 및 출처

  • 오브젝트