Ch5 책임 할당하기

본문


들어가며

이전 4장에서는 캡슐화, 응집도, 결합도를 설계 품질의 기준으로 삼고 데이터 중심 설계가 어떻게 캡슐화를 위반하면서 변경하기 어려운 설계가 되는지에 대해 살펴보았다.

또한 데이터 중심 설계가 갖는 폐해를 책임 주도 설계가 어떻게 해결하는지를 캡슐화를 기준으로 학습하였다.

이에 따라 캡슐화변경되는 개념에 대한 은닉, 변경 가능한 모든 것에 대한 은닉임을 깨닫고, 객체지향 설계의 기본 원리임을 확인할 수 있었다.

나아가 캡슐화, 결합도, 응집도변경의 관점에서 바라보는것이 중요함을 학습했다.

이번 장에서는 객체지향에서 가장 핵심인 책임을 어떻게 할당하는지를 살펴보려고 한다.

책임에 초점을 맞춰서 설계할 때 가장 큰 어려움은 어떤 객체에게 어떤 책임을 할당할지를 결정하기가 쉽지 않다는 것이다.

책임 할당의 과정은 일종의 트레이드오프 활동이다. 동일한 문제를 해결할 수 있는 다양한 책임 할당 방법이 존재하며, 어떤 방법이 최선인지는 상황과 문맥에 따라 달라진다. 따라서 올바른 책임을 할당하기 위해서는 다양한 관점에서 설계를 평가할 수 있어야 한다.

이번 장에서 학습하게 될 GRASP 패턴은 책임 할당의 어려움을 해결하기 위한 답을 제시해 줄 것이다.

GRASP 패턴을 이해하고 나면 캡슐화, 응집도, 결합도와 같은 다양한 기준에 따라 책임을 할당하고 결과를 트레이드오프할 수 있는 기준을 배우게 될 것이다.


