본문 바로가기

도서/Object

[Object] Chapter 05 - 책임 할당하기

반응형

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

책임 할당 과정은 일종의 트레이드오프 활동이다.

동일한 문제를 해결할 수 있는 다양한 책임 할당 방법이 존재하며, 어떤 방법이 최선인지는 문맥에 따라 달라진다.

 

이번에 살펴볼 GRASP 패턴을 통해 책임 할당의 어려움을 해소할 수 있다.

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

 

01 책임 주도 설계를 향해

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

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

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

 

 

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

객체에게 중요한 것은 데이터가 아니라 외부에 제공하는 행동이다. 데이터는 객체가 책임을 수행하는 데 필요한 재료를 제공할 뿐이다.

따라서 객체지향 설계에서 가장 중요한 것은 객체에게 적절한 책임을 할당하는 능력이 필요하다는 것이다. 그렇다면 객체에게 어떤 책임을 할당해야 하는가? 해결의 실마리를 협력에서 찾을 수 있다.

 

 

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

객체에게 할당된 책임의 품질은 협력에 적합한 정도로 결정된다.

협력을 시작하는 주체는 메시지 전송자이기 때문에 협력에 적합한 책임이란 메시지 수신자가 아니라 메시지 전송자에게 적합한 책임을 의미한다.

협력에 적합한 책임을 수확하기 위해서는 객체를 결정한 후에 메시지를 선택하는 것이 아니라 메시지를 결정한 후에 객체를 선택해야 한다.

메시지가 필요하고 존재하기 때문에 그 메시지를 처리할 객체가 필요한 것이다.

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

 

메시지를 먼저 결정하기 때문에 메시지 송신자는 메시지 수신자에 대한 어떠한 가정도 할 수 없다.

메시지 전송자의 관점에서 메시지 수신자가 깔끔하게 캡슐화되는 것이다.

책임 중심의 설계가 응집도가 높고 결합도가 낮으며 변경하기 쉽다고 말하는 이유가 여기에 있다.

 

위 두 원칙은 3장에서 소개한 책임 주도 설계 방법의 핵심과 거의 동일하다는 것을 알 수 있다.

 

책임 주도 설계

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

 

02 책임 할당을 위한 GRASP 패턴

다양한 책임 할당 기법이 고안되었는데, 그중에서 대중적으로 널리 알려진 것이 GRASP 패턴이다.

GRASP: General Responsibility Assignment Software Pattern (일반적인 책임 할당을 위한 소프트웨어 패턴)

 

 

도메인 개념에서 출발하기

설계를 시작하기 전에 도메인에 대한 개략적인 모습을 그려 보는 것이 유용하다.

도메인 안에는 무수히 많은 개념들이 존재하며 이 도메인 개념들을 책임 할당의 대상으로 사용하면 코드에 도메인의 모습을 투영하기 좀 더 수월해진다. 따라서 어떤 책임을 할당해야 할 때 가장 먼저 고민해야 하는 유력한 후보는 바로 도메인 개념이다.

 

 

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

책임 주도 설계 방식의 첫 단계는 애플리케이션이 제공해야 하는 기능을 애플리케이션의 책임으로 생각하는 것이다.

이 책임을 애플리케이션에 대해 전송된 메시지로 간주하고 이 메시지를 책임질 첫 번째 객체를 선택하는 것으로 설게를 시작한다.

 

영화 예매 시스템을 예로 설명하자면 사용자에게 제공해야 하는 기능은 영화를 예매하는 것이다.

이를 책임으로 간주하면 애플리케이션은 영화를 예매할 책임이 있다고 말할 수 있다.

이제 이 책임을 수행하는 데 필요한 메시지를 결정한다. 메시지는 메시지를 수신할 객체가 아니라 메시지를 전송할 객체의 의도를 반영해서 결정한다.

따라서 첫 번째 질문은 다음과 같다.

 

" 메시지를 전송할 객체는 무엇을 원하는가? "

 

협력을 시작하는 객체는 미정이지만 원하는 바는 분명해 보인다. 바로 영화를 예매하는 것이다.

 

" 예매하라 "

 

메시지를 결정했으므로 그에 적합한 객체를 선택해야 한다. 두 번째 질문은 다음과 같다.

 

" 메시지를 수신할 적합한 객체는 누구인가? "

 

