https://product.kyobobook.co.kr/detail/S000001766367

 

오브젝트 | 조영호 - 교보문고

오브젝트 | 역할, 책임, 협력을 향해 객체지향적으로 프로그래밍하라!객체지향으로 향하는 첫걸음은 클래스가 아니라 객체를 바라보는 것에서부터 시작한다. 객체지향으로 향하는 두번째 걸음

product.kyobobook.co.kr

책 목차:

1장 객체, 설계 : https://inhyeok-blog.tistory.com/32
2장 객체지향 프로그래밍 : https://inhyeok-blog.tistory.com/33
3장 역할, 책임, 협력 : https://inhyeok-blog.tistory.com/34
4장 설계 품질과 트레이드오프 : https://inhyeok-blog.tistory.com/36
5장 책임 할당하기 : https://inhyeok-blog.tistory.com/37
6장 메시지와 인터페이스 :
7장 객체 분해 :
8장 의존성 관리하기 :
9장 유연한 설계 :
10장 상속과 코드 재사용 :
11장 합성과 유연한 설계 :
12장 다형성 :
13장 서브클래싱과 서브타이핑 :
14장 일관성 있는 협력 :
15장 디자인 패턴과 프레임워크 :


앞서 살펴본 데이터 중심의 설계가 가지는 여러 문제를 해결할 수 있는 가장 좋은 방법은 책이 중심의 설계를 하는 것이다. 하지만 책임 중심의 설계는 어떤 객체에게 어떠 책임을 할당할지 결정하기 어렵다. 이를 해결하기 위해서 이번장은 GRASP 패턴을 확인해 본다. 그리고 책임 주도 설계의 과정을 한 걸음씩 따라가 보면서 객체에 책임을 할당하는 기본적인 원리를 살펴본다.

01. 책임 주도 설계를 향해

책임 주도 설계는 2가지 원칙을 따라야 한다.

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

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

객체에게 중요한 것은 데이터가 아니라 외부에 제공하는 행동이다. 객체는 협력에 참여하기 위해 존재하며, 협력 안에서 수행하는 책임이 객체의 존재가치를 증명한다.

데이터는 행동에 필요한 재료일 뿐이다. 따라서 우리는 객체의 데이터에서 행동으로 무게중심을 옮겨야 한다. 이 목표는 객체의 행동, 즉 책임을 먼저 결정한 후에 객체의 상태를 결정하므로써 달성할 수 있다.

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

객체에게 할당된 책임의 품질은 협력에 적합한 정도로 결정된다. 이때 협력에서 책임의 적합도는 메시지를 보내는 클라이언트의 의도에 적합한 책임을 말한다. 이를 위해서 메시지를 결정한 후에 객체를 선택해야 한다. 메시지를 결정한 후에 객체를 선택하게 된다면 메시지 송신자(클라이언트 객체)는 수신자에 대해 어떤 가정도 할 수 없다. 메시지 전송자의 관점에서 메시지 수신자가 캡슐화 되는 것이다. 

책임 주도 설계

이제 책임 주도 설계를 더 자세히 살펴 볼 것이다. 이를 위해서 책임 주도 설계의 흐름을 한번 더 확인해보자.

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

02. 책임 할당을 위한 GRASP 패턴

책임 할당 기법중에 가장 유명한 것은 GRASP패턴이다. 이를 통해서 책임 할당을 살펴보자.

도메인 개념에서 출발하기

설계 전에 도메인에대한 개략적인 모습을 그려보는 것은 유용하다. 예컨데 요구사항을 달성하는데 필요한 요소들을 간단하게 모델링 해 보는 것이다. 이때 중요한 것은 도메인 모델에 시간을 오래 쓰지 말아야 한다. 도메인 개념들의 의미와 관계는 설계를 하기전에 유용한 정보를 제공할 수 있다는 것으로 충분하다.

완벽한 도메인 모델은 존재하지 않는다. 설계를 고도화 할수록, 그리고 코드를 작성할 수록 더 나은 설계가 나오기 마련이다. 다시한번 강조한다. 도메인 개념은 설계를 시작하기전에 유용한 정보를 제공하는 수준에서 멈춰야한다. 더 이상의 노력을 들이지 말자

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

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

 