핵심.

  • 캡슐화외부로부터 감추는 것들(데이터, 내부 구현)뿐만 아니라 내부에서도 변경되는 개념이라면 캡슐화 대상이다.

  • 객체에게 중요한 것은 데이터가 아니라 외부에 제공하는 행동이다.

  • 클라이언트의 관점에서 객체가 수행하는 행동이란 곧 객체의 책임을 의미한다.

  • 책임 중심 설계에서는 객체의 행동(책임)을 먼저 결정한 후에 객체의 상태를 결정한다.

  • 객체에게 할당된 책임이 협력에 어울리면 그 책임은 좋은 것이다.

  • 책임은 객체의 입장이 아니라 객체가 참여하는 협력에 적합해야한다.

  • 협력에 적합한 책임이란 메시지 수신자가 아니라 메시지 전송자에게 적합한 책임을 의미한다.

  • 메시지를 전송하는 클라이언트의 의도에 적합한 책임(클라이언트의 의도를 드러내는 책임)이 적절한 책임이다.

  • 협력에 적합한 책임(클라이언트의 의도에 적합한 책임)을 위해서는 메시지를 결정한 후에 객체를 선택해야 한다.

  • 객체가 메시지를 선택하는 것이 아니라 메시지가 객체를 선택하게 해야 한다.

  • 메시지는 클라이언트의 의도를 표현한다.

  • 메시지를 먼저 결정하기 때문에 메시지 송신자의 입장에서 메시지 수신자가 깔끔히 캡슐화된다.

  • 결국 책임 주도 설계의 헥심은 책임을 결정한 후에 책임을 수행할 객체를 결정한다는 것이다.

  • 도메인 개념들을 책임 할당의 대상으로 사용하면 코드에 도메인의 모습을 투영하기가 좀 더 수월해진다.

  • 책임을 할당할 때 가장 먼저 고민해야 하는 가장 유력한 후보는 도메인 개념이다.

  • 도메인 모델에 정답은 존재하지 않는다. 필요한 것은 도메인을 그대로 투영한 모델이 아니라 구현에 도움이 도는 모델이다.

  • 객체에게 책임을 할당하는 첫 번째 원칙은 책임을 수행 할 정보를 가장 잘 알고있는 정보전문가에게 책임을 할당하는 것이다.

  • 정보전문가 패턴은 객체가 상태행동을 함께 갖는 단위라는 객체지향의 가장 기본적인 원리를 책임 할당의 관점에서 표현한다.

  • 정보전문가 패턴을 따르는 것만으로도 자율성이 높은 객체들로 구성된 협력 공동체를 구축할 확률이 높아진다.

  • 정보전문가정보를 갖고있다는 것이 데이터를 저장하고 있음을 의미하지는 않는다. 정보를 제공할 수 있는 객체를 아는 것계산을 통해 정보를 제공하는 것 역시 정보전문가의 자격을 만족한다.

  • 정보전문가 패턴은 객체란 행동과 관련된 정보를 함께 가지는 단위라는 객체지향의 가장 기본적인 원리를 책임 할당의 관점에서 표현한 것이다.

  • 설계는 트레이드오프 활동임을 명심하라 동일한 기능을 구현할 수 있는 무수히 많은 설계가 존재한다.

  • 책임을 할당할 수 있는 여러가지 대안이 존재할 수 있다. 이 때에는 결합도응집도 측면에서 더 나은 선택을 하는것이 좋다.

  • Low Coupling(낮은 결합도) 패턴과 High Cohesion(높은 응집도) 패턴은 설계를 진행하면서 협력과 책임의 품질을 검토하는 데 사용할 수 있는 중요한 평가 기준이다.

  • 창조자에게 객체 생성 책임을 할당하는 것이 Creator 패턴이다.

  • 이미 결합되어있는 객체에게 생성 책임을 할당하는 것은 설계의 전체적인 결합도에 영향을 미치지 않는다.

  • 도메인 개념들을 후보로 책임을 분배하는 것은 설계를 시작하기 위한 대략적인 스케치에 불과하다. 실제 설계는 코드를 작성하는 동안 이루어진다.

  • 올바른 설계를 하고있는지 궁금하다면 코드를 작성하라.

  • 변경에 취약한 클래스란 변경의 이유를 하나 이상 갖는 클래스이다.

  • 하나 이상의 변경 이유를 갖는 클래스는 응집도가 낮음을 의미한다. 서로 관련성이 없는 기능이나 데이터가 한 클래스 안에 공존한다는 의미이다.

  • 변경의 시점이 서로 다른 코드가 한 클래스 내에 공존한다면 이 역시 응집도가 낮음을 의미한다.

  • 설계를 개선하는 작업은 변경의 이유가 하나 이상인 클래스를 찾는 것으로부터 시작하는 것이 좋다.

  • 변경의 이유가 하나 이상인 클래스, 즉 응집도가 낮은 클래스는 위험 징후를 나타내는 몇 가지 패턴이 존재한다.

  • 클래스의 인스턴스를 초기화하는 시점이 서로 다른 속성들이 존재한다면 응집도가 낮은 것이다. 초기화되는 속성 그룹을 기준으로 클래스를 분리하라.

  • 메서드 그룹이 속성 그룹을 사용하는지 여부로 나뉜다면 응집도가 낮은 것이다. 이들 그룹을 기준으로 클래스를 분리하라

  • 중요한 것은 변경의 이유에 따라 여러 개의 클래스로 분리해야 한다는 것이다.

  • 역할을 사용하면 객체의 구체적인 타입을 추상화할 수 있다.

  • 클라이언트는 협력하는 객체의 구체적인 타입을 몰라도 상관 없다. 단지 협력하는 객체가 책임을 수행할 수 있다(역할)는 사실만 알고 있어도 충분하다.

  • 객체의 타입에 따라 변하는 행동이 있다면 타입을 분리하고 변화하는 행동을 각 타입의 책임으로 할당하는 것이 다형성 패턴이다.

  • 변경을 캡슐화 하도록 책임을 할당 하는 것이 변경 보호 패턴이다.

  • 변화가 예상되는 불안정한 지점을 식별하고 그 주위에 안정적인 인터페이스를 형성하도록 책임을 할당하라.

  • 설계에서 변하는 것이 무엇인지 고려하고 변하는 개념을 캡슐화 하라.

  • 예측 가능한 변경으로 인해 여러 클래스들이 불안정해진다면 변경 보호 패턴에 타라 안정적인 인터페이스 뒤로 변경을 캡슐화 하라.

  • 다형성 관점에서 안정적인 인터페이스역할(추상 타입인 추상클래스, 인터페이스)를 의미하며, 변경이란 구체적인 클래스(타입)을 의미한다. 즉 역할뒤로 구체적인 타입을 캡슐화 함으로써 외부에 구체적인 타입이 드러나지 않도록 숨기는 것을 의미한다.

  • 도메인 구조코드의 구조를 이끈다.

  • 변경 역시 도메인 모델의 일부이다.

  • 도메인 모델에는 도메인에서 변하는 개념과 이들 사이의 관계가 투영되어 있어야 한다.

  • 구현을 가이드 할 수 있는 도메인 모델을 선택하라. 객체지향은 도메인 개념과 구조를 반영한 코드를 가능하게 만들기 때문에 도메인의 구조가 코드의 구조를 이끌어 내는 것이 바람직하다.

  • 코드의 구조가 도메인 구조에 대한 새로운 통찰력을 제공한다.

  • 도메인 모델은 구현과 밀접한 관계를 맺어야 한다. 도메인 모델은 코드에 대한 가이드를 제공할 수 있어야 하며 코드의 변화에 발맞춰 함께 변화해야 한다.

  • 책임객체를 선택하는 일에 어려움을 느낄 때에는 아무것도 없는 상태에서 책임과 협력에 관해 고민하기 보다는 일단 실행되는 코드를 얻고난 후에 코드 상에 명확하게 드러나는 책임들을 올바른 위치로 이동시키는 방법도 좋다.

  • 메서드 응집도가 높게끔 메서드를 잘게 분리하자.

  • 주석을 추가하는 대신 메서드를 잘게 분해해서 각 메서드의 응집도를 높여라.

  • 메서드 응집도를 높이는 이유도 변경과 관련이 깊다. 응집도 높은 메서드는 변경의 이유가 단 하나여야 한다.

  • 절차지향적 코드에서 객체로 책임을 분배할 때 가장 먼저 할 일은 메서드를 응집도 있는 수준으로 분해하는 것이다.

  • 코드를 작은 메서드들로 분해하면 전체적인 흐름을 이해하기도 쉬워진다.

  • 큰 메서드를 작은 메서드들로 나누면 한 번에 기억해야 하는 정보를 줄일 수 있다. 더 세부적인 정보가 필요하다면 그 때 각 메서드의 세부적인 구현을 확인하면 된다.

  • 메서드들이 하나의 변경 이유를 가지도록 개선될 때 결과적으로 응집도 높은 클래스가 만들어진다.

  • 응집도를 높이기 위해서는 변경의 이유가 다른 잘게 분해된 메서드들을 적절한 위치로 분배하면 된다.

  • 여기서 말하는 적절한 위치자신을 책임지는 자율적인 객체로 만들기 위한 위치이다. 즉, 자신이 소유하고 있는 데이터를 자기 스스로 처리하게끔 만드는 것이다.

  • 책임 주도 설계 방법에 익숙하지 않다면 일단 데이터 중심으로 구현한 뒤 응집도 높은 메서드를 만든 뒤, 적절한 객체에게 분배하는 리팩터링 작업으로도 유사한 결과를 얻을 수 있다.

  • 캡슐화, 응집도, 결합도를 이해하고 훌륭한 객체지향 원칙을 적용하기 위해 노력한다면 책임 주도 설계 방법을 단계적으로 따르지 않더라도 유연하고 깔끔한 코드를 얻을 수 있을 것이다.


책임 주도 설계를 향해

책임 중심 설계로 전환하기 위해서는 다음의 두 원칙을 따라야 한다.

  • 데이터보다 행동을 먼저 결정하라.
  • 협력이라는 문맥 안에서 책임을 결정하라.

두 원칙의 핵심은 설계를 진행하는 동안 데이터가 아니라 객체의 책임과 협력에 초점을 맞추는 것이다.

데이터보다 행동을 먼저 결정하라.

객체에게 중요한 것은 데이터가 아니라 외부에 제공하는 행동이다. 클라이언트의 관점에서 객체가 수행하는 행동이란 곧 객체의 책임을 의미한다. 데이터에 초점을 맞추면 객체의 캡슐화가 약화되기 때문에 낮은 응집도와 높은 결합도를 가진 객체들로 넘쳐나게 된다. 그 결과로 변경에 취약한 설계가 탄생하는 것이다.