이 질문에 답하기 위해서는 객체는 자신의 상태를 스스로 처리하는 자율적인 존재여야 한다는 사실에 집중해야 한다.

객체의 책임과 책임을 수행하는 데 필요한 상태는 동일한 객체 안에 존재해야 한다. 따라서 객체에게 책임을 할당하는 첫 번째 원칙은 책임을 수행할 정보를 알고 있는 객체에게 책임을 할당하는 것이다. GRASP에서는 이를 INFORMATION EXPERT(정보 전문가) 패턴이라고 부른다.

 

INFORMATION EXPERT 패턴에 따르면 예매하는 데 필요한 정보를 가장 많이 알고 있는 객체에게 예매하라 메시지를 처리할 책임을 할당해야 한다. '상영'이라는 도메인 개념이 적합할 것이다.

 

예매하라 메시지를 수신했을 때 Screening(상영)이 수행해야 하는 작업의 흐름을 생각해보자. 이제부터는 외부의 인터페이스가 아니라 Screening 내부로 들어가 메시지를 처리하기 위해 필요한 절차와 구현을 고민해보는 것이다. 개략적인 수준에서 책임을 결정하는 단계이기 때문에 너무 세세한 부분까지 고려할 필요는 없으며, 책임을 수행하는 데 필요한 작업을 구상해보고 스스로 처리할 수 없는 작업이 무엇인지를 가릴 정도의 수준이면 된다.

 

만약 스스로 처리할 수 없다면 외부에 도움을 요청해야 한다. 이 요청이 외부로 전송해야 하는 새로운 메시지가 되고, 최종적으로 이 메시지가 새로운 객체의 책임으로 할당된다. 이 같은 연쇄적인 메시지 전송과 수신을 통해 협력 공동체가 구성되는 것이다.

 

위 과정을 통해 살펴본 것처럼 INFORMATION EXPERT 패턴은 객체에게 책임을 할당할 때 가장 기본이 되는 책임 할당 원칙이다.

 

 

높은 응집도와 낮은 결합도

설계는 트레이드오프 활동이라는 것을 기억하라. 동일 기능을 구현할 수 있는 무수히 많은 설계가 존재하기 때문에 실제로 설계를 진행하다 보면 한 가지 설계를 선택해야 하는 경우가 빈번하게 발생한다.

이 경우에는 올바른 책임 할당을 위해 INFORMATION EXPERT 패턴 외의 다른 책임 할당 패턴들을 함께 고려할 필요가 있다.

 

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

GRASP에서는 이를 LOW COUPLING(낮은 결합도) 패턴과 HIGH COHESION(높은 응집도) 패턴이라고 부른다.

 

 

창조자에게 객체 생성 책임을 할당하라

영화 예매의 최종 결과물은 Reservation 인스턴스를 생성하는 것이다. 이것은 협력에 참여하는 어떤 객체에게는 Reservation 인스턴스를 생성할 책임을 할당해야 한다는 것을 의미한다. GRAPS의 CREATOR(창조자) 패턴은 이 같은 경우에 사용할 수 있는 책임 할당 패턴으로서 객체를 생성할 책임을 어떤 객체에게 할당할지에 대한 지침을 제공한다.

 

아래 조건을 최대한 많이 만족하는 B에게 객체 생성 책임을 할당하라

  • B가 A 객체를 포함하거나 참조한다.
  • B가 A 객체를 기록한다.
  • B가 A 객체를 긴밀하게 사용한다.
  • B가 A 객체를 초기화하는 데 필요한 데이터를 가지고 있다(이 경우 B는 A에 대한 정보 전문가다)

CREATOR 패턴의 의도는 어떤 방식으로든 생성되는 객체와 연결되거나 관련 필요가 있는 객체에 해당 객체를 생성할 책임을 맡기는 것이다. 따라서 어떤 방식으로든 생성될 객체와 연결될 것이다. 다시 말해 두 객체는 결합된다.

이미 결합된 객체에게 생성 책임을 할당하는 것은 설계 전체적인 결합도에 영향을 미치지 않는다. 결과적으로 CREATOR 패턴은 이미 존재하는 객체 사이의 관계를 이용하기 때문에 설계가 낮은 결합도를 유지할 수 있다.

 

03 구현을 통한 검증

 

클래스 응집도 판단하기