우린 앞서 책임 주도 설계에서 계속해서 책임을 나누고, 객체에 할당하는 과정을 반복해야 한다는 사실을 확인했다. 그럼 구체적으로 "책임을 나누는 방법""객체에 할당하는 방법"은 무엇일까?

  1. 메시지를 전송할 객체는 무엇을 원하는가?
    메시지(책임)은 메시지를 전송할 객체(클라이언트 객체)의 의도를 반영해서 결정해야 한다. 그래야 캡슐화를 성공적으로 구현할 수 있다.
    메시지를 클라이언트 입장에서 정의 해야한다는 사실은 최범균님의 HOW보단 WHAT을 드러내라라는 조언과 일맥상통한다.
  2. 메시지를 수신할 적합한 객체는 누구인가?
    객체는 자율적인 존재여야 한다. 따라서 책임을 수행할 정보(상태; 데이터)를 알고 있는 객체에게 책임을 할당해야 한다. 이를 GRASP에서는 INFORMATION EXPERT(정보 전문가) 패턴이라고 부른다. 이때 정보 전문가는 다른 객체에게 메시지를 보내서 얻을 수 있는 정보도 본인 소유의 정보라고 생각하는 것 이다. 그리고 객체가 메시지를 수행하기 위해서 메소드를 구현 할 때 필요에 따라서 메시지를 정의하고, 다른 정보 전문가를 찾는 것이다.

높은 응집도와 낮은 결합도

사실 동일한 기능을 구현하는 설계는 무수히 많다. 설계는 트레이드오프 활동이라는 것을 기억하라. 그럼 수많은 설계 중에 어떤 설계를 선택해야 할까?

높은 응집도와 낮은 결합도는 객체에 책임을 할당할 때 항상 고려해야 하는 기본 원리다. 이를 GRASP에서는 LOW COUPLING(낮은 결합도) 패턴, HIGH COHESION(높은 응집도) 패턴 이라고 부른다.

낮은 결합도를 달성하기 위해서는 이미 결합되어 있는 객체에게 메시지를 보내면 된다. 즉, 결합되는 객체의 수를 최소한으로 유지하라는 것이다. 

높은 응집도를 달성하기 위해서는 객체의 주된 책임을 확인해서 더 적합한 객체에게 메시지를 보내라. 이는 변경할 이유를 하나로 유지하는 방법에 대한 논의로, 이미 해당 객체가 변경될 이유에서 새로운 이유가 추가되지 않는다면 높은 응집도를 유지한 상태로 메시지를 처리할 수 있게 되는 것이다.

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

객체 A를 생성해야할 책임은 아래 조건을 최대한 많이 만족하는 B에게 할당해야 한다.

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

이러한 방식을 GRASP에서는 CREATOR(창조자) 패턴이라 칭한다. 이는 이미 결합돼 있는 객체에게 생성 책임을 할당하는 것으로 설계의 전체적인 결합도에 영향을 미치지 않도록 하는 것이다.

03. 구현을 통한 검증

DiscountCondition 개선하기

설계를 개선하는 작업은 변경의 이유가 하나 이상인 클래스를 찾는 것으로부터 시작하는 것이 좋다. 이는 클래스의 응집도가 낮은 것인데, 클래스의 응집도를 판단할 수 있는 세 가지 방법이 있다.

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

타입 분리하기

DiscountCondition는 순번조건과 기간조건이라는 두개의 타입을 하나의 클래스에서 처리한다. 이는 앞서 말한 개선번으로, SequenceCondition과 PeriodCondition으로 분리하면 된다. 그리고 Movie가 기존에 DiscountCondition만 의존하고 있던 상황을 SequenceCondition과 PeriodCondition을 동시에 의존하도록 코드를 변경하면 된다. 그런데 문제가 생겼다. 응집도를 높히려다 보니까 덩달아 시스템의 전체적인 결합도가 높아져 버린 것이다. 이는 다음과 같은 문제를 발생시킨다.

  • Movie클래스가 PeriodCondition과 SequenceCondition 클래스 양쪽 모두에게 결합된다는 것
  • 새로운 할인 조건을 추가하기 어려워졌다. 새로운 할인조건은 Movie 클래스에게 새로운 의존성은 물론이고, 새로운 메서드까지 추가하게 만든다.

다형성을 통해 분리하기