다시말해 책임 중심의 설계에서는 객체의 행동, 즉 책임을 먼저 결정한 후에 객체의 상태를 결정하는 것이 중요하다. 그렇다면 어떤 객체에게 어떤 책임을 할당해야 하는가? 해결의 실마리를 협력에서 찾을 수 있다.

협력이라는 문맥 안에서 책임을 결정하라.

객체의 입장에서는 책임이 조금 어색해 보이더라도 협력에 적합하다면 그 책임은 좋은 책임이다. 즉, 책임은 객체의 입장이 아니라 객체가 참여하는 협력에 적합해야 한다.

협력을 시작하는 주체는 메시지 전송자이기 때문에 협력에 적합한 책임이란 메시지 수신자(객체)가 아니라 메시지 전송자(클라이언트)에 적합한 책임을 의미한다. 다시말해서 메시지를 전송하는 클라이언트의 의도에 적합한 책임을 할당해야 한다.

클라이언트의 의도를 반영한 책임을 얻기 위해서는 객체를 결정한 후에 메시지를 결정하는 것이 아니라. 메시지를 결정한 후에 객체를 선택해야 한다. 메시지가 존재하기 때문에 그 메시지를 처리할 객체가 필요한 것이다. 객체가 메시지를 선택하는 것이 아니라 메시지가 객체를 선택하게끔 해야 한다.

여기서 메시지가 객체의 의도가 아니라 클라이언트의 의도를 표현한다는 사실을 명심하라. 클라이언트는 단지 임의의 객체가 메시지를 수신할 것이라는 사실을 믿고 자신의 의도를 표현한 메시지를 전송할 뿐이다. 그리고 메시지를 수신하기로 결정된 객체는 메시지를 처리할 책임을 할당 받게 된다.

메시지를 먼저 결정하기 때문에 메시지 전송자는 메시지 수신자에 대한 어떤 가정도 할 수 없다. 메시지 전송자(클라이언트)의 관점에서 메시지 수신자(객체)가 깔끔하게 캡슐화 되는 것이다. 이로써 무엇을 드러내는 충분히 추상적인 인터페이스를 얻을 수 있다.

정리하자면 객체에게 절절한 책임을 할당하기 위해서는 협력이라는 문맥을 고려해야 한다. 협력이라는 문맥에 적절한 책임이란 곧 클라이언트의 관점에서 적절한 책임을 의미한다. 올바른 객체지향 설곈는 클라이언트가 전송할 메시지를 결정한 후에야 비로소 객체의 상태를 저장하는 데 필요한 내부 데이터에 관해 고민하기 시작한다.

이 관점은 책임 주도 설계의 흐름과 거의 동일하다. 책임 주도 설계의 흐름은 아래와 같다.

  1. 시스템이 사용자에게 제공해야 하는 기능인 시스템 책임을 파악한다.
  2. 시스템 책임더 작은 책임으로 분할한다.
  3. 분할된 책임을 수행할 수 있는 적절한 객체 또는 역할을 찾아 책임을 할당한다.
  4. 객체가 책임을 수행하던 도중 다른 객체의 도움이 필요할 경우 이를 책임질 적절한 객체 또는 역할을 찾는다.
  5. 해당 객체 또는 역할에게 책임을 할당함으로써 두 객체가 협력하게 한다.

책임 주도 설계의 핵심은 책임을 정한 후에 책임을 수행 할 객체를 결정하는 것이다. 이제 책임 중심 설계GRASP 패턴에 따라 설계하는 과정을 그려보자.

책임 할당을 위한 GRASP 패턴

설계 과정은 도메인안에 존재하는 개념들을 정리하는 것으로 시작된다.

도메인 개념에서 출발하기.

설계를 시작하기 전에 도메인에 대한 개략적인 모습을 그려보는 것이 유용한다. 도메인 개념들을 책임 할당의 대상으로 사용하면 코드에 도메인의 모습을 투영하기가 좀 더 수월해진다. 따라서 어떤 책임을 할당해야 할 때 가장 먼저 고민해야 하는 유력한 후보는 바로 도메인 개념이다.

설계를 시작하는 단계에서는 개념들의 의미와 관계가 정확하거나 완벽할 필요가 없다. 단지 우리에게는 출발점이 필요할 뿐이다. 이 단계에서는 책임을 할당받을 객체들의 종류와 관계에 대한 유용한 정보를 제공할 수 있다면 충분하다.

첨언하자면, 올바른 도메인 모델이란 존재하지 않는다. 사람마다 생각하는 도메인 모델의 구조는 조금씩 차이가 있을수 있다. 어떤 도메인 모델이든 올바른 구현을 이끌어 낼 수 있다면 모두 정답이다.

도메인 모델은 도메인을 개념적으로 표현한 것이지만 그 안에 포함된 개념과 관계는 구현의 기반이 되어야 한다. 이것은 도메인 모델구현을 염두에 두고 구조화 되는 것이 바람직하다는 것을 의미한다. 반대로 코드의 구조가 도메인을 바라보는 관점을 바꾸기도 한다. 즉, 코드의 구조도메인 구조를 변경할 수도 있다.

정리하자면 코드의 구조도메인 모델의 구조를 기반으로 반영된다. 그리고 유연성재사용성을 고려한 코드의 구조도메인 모델의 구조를 변경하기도 한다.

이는 도메인 모델구현에 도움이 되는 모델임을 의미하며, 실용적이면서 유용한 모델을 추구해야 함을 의미한다.

정보 전문가에게 책임을 할당하라.

책임 주도 설계의 첫 단계는 애플리케이션이 제공해야 하는 기능애플리케이션의 책임으로 생각하는 것이다. 이 책임을 애플리케이션에 대해 전송된 메시지로 간주하고 이 메시지를 책임질 첫 번째 객체를 선택하는 것으로 설계를 시작한다.

메시지는 메시지를 수신할 객체(수신자)가 아니라 메시지를 전송할 객체(클라이언트)의 의도를 반영해서 결정해야 한다. 이를 위해서는 아래와 같이 생각하는 것이 유용하다.

  1. 메시지를 전송할 객체(클라이언트)는 무엇을 원하는가
  2. 메시지를 수신할 객체는 누구인가?