클래스가 다음과 같은 징후로 몸살을 앓고 있다면 클래스의 응집도는 낮은 것이다.

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

 

다형성을 통해 분리하기

Movie 입장에서 보면 순번조건(SequenceCondition) 기간 조건(PeriodCondition)은 아무 차이 없다. 둘 모두 할인 여부를 판단하는 동일한 책임을 수행하고 있을 뿐이다. 두 클래스가 할인 여부를 판단하기 위해 사용하는 방법이 서로 다르다는 사실은 Movie 입장에서는 그다지 중요치 않다. 그저 할인 가능 여부를 반환해 주기만 하면 된다.

 

자연스럽게 역할의 개념이 무대 위로 등장한다. Movie 입장에서 SequenceCondition과 PeriodCondition이 동일한 책임을 수행한다는 것은 동일한 역할을 수행한다는 것을 의미한다. 역할은 협력 안에서 대체 가능성을 의미하기 때문에 SequenceCondition과 PeriodCondition에 역할의 개념을 적용하면 Movie가 구체적인 클래스를 알지 못한 채 오직 역할에 대해서만 결합되도록 의존성을 제한할 수 있다.

객체의 타입에 따라 변하는 행동이 있다면 타입을 분리하고 변화하는 행동을 각 타입의 책임으로 할당하는 것이다. GRASP에서는 이를 POLYMORPHISM(다형성) 패턴이라고 부른다.

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

POLYMORPHSIM 패턴은 객체의 타입을 검사해서 타입에 따라 여러 대안들을 수행하는 조건적인 논리를 사용하지 말라고 경고한다. 대신 다형성을 이용해 새로운 변화를 다루기 쉽게 확장하라고 권고한다.

 

변경과 유연성

설계를 주도하는 것은 변경이다. 개발자로서 변경에 대비할 수 있는 두 가지 방법이 있다.

  • 코드를 이해하고 수정하기 쉽도록 최대한 단순하게 설계하는 것
  • 코드를 수정하지 않고도 변경을 수용할 수 있도록 코드를 더 유연하게 만드는 것

대부분의 경우에 전자가 더 좋지만 유사한 변경이 반복적으로 발생하고 있다면 복잡성이 상승하더라도 유연성을 추가하는 후자의 방법이 더 좋다.

 

04 책임 주도 설계의 대안

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

아무것도 없는 상태에서 책임과 협력에 관해 고민하기 보다는 일단 실행되는 코드를 얻고 난 후에 코드 상에 명확하게 드러나는 책임들을 올바른 위치로 이동시키는 것이다.

주의할 점은 코드를 수정한 후에 겉으로 드러나는 동작이 바뀌어서는 안 된다는 것이다.

캡슐화를 향상시키고, 응집도를 높이고, 결합도를 낮춰야 하지만 동작은 그대로 유지해야 한다.

이처럼 이해하기 쉽고 수정하기 쉬운 소프트웨어로 개선하기 위해 겉으로 보이는 동작은 바꾸지 않은 채 내부 구조를 변경하는 것을 리팩터링이라고 부른다.

 

메서드 응집도

긴 메서드는 다양한 측면에서 코드의 유지보수에 부정적인 영향을 미친다.

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

한 마디로 긴 메서드는 응집도가 낮기 때문에 이해하기도 어렵고 재사용하기도 어려우며 변경하기도 어렵다. 이와 같은 메서드를 몬스터 메서드라고 부른다.

클래스의 응집도와 마찬가지로 메서드의 응집도를 높이는 이유도 변경과 관련이 깊다. 응집도가 높은 메서드는 변경되는 이유가 단 하나여야 한다. 클래스가 작고, 목적이 명확한 메서드들로 구성돼 있다면 변경을 처리하기 위해 어떤 메서드를 수정해야 하는지를 쉽게 판단할 수 있다.

 

객체를 자율적으로 만들자

객체를 자율적으로 만들기 위해서는 변경의 이유가 다른 메서드들을 적절한 위치로 분배해야 한다. 예상하겠지만 적절한 위치란 바로 각 메서드가 사용하는 데이터를 정의하고 있는 클래스를 의미한다.

 

책임 주도 설계 방법에 익숙하지 않다면 일단 데이터 중심으로 구현한 후 이를 리팩터링 하더라도 유사한 결과를 얻을 수 있다.

반응형