사실 Movie입장에서 PeriodCondition과 SequenceCondition에게 보내는 메시지는 동일하다. 즉, 동일한 책임들을 수행하는 것이고, 같은 역할을 수행한다는 것이다. 협력안에서 역할은 대체 가능성을 의미하기 때문에, Movie는 구체적인 클래스는 알지 못한 채 오직 역할(Interface or Abstract)에 대해서만 결합되도록 의존성을 제한할 수 있는 것이다.

이 사례에서 알 수 있듯, 객체의 구체적인 타입에 따라 행동을 분기해야 한다면 명시적인 클래스를 정의해서 메시지를 수신하고 행동을 나눔으로써 응집도 문제를 해결할 수 있다. 이를 GRASP에서는 POLYMORPHISM(다형성) 패턴이라고 부른다.

프로그램을 if ~ else 또는 switch ~ case 등의 조건 논리를 사용해서 설계한다면 새로운 변화가 일어난 경우 조건 논리를 수정해야한다. 이럴 때 POLYMORPHISM 패턴은 객체의 타입을 검사해서 타입에 따라 여러 대안들을 수행하는 조건적인 논리를 사용하지 말라고 경고한다.

변경으로부터 보호하기

새로운 할인 조건을 추가하는 경우에는 어떻게 해야할까? 그냥 DiscountCondition을 상속받는 새로운 조건 객체를 만드는 것 만으로 충분하다. 어차피 Movie는 DiscountCondition이라는 인터페이스에 의존하기 때문에 전혀 문제가 없다.이처럼 변경을 캡슐화하도록 책임을 할당하는 것을 GRASP에서는 PREOTECTED VARIATIONS(변경 보호) 패턴이라고 부른다.

변화가 예상되는 지점을 식별하고 그 주위에 안정된 인터페이스를 형성하도록 책임을 할당하라. 우리가 캡슐화해야 하는 것은 변경이다.

Movie 클래스 개선하기

Movie 클래스도 응집도가 낮다. 금액 할인 정책과 비율 할인 정책 두 가지 타입을 한 클래스에서 구현하고 있기 때문이다. 이를 개선하기 위해서 POLYMORPHSM 패턴을 적용하자. 할인 정책을 타입별로 분리하고, 메시지로만 협력하게 하는 것이다. 그리고 우리는 이를통해 PROTECTED VARIATIONS 패턴의 목표도 달성할 수 있다.

책에서는 Movie를 추상클래스로 만들고 AmountDiscountMovie, PercentDiscountMovie, NoneDiscountMovie 3가지 구체클래스로 상속받아 구현했다. 이 방식을 통해서 Movie의 공통되는 구현을 재사용하고 타입을 나눠서 POLYMORPHISM 패턴을 구현했다.
이 방식은 흔히 말하는 Template Method Pattern(템플릿 메소드 패턴)이다. 이 방식을 통해서 특정 작업을 처리하는 일부분을 서브 클래스로 캡슐화하여 전체적인 구조는 바꾸지 않으면서 특정 단계에서 수행하는 내용을 바꿀 수 있다. 자주 사용하는 패턴이기도 하고, 다형성을 효과적으로 수행하는 방식이다(물론 코드 재사용을 위한 상속은 좋지 않다. 상속보다는 합성임을 기억하자). 단적인 예로 Spring Framework에서 AOP를 구현하기 위해서 Template Method Pattern을 변형해서 사용했다. 

위의 그림은 최종 결과물이다. 그런데 이 그림은 앞서 만든 도메인 모델과 유사하다. 우린 도메인 모델을 만들 때 할인 정책과 할인 조건이 변경될 수 있다는 직관을 포함했다. 그리고 이 직관은 우리의 설계를 유연하게 만들었다. 도메인의 역할은 구현을 가이드하는 것이다. 객체지향은 도메인의 개념과 구조를 반영한 코드를 가능하게 만들기 때문에 도메인의 구조가 코드의 구조를 이끌어 내도록 해야 하는 것이다.

변경과 유연성

설계를 주도하는 것은 변경이다. 변경을 대비할 수 있는 방법은 2가지가 있다.

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

앞선 그림에서 Movie를 상속해서 할인 정책을 구현현했다. 그런데 실행중 할인 정책을 변경해야 한다면 문제가 생긴다. Movie를 상속받은 객체를 다시 생성해서 필요한 정보를 복사해야한다. 또한 새로 생성된 객체는 물리적으로 다를 객체이기 때문에 모든 식별자를 갱신해 줘야하는 어려움도 있다.