이 질문들에 답하기 위해서는 객체가 행동과 행동에 필요한 상태를 함께 갖는 캡슐화의 단위라는 사실에 집중해야 한다. 객체는 자신의 상태를 스스로 처리하는 자율적인 존재여야 한다. 객체의 책임과 책임을 수행하는 데 필요한 상태는 동일한 객체 안에 존재해야 한다. 따라서 객체에게 책임을 할당하는 첫 번째 원칙은 책임을 수행할 정보를 알고 있는 객체에게 책임을 할당하는 것이다.

정보전문가 패턴객체가 자신이 소유하고 있는 정보와 관련된 책임을 수행한다는 일반적인 직관을 표현한 것이다. 여기서 주의해야 할 점이 있다. 정보데이터(상태)와 다르다는 것을 유의해야 한다. 책임을 수행하는 객체가 정보를 ‘알고’있다고 해서 그 정보를 ‘저장’하고 있을 필요는 없다.

객체는 해당 정보를 제공할 수 있는 다른 객체를 알고 있거나 정보를 계산해서 제공할 수도 있다. 어떤 방식이건 정보 전문가가 데이터를 반드시 저장하고 있을 필요는 없다는 사실을 이해하는 것이 중요하다.

정리하자면 정보 전문가 패턴은 객체가 행동과 행동에 필요한 정보를 함께 갖는 단위라는 객체지향의 가장 기본적인 원리를 책임 할당의 관점에서 표현한 것이다. 이 기본적인 패턴을 따르는 것만으로도 자율성이 높은 객체들로 구성된 협력 공동체를 구축할 가능성이 높아진다.

높은 응집도와 낮은 결합도.

설계는 트레이드오프 활동임을 기억하자. 동일한 기능을 구현할 수 있는 수 많은 설계가 존재한다. 따라서 실제로 설계를 진행하다 보면 몇 가지 설계 중 한 가지를 선택해야 하는 경우가 발생한다. 이 경우에는 올바른 책임 할당을 위해 정보 전문가 패턴 이외의 다양한 책임 할당 패턴을 고려할 필요가 있다.

이 때 유용한 기준이 결합도응집도 이다. 높은 응집도낮은 결합도는 객체에 책임을 할당할 때 항상 고려해야 하는 기본 원리다. 책임을 할당할 수 있는 다양한 대안이 존재한다면 응집도와 결합도의 측면에서 더 나은 대안을 선택하는것이 좋다.

DiscountCondition(할인 조건)Movie와 협력하는 것이 좋을까, 아니면 Screening과 협력하는 것이 좋을까? 해답의 실마리는 결합도응집도를 고려해봄으로써 알 수 있다.

먼저 결합도 측면에서 살펴보자. 도메인 개념을 확인했다면 도메인 상으로 MovieDiscountCondition의 목록을 상태로 가지고있다. 다시말해 MovieDiscountCondition은 이미 결합되어 있기 때문에 MovieDiscountCondition을 협력하게 한다면 설계 전체적으로 결합도를 추가하지 않고 협력을 완성할 수 있다.

반면에 ScreeningDiscountCondition이 협력할 경우 둘 사이에 새로운 결합도가 추가된다. 따라서 Low Coupling 관점에서 MovieDiscountCondition과 협력하는 것이 더 나은 설계 대안인 것이다.

다음으로 응집도 측면에서도 살펴보자. Screening의 가장 중요한 책임예매하는 것이다. 만약 반면 할인 조건에 대해 아는 것은 요금 계산과 관련된 책임이다. 만일 ScreeningDiscountCondition이 협력을 해야 한다면 Screening요금 계산과 관련된 책임을 일부 떠안아야 할 것이다. 이 경우 ScreeningDiscountCondition이 할인 여부를 판단 가능함을 알아야 함과 동시에 Movie가 할인 여부를 필요로 한다는 사실도 알고 있어야 한다.

다시 말해서 요금 계산 방식의 변경이 일어날 경우 Screening도 함께 변경되어야 한다는 것이다. 결과적으로 ScreeningDiscountCondition이 협력하게 될 경우 Screening서로 다른 이유로 변경되는 책임을 짊어지게 되므로 낮은 응집도를 갖게 될 수 밖에 없다.

반면 Movie의 주된 책임은 요금 계산이다. 따라서 요금 계산에 필요한 할인 조건을 판단하기 위해 MovieDiscountCondition과 협력하는 것은 응집도에 아무런 해도 끼치지 않는다.

이와 같이 Low Coupling(낮은 결합도)High Cohesion(높은 응집도)는 설계를 진행하면서 책임협력의 품질을 검토하는데 사용할 수 있는 중요한 평가 기준이다.

구현을 통한 검증

현재까지의 책임 할당 과정은 설계를 시작하기 위한 대략적인 스케치에 불과하다. 실제 설계는 코드를 작성하는 동안 이뤄진다. 협력과 책임이 제대로 동작하는지 확인할 수 있는 유일한 방법은 코드를 작성하고 실행해 보는 것이다. 올바르게 설계하고 있는지 궁금하다면 코드를 작성하라.

1. 먼저 Screening의 구현 코드를 살펴보자.

  1. Screening예매하라 라는 메시지에 응답할 수 있어야 하기 때문에 메시지를 처리하는 메서드 reserve를 구현한다.
  2. Screening예매하라 라는 책임을 수행하기 위해 필요한 인스턴스 변수를 결정한다.
  3. Screening예매하라 라는 책임을 수행하는 중 할 수 없는 작업(요금 계산)에 대해서는 요금 계산의 정보 전문가인 movie에게 위임한다. 단, 여기서 요금을 계산하라(calculateMovieFee) 라는 메시지는 메시지 수신자인 movie가 아닌 메시지 전송자인 Screening의 의도를 표현함을 주목하자. 또한 ScreeningcalculateMovieFee라는 전송할 메시지를 결정할 때에는 Movie가 단순히 영화 요금을 계산할 수 있다.라는 책임에 대해서만 알 뿐 구체적인 구현에 대해서는 알지 못한다. 다시말해 Screening(클라이언트) 입장에서 Movie의 내부 구현이 깔끔하게 캡슐화 된 것이다.

