[오브젝트] 5장_책임 할당하기
데이터 중심 설계로 인한 문제를 해결하기 위한 가장 기본적인 방법은, 책임에 초점을 맞추는 것이다.
이번 장에서는 GRASP 패턴을 살펴본다.
결합도와 응집도를 합리적인 수준으로 유지할 수 있는 원칙이 있다. 객체의 행동에 초점을 맞추는 것이다.
이번 장에서는 책임이 아닌, 상태를 표현하는 데이터 중심의 설계를 보고 객체지향적으로 설계한 구조와 어떤 차이가 있는지 확인한다.
상속, 지연 바인딩도 중요하지만, 구현 측면에 치우쳐져 있기 때문에 객체지향 패러다임의 본질과는 거리가 멀다.
01 책임 주도 설계를 향해
두 가지 원칙이 있다.
- 데이터보다 행동을 먼저 결정하라
- 협려이라는 문맥안에서 책임을 결정하라
책임을 드러내는 안정적인 인터페이스 뒤로 책임을 수행하기 위해 필요한 상태를 캡슐화함으로써 구현 변경에 대한 파장이 외부로 퍼져나가는 것을 방지한다. 그래서 책임에 초점을 맞추면 안정적인 설계를 할 수 있다.
데이터보다 행동을 먼저 결정하라
데이터는 객체가 책임 수행을 위해 필요한 재료일 뿐이다.
“이 객체가 해야하는 책임은 무엇인가” 를 결정하고, 이 책임을 수행하는데 필요한 데이터는 무엇인가 결정해라.
99 Page
객체의 종류를 저장하는 인스턴스 변수 (movieType) 와 인스턴의 종류에 따라 배타적으로 상용될 인스턴스 변수 (discountAmount, discountPercent) 를 하나의 클래승 안에 포함시키는 방식은 데이터 중심 설계에서 흔히 보인다.
메세지를 수신한 객체는 메세드를 실행해 요청에 응답한다. Screening 이 Movie 에게 처리를 위임하는 이유는 요금을 계산하는데 필요한 기본 요금과 할인 정책을 가장 잘 알고 있는 객체가 Movie 이기 때문이다.
자율적인 객체란 자신의 상태를 직접 관리하고 스스로의 결정에 따라 행동하는 객체이다. 객체의 자율성을 보장하기 위해서는 필요한 정보와 정보에 기반한 행동을 같은 객체 안에 모아두어야 한다.
결과적으로 객체를 자율적으로 만드는 가장 기본적인 방법은 내부 구현을 캡슐화 하는 것이다.
자율적인 객체는 자신에게 할당된 책임을 수행하던 중에 필요한 정보를 알지 못하거나 외부의 도움이 필요한 경우 적절한 객체에게 메세지를 전송해 협력을 요청한다.
협력이라는 문맥 안에서 책임을 결정하라
책임은 객체의 입장이 아니라, 객체가 참여하는 협력에 적합해야한다. 즉, 메세지를 전송하는 클라이언트의 의도에 적합한 책임을 할당해야한다.
책임 주도 설계
- 시스템이 사용자에게 제공해야 하는 기능인, 시스템 책임을 파악
- 시스템 책임을 더 작은 책임으로 분할
- 분할된 책임을 수행할 적절한 객체, 또는 역할을 찾아 책임 할당
- 객체가 책임 수행중 다른 객체의 도움이 필요하면, 이를 책임질 적절한 객체나 역할 찾음
- 해당 객체 또는 역할에게 책임 할당해서 두 객체가 협력하도록
02 책임 할당을 위한 GRASP 패턴
General Responsibility Assignment Software Pattern : 일반적인 책임 할당을 위한 소프트웨어 패턴
객체에게 책임을 할당할 때 지침으로 삼을 수 있는 원칙들의 집합을 패턴 형식으로 정리한 것이다.
도메인 개념에서 출발하기
어떤 책임을 할당할 때 가장 먼저 고민해야하는 유력한 후보는 도메인 개념이다. 필요한 것은 도메인을 그대로 투영한 모델이 아니라, 구현에 도움이 되는 모델이다.
도메인 개념을 정리하는데 많은 시간을 들이지 말고,빠르게 설계와 구현을 진행하라.
정보 전문가에게 책임을 할당하라
메세지를 전송할 객체는 무엇을 원하는가 ?
협력을 시작하는 객체는 미정이다. 하지만 객체가 원하는 것은 영화를 예매하는 것이다. 따라서 메세지는 “예매하라”메세지를 수신할 객체는 누구인가 ?
책임을 수행할 정보를 알고 있는 객체에게 책임을 할당해라. (정보 전문가 패턴) 객체는 해당 정보를 제공할 수 있는 다른 객체를 알고 있거나 필요한 정보를 계산해서 제공할 수 있다. 정보 전문가가 반다스 데이터를 저장하고 있을 필요는 없다. 여기서는, 영화에 대한 정보와 상영 시간, 상영 순번 등 예매에 필요한 정보를 알고 있는 “상영” 이 적합하다.Screening 내부로 들어가 메세지 처리에 필요한 절차와 구현을 고민한다.
“예매하라” 메세지를 완료하기 위해 예매 가격을 계산하는 작업이 필요하다. 가격 계산하는데 필요한 정보를 상영이 모르기 때문에 외부 객체에게 도움을 요청해야한다.새로운 메세지
“가격을 계산하라”책임질 객체 선택
Movie가격 계산을 위해 Movie 가 어떤 작업을 해야하나
할인 조건에 따라 영화가 할인 가능한지 판단해아한다새로운 메세지
“할인 여부를 판단해라”책임질 객체 선택
DiscountConditionDiscountCondition 은 자체적으로 할인 여부를 판단하는데필요한 정보를 알고 있어서 외부의 도움 없이 스스로 판단할 수 있다.
높은 응집도와 낮은 결합도
Movie 대신 Screening 이 직접 DiscountCondition 과 협력하도록 하는건 어떨까 ?
높은 응집도와 낮은 결합도를 얻을 수 있는 설계를 선택해야한다.
도메인 상으로 Movie 가 DiscountCondition 목록을 속성으로 포함하고 있다. 이미 결합되어 있기 때문에 Movie 가 DiscountCondition 과 협력하게 하면 설계 전체적으로 결합도를 추가하지 않고 협력을 완성할 수 있다.
Screening 이 DiscountCondition 과 협력한다면 Screening 이 영화 요금 계산과 관련된 책임 일부를 맡아야한다. 그래서, 영화 예매 요금을 계산하는 방식이 바뀔 경우, Screening 도 함께 변경해야한다.
반면, Movie 의 주된 책임은 영화 요금 계산이다. 그래서, 영화 요금 계산하는데 필요한 할인 조건을 판단하기 위해 DiscountCondition 과 협력하는 것은 응집도에 아무러 해를 끼치지 않는다.
창조자에게 객체 생성 책임을 할당하라
GRASP 의 창조자 패턴은, 객체를 생성할 책임을 어떤 객체에게 할당할지 지침을 제공한다.
창조자 패턴은 어떤 방식으로든, 생성되는 객체와 연결되거나 관련될 필요가 있는 객체에 해당 객체를 생성할 책임을 맡기는 것이다.
Reservation 을 잘 알고 있거나, 긴밀하게 사용하거나, 초기화에 필요한 데이터를 가지고 있는 객체는 무엇인가 ?
Screening 이다. Screening 을 Reservation 의 Creator 로 선택해라.
03 구현을 통한 검증
Screening 은 영화 예매할 책임을 맡고 있으며, Reservation 인스턴스를 생성할 책임을 수행해야한다.
1 | public class Screeing { |
Screeing을 구현하는 과정에서, Movie 에 전송하는 메세지의 시그니처는 메세지의 수신자인 Movie 가 아니라 송신자인 Screening 의 의도를 표한한다. 이처럼, Movie 의 구현을 고려하지 않고 필요한 메세지를 결정하면 Movie 의 내부 구현을 캡슐화할 수 있다.
DiscountCondition 개선하기
151 Page
변경에 취약한 클래스란, 코드를 수정해야하는 이유를 하나 이상 가지는 클래스다.
DiscountCondition 은 다음 서로 다른 이유로 변경될 수 있다.
- 새로운 할인 조건 추가
- 순번 조건을 판단하는 로직 변경
- 기간 조건을 판단하는 로직 변경
이를 해결 하기 위해, 변경의 이유에 따라 클래스를 분리해아한다.
일반적으로, 설계를 개선하는 작업은 변경의 이유가 하나 이상인 클래스를 찾는 것으로 시작하는 것이 좋다. 코드를 통해 변경 이유를 파악할 수 있는 방법은,
인스턴스 변수가 초기화 되는 시점을 살펴봐라. 함께 초기화 되는 속성을 기준으로 코드 분리 해야한다.
응집도 높은 클래스는 인터스턴 생성할 때 모든 속성을 함께 초기화한다.
DiscountCondition 클래스는 순번 조건을 표현하는 경우 sequence 는 초기화되지만, dayOfWeek, startTime, endTime 은 초기화되지 않는다.인스턴스 변수를 사용하는 방식을 봐라. 속성 그룹과 해당 그룹에 접근하는 메서드 그룹을 기준으로 코드 분리해야한다.
모든 메서드가 객체의 모든 속성을 사용하면 클래스의 응집도가 높다. isSatisfiedByPeriod 메서드는 dayOfWeek, startTime, endTime 은 사용하지만 sequence 는 사용하지 않는다.
타입 분리하기
SequnceCondition 과 PeriodCondition 으로 분리한다.
SequnceCondition 과 PeriodCondition 은 자신의 모든 인스턴스 변수를 함께 초기화할 수 있다. 그리고, 클래스에 있는 모든 메서드는 동일한 인스턴스 변수 그룹을 사용한다.
하지만 이 방법은 두가지 문제가 있다.
- Movie 클래스가 SequnceCondition 과 PeriodCondition 클래스 양쪽에 결합한다.
- 새로운 할인 조건 추가가 어렵다.
다형성을 통해 분리하기
Movie 입장에서 SequnceCondition 과 PeriodCondition 은 동일한 책임을 수행한다. 즉, 동일한 역할을 수행한다.
- 역할을 대체할 클래스들 사이에서구현을 공유하면, 추상클래스를 사용
- 구현 공유가 필요없고 역할을 대체하는 객체들의 책임만 정의하고 싶다면 인터페이스 사용
객체의 타입에 따라 변하는 행동이 있다면, 타입을 분리하고 변화하는 행동을 각 타입의 책임으로 할당해라. 이것이 다형성 패턴이다.
변경으로부터 보호하기
Movie 입장에서 DiscountCondition 의 타입이 캡슐화 된다는 것은 새로운 DiscountCondition 타입을 추가해도, Movie 가 영향을 받닌 않는다. 이처럼, 변경을 캡슐화 하도록 책임 할당하는 것을 GRASP 에서 변경 보호 패턴이라고 한다.
클래스를 변경에 따라 분리하고 인터페이스를 이용해 변경을 캡슐화하면, 결합도와 응집도를 향상시킨다.
Movie 클래스 개선하기
AmountDiscountMovie / PercentDiscountMovie / NoneDiscountMovie
변경와 유연성
영화에 설정된 할인 정책을 실행중에 변경해야하는 요구사항이 생기면, 상속을 이용하고 있기 때문에 새로운 인스턴스 생성후에 필요한 정보를 복사해야한다. 이것은 번거롭고 오류 발생 가능성이 높다.
해결방법은 합성을 이용하는 것이다. 할인 정책을 DiscountPolicy 로 분리하고 Movie 에 합성하도록 하면 유연한 설계가 된다.
도메인 모델은 코드에 대한 가이드를 제공해야하고, 코드의 변화에 맞춰 함께 변화해야한다.
04 책임 주도 설계의 대안
- 최대한 빠르게 목적한 기능을 수행하는 코드를 작성한다.
- 이해가기 쉽고 수정하기 쉬운 소프트웨어로 개선하기 위해 겉으로 보인느 동작은 바꾸지 않고 내부 구조를 변경한다.
책임 주도 설계에 익숙하지 않으면, 위와 같이 데이터 중심으로 구현하고 이를 리팩터링하는 것도 방법이다.
메서드 응집도
ReservationAgency 에 포함된 로직들을 적절한 객체의 책임으로 분배한다.
긴 메서드의 단점은,
- 코드를 전체적으로 이해하는데 너무 많은 시간 소요
- 변경이 필요할 때 수정할 부분 찾기 힘듦
- 메서드 내부 일부 로직을 수정하더라도 메서드 나머지 부분에서 버그 발생 가능
- 로직의 일부만 재사용 불가능
- 코드 중복 초래
짧고 이해하기 쉬운 이름으로 된 메서드는,
- 다른 메서드에서 사용될 확률 높음
- 일련의 주석을 읽는 것 같은 느낌
- 오버라이딩 쉬움
객체로 책임을 분배할 때 가장 먼저, 메서드를 응집도 높은 수준으로 분해하는 것이다. 메서드들의 응집도가 높아졌지만, ReservationAgency 의 응집도는 아직 낮다. 그래서, 변경의 이유가 다른 메서드들을 적절한 위치에 분배한다. 적절한 위치란, 각 메서드가 사용하는 데이터를 정의하고 있는 클래스다.
isDiscountable 메서드가 ReservationAgency 에 속할 때는 구현의 일부였지만, DiscountCondition 에 옮기고 나서는 퍼블릭 인터페이스의 일부가 된다.
오브젝트 <조영호>