2장에서 코드를 재사용하기 위해서는 합성을 사용하라고 조언했다. 이처럼 DiscountPolicy 인터페이스를 만들어서 할인정책을 캡슐화해보자.

이제 Movie에 연결된 DiscountPolicy를 교체하는 것만으로 할인 정책을 변경할 수 있다. 객체를 복사할 필요도, 식별자를 갱신할 필요도 없다.

우리는 변경을 대비하기 위해 코드의 구조가 바뀌는 모습을 지켜봤다. 그럼 도메인에 대한 관점도 변경되어야 한다. 

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

하지만 여전히 책임을 올바르게 할당하는 것은 어려운 일이다. 이를 위해 책임 주도 설계가 아닌 두번째 대안을 제시한다. 바로 절차형 코드로 실행되는 프로그램을 빠르게 작성한 후 완성된 코드를 객체지향적인 코드로 변경하는 것이다.(어쩌면 실무에서 더 많이 사용되고 있는 방법이다)

04. 책임 주도 설계의 대안

숙련된 설계자도 적절한 책임과 객체를 선택하기는 어렵다. 아무것도 없는 상태에서 책임과 협력에 관해 고민하기 보다는 빠르게 실행되는 코드를 얻고 리팩토링 하는 방법도 좋은 방법이다. 이때 주의할 점은 코드를 수정한 후에 겉으로 드러나는 동작이 바뀌면 안된다는 것이다. 동작을 유지한 채 캡슐화를 향상시키고, 응집도를 높이고, 결합도를 낮춰야한다.

이 방식을 확인하기 위해서 4장에서 만든 데이터 중심 설계를 리팩토링 해보자.

메서드 응집도

데이터 중심의 설계에서는 객체는 단지 데이터의 집합이며, 모든 절차는 ReservationAgency에 집중되어있다.

처음으로 할일은 응집도 높은 메소드로 ReservationAgency를 나누는 것이다. 긴 메서드는 다양한 단점을 가진다.

  • 코드를 이해하는 데 시간이 오래 걸린다.
  • 변경이 발생하면 수정할 부분을 찾기 어렵다.
  • 일부 로직을 수정하면 나머지에서 버그가 발생한다.
  • 로직의 일부를 재사용할 수 없다.
  • 재사용을 위해서 코드를 복붙한다.(코드의 중복이 발생한다.)

이런 긴 메소드를 몬스터 메서드(monster method)라고 부른다.

메서드의 응집도를 높힌다는 것은 메서드가 단 하나의 이유로 변경된다는 것이다. 메서드 응집도를 높히면 각 메서드를 적절한 클래스로 이동시키기 쉬워진다.

 평소에 내가 줄기차게 주장하던 하나의 메서드는 같은 수준의 지시문(private method 등)을 나열해야한다는 내용과 일맥상통한다. 이를 통해 public 메서드는 상위 수준의 명세를 읽는 것 같은 느낌이 들 수 있도록 하는 것이다.

메서드의 응집도를 높히므로서 이제 ReservationAgency 클래스는 오직 하나의 작업만 수행하고, 하나의 변경이유만 가지는 작고, 명확하고, 응집도가 높은 메서드들로 구성돼 있다. 이를 통해 앞서 언급한 긴 메스드의 단점을 모두 해결할 수 있었다. 그리고 이제 메서드를 적절한 위치로 이동시키자. 적절한 위치는 바로 메서드가 사용하는 데이터를 정의하고 있는 클래스이다.

객체를 자율적으로 만들자

자신이 소유하고 있는 데이터를 자기 스스로 처리하도록 만드는 것으로 자율적인 객체를 만들 수 있다. 이를 위해 메서드 안에서 어떤 클래스의 접근자 메서드를 사용하는지 파악하고, 그 객체로 메서드를 옮겨보자. 이를 통해서 캡슐화와 높은 응집도, 낮은 결합도를 가지는 설계를 얻게된다. 여기에 POLYMORPHISM 패턴과 PROTECTED VARIATIONS 패턴을 차례대로 적용하면 우리가 원하는 설계를 얻을 수 있다. 

+ Recent posts