이제 ScreeningMovie를 연결하는 유일한 연결고리는 메시지 뿐이다. 따라서 메시지가 변경되지 않는 한(책임, 인터페이스가 변경되지 않는 한) Movie 내부에 어떤 수정을 가하더라도 Screening에는 영향을 미치지 않는다. 메시지를 기반으로 협력을 구성하면 Screening(메시지 전송자, 클라이언트)와 Movie(메시지 수신자) 사이에 결합도를 느슨하게 유지할 수 있다.

이처럼 메시지가 객체를 선택하도록 책임 주도 설계 방식을 따르면 캡슐화와 낮은 결합도라는 목표를 비교적 손쉽게 달성할 수 있다.

이제 MovieScreening이 전송한 calculateMovieFee라는 메시지에 응답하기 위해 메서드를 구현해야 한다.

2. calculateMovieFee 메시지를 전송받은 Movie를 구현해보자.

먼저 알아둬야 할 것은 아래 구현 코드는 책임 주도 설계 순서에 맞춰 구현했지만 아직 결합도응집도를 고려하지 않은 코드이다. 이는 이후 메서드 응집도리팩터링 과정에서 개선해보도록 하자.

  1. Movie영화 요금을 계산하라 라는 메시지에 응답할 수 있어야 하기 때문에 메시지를 처리하는 메서드 calculateMovieFee를 구현한다.
  2. Movie영화 요금을 계산하라 책임을 수행하는 중 할 수 없는 작업(할인 여부 판단)에 대해서는 할인 여부 판단에 대한 정보 전문가인 DiscountCondition에게 위임한다.
  3. Movie영화 요금을 계산하라 책임을 수행하기 위해 MovieType에 따라 다른 메서드(calculateAmountDiscount, calculatePercentDiscount)를 호출한다.

다음으로 DiscountConditionMovie가 전송한 isDiscountable에 대한 메시지에 응답하기 위해 메서드를 구현해야 한다.

3. isDiscountable 메시지를 전송받은 DiscountCondition을 구현해보자.

자 이제 영화 예매를 위해 Screening ~ Movie ~ DiscountCondition 사이에 협력이 완성되었다. 각 클라이언트 객체들은 메시지를 먼저 선택하고 수신받을 객체 결정을 함으로써 책임 주도 설계의 기본 절차를 충분히 따랐다. 그 결과로 충분히 추상적인 인터페이스를 얻고 내부 구현을 캡슐화한 객체를 얻은 것 같다.

하지만 아직 부족하다. 무엇이 부족한 것일까?

개선하기

개인적인 캡슐화의 이해 start

외부에서 바라보는(Movie가 바라보는) DiscountCondition의 내부 구현은 충분히 캡슐화되었다. 하지만 DiscountCondition의 내부를 살펴보면 한 가지 이상의 변경 이유를 갖는다. 이는 낮은 응집도를 갖게 될 때 나타나는 문제점인데, 어떤 원인으로 DiscountCondition낮은 응집도를 갖게되었는지 고민해보았다.

결론은 명확하고 단순하다. 바로 캡슐화가 안되었기 때문이다. 아니, 분명히 앞에서 DiscontCondition의 내부 구현이 충분히 캡슐화 되었다고 했는데 뜬금없이 캡슐화가 되지 않았다니?

이는 누구의 입장에서, 어떤 부분에서 캡슐화가 되었는가 를 살펴보아야 한다. 분명히 외부 클라이언트의 입장에서는 DiscountCondition의 세부 구현을 알지 못한다. 인터페이스를 변경하는것이 아니라면 DiscountCondition의 내부 구현이 변경된다 한들 외부에는 전혀 영향을 끼치지 않는다는 점에서 충분한 캡슐화라고 할 수 있다.

하지만 DiscountCondition내부의 변경의 이유를 살펴보면 한 가지 이상의 변경의 원인을 갖고, 이는 낮은 응집도에서 기인하였으며, 낮은 응집도를 갖게 된 원인은 할인 조건 타입캡슐화되지 않았기 때문이다.

변경되는 개념(할인 조건 타입)캡슐화되지 않았기 때문에 순번 할인 조건, 기간 할인 조건이 변경될 때 마다 DiscountCondition이 변경된다.

여기서 말하고 싶은 것은 캡슐화변경되는 개념을 감추는 것을 의미한다는 점이다.

단순히 데이터, 내부 구현을 외부로부터 숨기는 것 만이 캡슐화가 아니라, 내부에서도 변경되는 개념이 존재한다면 이는 캡슐화의 대상이다.

자 다시한번 상기하자 캡슐화는 문맥을 고려하여, 대상에 상관 없이 “변경되는 개념”이라면 은닉의 대상이다.

end

낮은 응집도 개선

가장 큰 문제점은 변경에 취약한 클래스를 포함하고 있다는 것이다. 변경에 취약한 클래스란 코드를 수정해야 하는 이유를 하나 이상 가지는 클래스다. 현재의 코드에서 변경의 이유가 다양한 클래스는 먼저 DiscountCondition이다.

DiscountCondition은 다음과 같이 서로 다른 세 가지 이유로 변경될 수 있다.

  1. 새로운 할인 조건 추가 시 DiscountConditionType의 속성이 추가되고, 새로운 할인 조건에 필요한 속성에 해당하는 변수를 추가하는 작업도 필요하다. 또한 isSatisfiedBy 메서드 안의 if ~ else 구문을 수정해야 한다.

  2. 순번 조건 판단 로직 변경isSatisfiedSequence 메서드의 내부 구현을 수정해야 한다. 물론 순번 조건 판단에 필요한 sequence 속성 역시 변경되어야 할 것이다.

  3. 기간 조건 판단 로직 변경isSatisfiedByPeriod 메서드의 내부 구현을 수정해야 한다. 물론 기간 조건 판단에 필요한 속성들 역시 변경되어야 할 것이다.

위와 같이 DiscountCondition하나 이상의 변경 이유를 가지기 때문에 응집도가 낮다. 응집도가 낮다는 것은 서로 연관성이 없는 기능이나 데이터가 하나의 클래스 안에 뭉쳐져 있다는 것을 의미한다.

위와 같이 낮은 응집도가 초래하는 문제를 해결하기 위해서는 변경의 이유에 따라 클래스를 분리해야 한다.

DiscoutnCondition 안에 구현된 isSatisfiedBySequence 메서드와 isSatisfiedByPeriod 메서드는 서로 다른 이유로 변경된다. 두 가지 변경이 코드에 영향을 미치는 시점은 다를 수 있다. 다시말해 DiscountCondition서로 다른이유로, 서로 다른 시점에 변경될 가능성이 높다. 즉, 서로 다른이유로 변경되는 두 개의 메서드를 지닌 DiscountCondition 클래스의 응집도는 낮아질 수 밖에 없는 것이다.

설계를 개선하는 작업은 변경의 이유가 하나 이상인 클래스를 찾는 것으로부터 시작하는 것이 좋다. 이렇게 변경의 이유가 하나 이상인 클래스는 위험 징후를 또렷하게 나타내는 몇 가지 패턴이 존재한다.

인스턴스 변수가 초기화 되는 시점을 살펴봄으로써 변경의 이유를 파악할 수 있다. 응집도가 높은 클래스는 인스턴스를 생성할 때 모든 속성을 함께 초기화 한다. 반면 응집도가 낮은 클래스는 객체의 속성 중 일부만 초기화 하고 일부는 초기화되지 않은 상태로 남겨진다.

따라서 응집도를 높이기 위해서는 함께 초기화되는 속성을 기준으로 코드를 분리해야 한다.

메서드들이 인스턴스 변수를 사용하는 방식을 살펴봄으로써 변경의 이유를 파악할 수 있다. 모든 메서드가 모든 속성을 사용한다면 클래스의 응집도는 높다고 볼 수 있다. 반면에 메서드들이 사용하는 속성에 따라 그룹이 나뉜다면 클래스의 응집도가 낮다고 볼 수 있다.

이 경우 클래스의 응집도를 높이기 위해서는 속성 그룹과 해당 그룹에 접근하는 메서드 그룹을 기준으로 코드를 분리해야 한다.

정리해보자.

  1. 클래스가 하나 이상의 이유로 변경된다면 응집도가 낮은 것이다. 변경의 이유를 기준으로 클래스를 분리하라.
  2. 클래스의 인스턴스 초기화시점에 경우에 따라 서로 다른 속성들을 초기화하고 있다면 응집도가 낮은 것이다. 초기화되는 속성의 그룹을 기준으로 클래스를 분리하라.
  3. 속성 그룹을 사용하는지 여부로 메서드 그룹이 나뉜다면 응집도가 낮은 것이다. 이들 그룹을 기준으로 클래스를 분리하라.

경우에 따라서 메서드 크기가 너무 커서 긴 코드 라인 속에 숨겨진 문제가 명확하게 보이지 않을 수 도 있다. 긴 메서드를 응집도 높은 작은 메서드로 잘게 분해해 나가면서 문제점을 찾아보자.

타입 분리 개선

DiscountCondition의 가장 큰 문제는 순번 조건기간 조건이라는 두 개의 독립적인 타입이 하나의 클래스 안에 공존하고 있다는 점이다. 이 독립적인 타입을 SequenceConditionPeriodCondition이라는 두 개의 클래스로 분리하자.

클래스를 분리하면 앞에서 언급했던 하나 이상의 변경의 이유를 갖는 낮은 응집도 문제가 해결된다. 분리된 각 클래스의 모든 메서드들은 동일한 인스턴스 변수 그룹을 사용한다. 클래스를 분리함으로써 코드의 품질을 높이는데 성공한 것이다.

다형성을 통해 분리하기

이제 Movie 입장에서 보면 SequenceConditionPeriodCondition은 아무 차이가 없다. 단지 할인 여부 판단이라는 동일한 책임을 수행하고 있을 뿐이다. 두 클래스가 할인 여부를 판단하기 위해 사용하는 구체적인 방법이 다르다는 사실은 Movie 입장에서는 그다지 중요하지 않다. 할인 여부만 판단 할 수 있다면 Movie는 구체적인 타입에 대해 신경쓰지 않는다.

이 시점에 역할이 자연스럽게 무대 위로 등장한다. Movie의 입장에서 SequenceConditionPeriodCondition동일한 책임을 수행하는, 동일한 역할을 가진 존재다.

역할을 사용하면 객체의 구체적인 타입을 추상화 할 수 있다. 자바에서는 일반적으로 역할을 구현하기 위해 추상 클래스 또는 인터페이스를 사용한다. 역할을 대체할 클래스들 사이에서 구현을 공유해야할 필요가 있다면 추상클래스를, 구현을 공유할 필요 없이 역할을 대체하는 객체들의 책임만 정의하고 싶다면 인터페이스를 사용하면 된다.

할인 조건의 경우에는 SequenceConditionPeriodCondition이 구현을 공유할 필요는 없어보인다. 따라서 DiscountCondition이라는 이름을 가진 인터페이스를 이용해 역할을 구현하자.

이제 Movie는 협력하는 객체의 구체적인 타입을 몰라도 상관 없다. 협력하는 객체가 isSatisfiedBy 메시지를 수신할 수 있는 DiscountCondition 역할을 수행하기만 한다면 충분하다.

DiscountCondition의 경우에서 알 수 있듯이 객체의 암시적인 타입에 따라 행동을 분기해야 한다면 명시적인 클래스로 정의하고 행동을 나눔으로써 응집도 문제를 해결할 수 있다. 다시말해 객체의 타입에 따라 변하는 행동이 있다면 타입을 분리하고 각 행동을 각 타입의 책임으로 할당하라는 것이다. 이를 다형성 패턴이라고 부른다.

객체의 암시적인 타입에 따라 변하는 로직이 있을 때에는 암시적인 타입을 명시적으로 정의하고 각 타입에 다형적으로 행동하는 책임을 할당하라.

if ~ else, switch 등의 조건 논리를 사용하여 설계한다면 새로운 변화가 일어난 경우 조건 논리를 수정해야 한다. 이것은 프로그램을 수정하기 어렵고 변경에 취약하게 만든다.

다형성 패턴은 조건논리로 객체의 타입을 검사하지 말고 다형성을 이용하여 새로운 변화를 다루기 쉽게 확장하도록 권고한다.

변경으로부터 보호하기

위 그림을 보면 DiscountCondition의 두 서브클래스들은 서로 다른 이유로 변경된다는 사실을 알 수 있다. SequenceCondition은 순번 조건의 구현 방법이 변경될 경우에만 수정된다. PeriodCondition은 기간 조건의 구현 방법이 변경될 경우에만 수정된다. 두 개의 서로 다른 변경이 두 개의 서로 다른 클래스 안으로 캡슐화 된다.

새로운 할인 조건이 추가 되어도 안전하다. DiscountCondition이라는 역할Movie로부터 구체적인 타입을 감춘다는 사실에 주목하자. Movie의 관점에서 DiscountCondition의 타입이 캡슐화 된다는 것은 새로운 DiscountCondition이 추가 되어도 Movie가 영향을 받지 않는다는 것을 의미한다.

이처럼 변경을 캡슐화 하도록 책임을 할당하는 것을 변경 보호 패턴이라고 부른다. DiscountCondition은 구체적인 할인 조건 타입을 캡슐화 하도록 책임을 할당받았다.

변화가 예상되는 불안정한 지점들을 식별하고 그 주위에 안정적인 인터페이스를 형성하도록 책임을 할당하라.

설계에서 변하는 것이 무엇인지 고려하고 ‘변하는 개념’을 캡슐화 하라 라는 객체지향의 오래된 격언은 변경 보호 패턴의 본질을 잘 설명해준다. 우리가 캡슐화 해야 하는 것은 ‘변경’이다. 변경될 가능성이 높은가? 그렇다면 캡슐화 하라. 그것이 데이터든, 구체적인 타입이든 뭐든!

클래스를 변경에 따라 분리하고 인터페이스를 이용해 변경을 캡슐화하는 것은 설계의 결합도응집도를 향상시키는 매우 강력한 방법이다. 즉, 다형성 패턴을 통해 암시적인 타입을 구체적인 클래스로 분리하고 변경보호 패턴을 통해 안정적인 인터페이스 뒤로 변경(여기서는 구체적인 클래스)을 캡슐화하라.

Movie 클래스도 개선해보자.

지금까지 DiscountCondition변경의 이유를 기준으로 구체적인 타입으로 구분하여 책임을 할당하고, 변경을 캡슐화하기 위해 구체적인 타입역할(인터페이스)로 캡슐화 하였다.

Movie도 개선되기 전의 DiscountCondition과 동일한 문제로 몸살을 앓고 있다. 바로 금액 할인 정책비율 할인 정책이라는 독립적인 타입이 하나의 Movie 클래스 안에서 구현되고 있기 때문에 하나 이상의 이유로 변경될 수 있다. 한 마디로 말해서 응집도가 낮은 것이다.

해결 방법 역시 DiscountCondition과 동일하다. 단, DiscountCondition은 역할을 수행할 클래스들(PeriodCondition, SequenceCondition) 사이에 구현을 공유할 필요가 없기 때문에 인터페이스를 사용했지만 Movie의 경우에는 구현을 공유할 필요가 있다. 따라서 추상클래스를 이용하여 역할을 구현하자.

이제 모든 구현이 끝났다. 모든 클래스의 내부 구현은 캡슐화되어있고 모든 클래스는 변경의 이유를 오직 한 가지씩만 가진다. 또한 각 클래스는 응집도가 높고 다른 클래스와 최대한 느슨하게 결합되어 있다. 클래스는 작고 오직 한 가지의 일만 수행한다. 책임 역시 적절히 분배되어 있다. 이것이 책임을 중심으로 설계를 할 때 얻을 수 있는 혜택이다.

도메인의 구조가 코드의 구조를 이끈다.

위의 구조가 이번 장을 처음 시작할 때 소개됐던 도메인 모델의 구조와 유사하다는 것을 눈여겨 보자. 도메인 모델은 단순히 설계에 필요한 용어를 제공하는 것을 넘어 코드의 구조에도 영향을 미친다.

여기서 강조하고 싶은 것은 변경 역시 도메인 모델의 일부라는 것이다. 도메인 모델에는 도메인 안에서 변하는 개념과 이들 사이의 관계가 투영되어 있다. 이번 장 처음 부분의 도메인 모델에는 할인 정책할인 조건이 변경될 수 있다는 도메인에 대한 직관이 반영되어 있다. 그리고 이 직관이 우리의 설계가 가져야 하는 유연성을 이끌었다.

구현을 가이드 할 수 있는 도메인 모델은 선택하라. 객체지향은 도메인의 개념과 구조를 반영한 코드를 가능하게 만들기 때문에 도메인의 구조가 코드의 구조를 이끌어 내는 것은 자연스러울 뿐 아니라 바람직한 것이다.

코드의 구조가 도메인 구조에 대한 새로운 통찰력을 제공한다.

현재의 설계 대로 운영되던 영화 예매 시스템에 새로운 요구사항이 추가되었다고 가정하자. 영화에 설정된 할인 정책을 실행 중에 변경할 수 있어야 한다. 는 요구사항이다.

현재의 설계는 위 요구사항을 반영하기에 조금 난감하다. 각 할인 정책Movie상속한 구조이기 때문에 런타임에 할인 정책을 변경하려면 Movie 구현체 자체를 변경해야 한다. 다시말해 할인 정책Movie강하게 결합된다.

여기서 변경되는 부분이 무엇인지를 잘 살펴보자. 처음 설계할 당시에는 영화의 할인 정책은 변경되는 대상이 아니었다. 하지만 이번 요구사항으로 인해 할인 정책은 변경되는 대상이 되었다. 즉, 유연하게 변경 가능한 구조가 필요한 것이다.

런타임에 타입을 유연하게 변경하기 위해서는 상속 보다는 합성이 유용하다. 할인 정책을 독립적인 DiscountPolicy로 분리한 뒤 Movie합성시키면 2장에서 살펴보았던 런타임에 변경 가능한 할인 정책의 요구사항을 만족하는 설계가 완성된다.

이 예는 유연성의 압박이 설계에 어떤 영향을 미치는지를 잘 보여준다. 실제로 유연성의존성 관리의 문제이다. 요소들 사이의 의존성 정도가 유연성의 정도를 결정한다. 유연성의 정도에 따라 결합도를 조절할 수 있는 능력은 객체지향 개발자가 갖춰야 하는 중요한 기술 중 하나다.

코드의 구조가 바뀌면 도메인에 대한 관점도 함께 바뀐다. 할인 정책을 자유롭게 변경할 수 있다는 것은 도메인에 포함된 중요한 요구사항이다. 이 요구사항을 반영하기 위해 할인 정책이라는 개념을 코드 상에 명시적으로 드러냈다면 도메인 모델 역시 코드의 관점에 따라 바뀌어야 한다.

도메인 모델은 단순히 도메인의 개념과 관계를 모아놓은 것이 아니다. 도메인 모델은 구현과 밀접한 관계를 맺어야 한다.

도메인 모델은 코드에 대한 가이드를 제공할 수 있어야 하며 코드의 변화에 발맞춰 함께 변화해야 한다.

책임 주도 설계의 대안

책임 주도 설계에 익숙해지기 위해서는 부단한 노력과 시간이 필요하다. 설계를 진행하는 동안 데이터가 아닌 책임 관점에서 사고하기 위해서는 충분한 경험과 학습이 필요하다.

책임과 객체 사이에서 방황할 때 돌파구를 찾기 위한 방법중 하나는 최대한 빠르게 목적한 기능을 수행하는 코드를 작성하는 것이다.

일단 실행되는 코드를 얻고난 후에 코드 상에 명확하게 드러나는 책임들을 올바른 위치로 이동시키는 리팩터링 작업을 통해 책임 주도 설계의 절차대로 진행한 설계와 같은 결과물을 얻을 수 있다.

자 그렇다면 이제 절차 지향 설계(데이터 중심설계)로부터 책임 재배치(리팩터링) 작업을 통해 위에서 말한 방법이 어떤 건지 알아보자.

메서드 응집도

데이터 중심으로 설계된 영화 예매 시스템에서 도메인 객체들은 단지 데이터의 집합(데이터 객체)일 뿐이며 영화 예매를 처리하는 모든 절차는 제어 객체ReservationAgency에 집중돼 있다. 따라서 ReservationAgency에 포함된 로직들을 적절한 객체의 책임으로 분배하면 책임 주도 설계와 거의 유사한 결과를 얻을 수 있다.

위 코드는 데이터 중심 설계대로 작성된 절차적 코드이다. 살펴보면 알겠지만 reserve 메서드의 길이가 너무 길고 이해하기도 어렵다. 긴 메서드는 다양한 측면에서 코드와 유지보수에 부정적인 영향을 끼친다.

  • 어떤 일을 수행하는지 한눈에 파악하기 어렵기 때문에 코드를 전체적으로 이해하는데 너무 많은 시간이 든다.
  • 하나의 메서드 안에서 너무 많은 작업을 처리하기 때문에 변경이 필요할 때 마다 수정해야 할 부분을 찾기 어렵다.
  • 메서드 내부의 일부 로직만 수정하더라도 메서드의 나머지 부분에서 버그가 발생할 확률이 높다.
  • 로직의 일부만 재사용 하는것이 불가능하다.
  • 코드를 재사용하는 유일한 방법은 원하는 코드를 그대로 복붙하는 방법일 뿐이므로 코드 중복을 초래하기 쉽다.

자 첫 번째로 절차 지향 코드의 문제점인 긴 메서드 문제를 발견하였다.

메서드를 작게 분해해서 각 메서드의 응집도를 높이자. 메서드의 응집도를 높이는 이유도 변경과 관련이 깊다. 응집도 높은 메서드는 변경의 이유가 단 하나여야 한다.

객체로 책임을 분배(리팩터링)할 때 가장 먼저 할 일은 메서드를 응집도 있는 수준으로 분해하는 것이다.

아래는 위의 ReservationAgency를 응집도 높은 메서드들로 잘게 분해한 것이다.

이제 ReservationAgency 클래스는 오직 하나의 작업만 수행하고, 하나의 변경 이유만 가지는 작고 명확하고 응집도가 높은 메서드들로 구성돼 있다. 비록 클래스의 길이는 길어졌지만 일반적으로 명확성의 가치가 클래스의 길이보다 더 중요하다.

이렇게 조그마한 부분에서 개선된 명확성이 모여 변경하기 쉬운 코드가 만들어진다.

일단 메서드를 분리하고 나면 public 메서드는 상위 수준의 명세를 읽는 것 같은 느낌이 든다.

코드를 작은 메서드들로 분해하면 전체적인 흐름을 이해하기도 쉬워진다.

큰 메서드를 작은 메서드들로 나누면 한 번에 기억해야 하는 정보를 줄일 수 있다. 더 세부적인 정보가 필요하다면 그 때 각 세머드의 세부적인 구현을 확인하면 되기 때문이다.

수정 후의 코드는 변경하기도 더 쉽다. 각 메서드는 단 하나의 이유에 의해서만 변경된다.

안타깝게도 메서드 응집도는 높아졌지만 이 메서드들을 담고 있는 ReservationAgency의 응집도는 여전히 낮다. ReservationAgency의 응집도를 높이기 위해서는 변경의 이유가 다른 메서드들을 적절한 위치로 분배해야 한다.

객체를 자율적으로 만들자.

어떤 메서드를 어떤 클래스로 이동시켜야 할까? 객체는 자율적인 존재라는 사실을 떠올리면 쉽게 답이 가능하다. 자신이 소유하고 있는 데이터를 자기 스스로 처리하도록 만드는 것이 자율적인 객체를 만드는 지름길이다.

위 내용을 기억하고 ReservationAgencyisDiscountable 메서드를 살펴보자.

isDiscountable 메서드는 DiscountCondition에 속한 데이터를 주로 이용한다는 것을 알 수 있다. 이 메서드들을 데이터가 존재하는 DiscountCondition으로 이동시키자.

이제 ReservationAgency는 할인 여부를 판단하기 위해 DiscountConditionisDiscountable 메서드를 호출하도록 변경된다.

이렇게 각 응집도 있는 메서드들을 (자율적 객체가 되도록)적절한 객체에게 분배한 뒤 다형성 패턴변경 보호 패턴을 적절히 적용하면 우리의 최종 설계와 유사한 모습의 코드를 얻게 될 것이다.

여기서 하고 싶은 말은 책임 주도 설계 방법에 익숙하지 않다면 일단 데이터 중심으로 구현한 뒤 메서드 응집도를 높이고 객체들에게 책임을 분배하는 리팩터링 작업을 하더라도 유사한 결과를 얻을 수 있다는 것이다.

캡슐화, 응집도, 결합도에 대해 이해하고 훌륭한 객체지향 원칙을 적용하기 위해 노력한다면 책임 주도 설계 방법을 단계적으로 따르지 않더라도 유연하고 깔끔한 코드를 얻을 수 있을 것이다.

참고 및 출처

  • 오브젝트