결국엔 프로그래밍
[OBJECT] 오브젝트 Chapter 5 - 책임 할당하기 본문
5장
책임에 초점을 둔 설계의 가장 어려운 점은 어떤 객체에게 어떤 책임을 할당할지를 정하는 것이다.
GRASP 패턴은 이러한 어려움을 해결할 수 있게 도와준다.
01. 책임 주도 설계를 향해
데이터 중심의 설계에서 책임 중심의 설계로 전환하기 위해
- 데이터보다 행동을 먼저 결정하자
- 협력이라는 문맥 안에서 책임을 결정하자
데이터보다 행동을 먼저 결정하자
너무 이른 시기에 데이터에 초점을 맞추면 캡슐화가 악화되기 때문에 낮은 응집도와 높은 결합도를 가진 객체들이 생기게 되고 변경에 취약한 설계가 된다. 따라서 객체의 책임을 결정한 후에 객체의 상태를 결정하는 책임 중심의 설계를 해야 한다.
협력이라는 문맥 안에서 책임을 결정하자
책임은 객체의 입장이 아니라 객체가 참여하는 협력에 적합해야 한다.
협력을 시작하는 주체는 메시지 전송자이기 때문에 협력에 적합한 책임이란 메시지 수신자가 아니라 메시지 전송자에게 적합한 책임을 의미한다. 따라서 메시지를 결정한 후에 객체를 선택해야 한다.
메시지는 클라이언트의 의도를 표현하고 메시지를 수신하는 객체는 메시지를 처리할 책임을 할당받는다.
여기서 메시지를 먼저 결정하기 때문에 메시지 송신자는 수신에 대한 어떠한 가정도 할 수 없고 결과적으로 깔끔한 캡슐화가 이뤄지게 된다.
결국 올바른 객체지향 설계를 위해서는 클라이언트가 전송할 메시지를 결정한 후에 객체의 상태를 저장하는 내부 데이터에 대한 고민을 해야 한다.
02. GRASP 패턴
책임 할당 기법인 GRASP 패턴
객체에게 책임을 할당할 때 지침으로 삼을 수 있는 원칙들의 집합을 패턴 형식으로 정리한 것
도매인 개념
설계를 시작하기 전에 도메인에 대한 개략적인 모습을 그려보는 것이 유용하다.

이런 식으로 도메인 개념을 정리하면 책임을 할당받을 객체들 간의 관계가 정리된다는 장점이 있다.
그러나 배보다 배꼽이 크면 안 된다.
도메인 개념을 정리하는데 너무 많은 시간을 들이지 말고 빠르게 설계와 구현을 해야 한다.
애초에 완벽한 도메인 모델이라는 것은 없다. 구현에 도움이 되는 도메인 모델을 만들면 된다.
정보 전문가
애플리케이션의 기능 → 애플리케이션의 책임 → 애플리케이션에 대해 전송된 메시지 → 책임질 객체 선택의 과정으로 설계
가장 먼저 메시지를 보내는 객체의 의도를 반영해서 메시지를 결정한다.
그다음 이 메시지에 대한 책임을 질 적합한 객체를 선택해야 하는데, 여기서 '정보 전문가 패턴'이 등장한다.
정보 전문가 패턴
객체는 자율적인 존재여야 한다. 책임을 수행할 정보를 알고 있는 객체에 책임을 할당하는 것을 정보 전문가 패턴이라 한다.
정보 전문가 패턴을 따르면 정보와 행동을 최대한 가까운 곳에 위치시켜 캡슐화를 유지할 수 있다. 하지만 그렇다고 정보 전문가가 데이터를 반드시 저장하고 있을 필요는 없다.
높은 응집도와 낮은 결합도
결국 설계는 트레이드오프의 산물이다. 선택에 따라 다양한 관점의 설계가 가능하다. 이러한 이때 올바른 책임 할당을 위해서 LOW COUPLING패턴과 HIGH COHESION패턴을 알면 좋다. 책임을 할당할 수 있는 다양한 대안들이 존재한다면 응집도와 결합도 측면에서 더 나은 대안을 선택을 할 수 있기 때문이다.
LOW COUPLING 패턴
낮은 결합도 패턴

이 도메인 개념을 통해 봤을 때, 영화-할인 정책 그리고 상영-할인 정책 이 두 가지의 협력 방법 중 어떤 것이 '낮은 결합도' 측면에서 유리할까? 답은 영화-할인 정책이다. 이미 영화와 할인 정책은 결합되어 있기 때문에 결합도가 추가되지 않는다.
HIGH COHESION 패턴
높은 응집도 패턴

상영-할인 정책의 협력은 상영이 영화 요금 계산과 관련된 일부 책임을 떠안게 한다. 이때 만약 예매 요금 계산 방식이 바뀐다면 상영도 변경되어야 한다. 이는 상영의 입장에서 서로 다른 이유로 변경되는 책임을 짊어지게 되므로 응집도가 낮아진다.
반면에 영화-할인 정책의 협력에서 영화는 애초에 주된 목적이 영화요금 계산이기 때문에 같은 상황이 주어졌을 때 응집도에 영향을 미치지 않는다.
LOW COUPLING패턴과 HIGH COHESION패턴은 책임과 협력의 품질을 검토하는 중요한 기준이다. 이 두 가지 관점에서 설계를 한다면 단순하면서도 재사용 가능한 유연한 설계를 얻을 수 있다.
CREATOR 패턴
객체를 생성할 책임을 어떤 객체에게 할당할지에 대한 지침을 제공한다
- 객체를 포함하거나 참조
- 객체를 기록
- 객체를 긴밀하게 사용
- 개체를 초기화하는데 필요한 데이터를 가짐(객체에 대한 정보 전문가)
위와 같은 조건에 가장 부합하는 경우 객체 생성의 책임을 할당한다.
이 패턴의 의도는 어떤 방식으로든 생성되는 객체와 연결되거나 관련될 필요가 있는 객체에 해당 객체를 생성할 책임을 맡기는 것이다. 그리고 이 패턴은 이미 존재하는 객체 사이의 관계를 이용하기 때문에 낮은 결합도를 유지하는데 유리하다.

위 예시에서는 예매 정보를 생성해야 하고 이를 생성하는데 필요한 영화, 상영시간, 상영 순번 등에 대한 정보 전문가이며 , 예매 요금 계산에 필요한 Moive도 알고 있어 '상영'을 CREATOR로 설정하는 것이 적절하다.
03. 구현을 통한 검증
public class DiscountCondition {
private DiscountConditionType type;
private int sequence;
private DayOfWeek dayOfWeek;
private LocalTime startTime;
private LocalTime endTime;
public boolean isSatisfiedBy(Screening screening) {
if (type == DiscountConditionType.PERIOD) {
return isSatisfiedByPeriod(screening);
}
return isSatisfiedBySequence(screening);
}
private boolean isSatisfiedByPeriod(Screening screening) {
return dayOfWeek.equals(screening.getWhenScreened().getDayOfWeek()) &&
startTime.compareTo(screening.getWhenScreened().toLocalTime()) <= 0 &&
endTime.compareTo(screening.getWhenScreened().toLocalTime()) <= 0;
}
private boolean isSatisfiedBySequence(Screening screening) {
return sequence == screening.getSequence();
}
}
DiscountCondition 클래스가 변경에 취약한 이유
- 새로운 할인 조건 추가 시 isSatisfiedBy의 조건 구문을 수정해야 하고 새로운 데이터를 필요로 하는 경우 속성도 추가해야 한다.
- Period 조건 판단 로직 변경 시 구현 변경, 조건 판단에 필요한 데이터가 바뀔 경우 속성도 바뀌어야 한다.
- Sequence 조건 판단 로직 변경 시 구현 변경, 조건 판단에 필요한 데이터가 바뀔 경우 속성도 바뀌어야 한다.
이렇듯 다양한 변경의 이유를 가지기 때문에 응집도가 낮다. 따라서 변경의 이유에 따라 클래스를 분리해야 한다.
변경의 이유를 찾기가 어려울 땐 다음의 조건들을 확인한다.
- 응집도가 낮은 클래스는 객체의 속성 중 일부만 초기화하고 일부는 초기화되지 않은 상태로 남겨둔다.
→ 함께 초기화되는 속성을 기준으로 코드를 분리한다. - 응집도가 낮은 클래스는 모든 메서드가 모든 객체의 속성을 사용하지 않고 매서드들이 사용하는 속성에 따라 그룹이 나뉜다.
→ 속성 그룹, 그리고 해당 그룹에 접근하는 메서드 그룹을 기준으로 코드를 분리한다.
타입 분리
public class PeriodCondition {
private DayOfWeek dayOfWeek;
private LocalTime startTime;
private LocalTime endTime;
public PeriodCondition(DayOfWeek dayOfWeek, LocalTime startTime, LocalTime endTime) {
this.dayOfWeek = dayOfWeek;
this.startTime = startTime;
this.endTime = endTime;
}
public boolean isSatisfiedBy(Screening screening) {
return dayOfWeek.equals(screening.getWhenScreened().getDayOfWeek()) &&
startTime.compareTo(screening.getWhenScreened().toLocalTime()) <= 0 &&
endTime.compareTo(screening.getWhenScreened().toLocalTime()) >= 0;
}
}
public class SequenceCondition {
private int sequence;
public SequenceCondition(int sequence) {
this.sequence = sequence;
}
public boolean isSatisfiedBy(Screening screening) {
return sequence == screening.getSequence();
}
}
DiscountCondition의 가장 큰 문제점인 period, sequence의 두 가지 독립적인 타입을 갖고 있다는 점을 해결하기 위해 두 타입을 두 개의 클래스로 분리했다. 이에 따라 Movie는 이전과 달리 두 개의 클래스와 각각 협력해야 했다.
public class Movie{
private List<PeriodCondition> periodConditions;
private List<SequenceCondition> sequenceConditions;
private boolean isDiscountable(Screening screening) {
return checkPeriodConditions(screening) ||
checkSequenceConditions(screening);
}
private boolean checkPeriodConditions(Screening screening) {
return periodConditions.stream()
.anyMatch(condition -> condition.isSatisfiedBy(screening));
}
private boolean checkSequenceConditions(Screening screening) {
return sequenceConditions.stream()
.anyMatch(condition -> condition.isSatisfiedBy(screening));
}
}
이 결과 응집도는 높아졌지만 결합도도 높아진다는 문제가 발생했다.
다형성을 통해 분리

그런데 사실 Movie의 입장에서 Period Condition, Sequence Condition은 차이가 없다. 그냥 Discount Condition이라는 역할을 만들고 그 역할에 책임을 할당하면 해결되는 것이다.
public interface DiscountCondition {
boolean isSatisfiedBy(Screening screening);
}
public class SequenceCondition implements DiscountCondition { ... }
public class PeriodCondition implements DiscountCondition { ... }
추상 클래스와 인터페이스 중 인터페이스를 선택한 이유는 구현에 대한 공유는 필요 없고 단지 역할을 대체하는 객체들의 책임만을 정의하고 싶었기 때문이다.
이렇게 객체의 타입에 따라 변하는 행동이 있는 경우 타입을 분리하고 변화하는 행동을 각 타입의 책임으로 할당하는 것을 POLYMORPHISM (다형성) 패턴이라 한다.
변경 보호 패턴
위의 상황에서 만약 새로운 할인 조건이 추가된다 해도 Movie는 영향을 받지 않는다. 즉, Movie는 어떤 수정도 할 필요가 없다. 오직 DiscountCondition 인터페이스를 실체화하는 클래스만 추가하면 된다. 이렇게 변경을 캡슐화하도록 책임을 할당하는 것을 GRASP에서 PROTECTED VARIATIONS (변경 보호) 패턴이라 한다.

Movie 클래스까지 다형성, 변경 보호 패턴을 적용해 수정한 결과 위와 같은 구조를 얻을 수 있다.

위의 도메인 모델과 코드의 구조가 유사하다는 것을 알 수 있다. 즉, 도메인 모델은 설계를 넘어 코드의 구조까지 영향을 미친다.
변경과 유연성
- 코드를 이해하고 수정하기 쉽도록 최대한 단순하게 설계한다
- 코드를 수정하지 않고도 변경을 수용할 수 있도록 코드를 더 유연하게 만든다.
변경에 대한 대비를 위해 둘 중 어떤 방법이 더 좋을까, 유사한 반복이 발생되는 경우 복잡하더라도 후자가 더 좋다.

위의 경우 새로운 할인 정책이 추가될 때마다 인스턴스를 생성하고 상태를 복사하는 등의 과정을 거친다. 이럴 땐 변경을 쉽게 수용할 수 있도록 코드를 유연하게 만드는 것이 더 좋은 방법이다.

상속 대신 합성을 사용하는 것이 해결방법이다. Movie에서 DiscountPolicy를 분리하고 Movie에 합성시켜 유연한 설계를 완성할 수 있다. 이렇게 되면 새로운 할인 정책을 추가하더라도 Movie에 연결된 DiscountPolicy의 인스턴스만 교체하는 단순한 작업으로 해결이 가능하다.
04. 책임 주도 설계의 대안
책임 주도 설계는 쉽게 익숙해질 수 있는 것이 아니다. 책임 주도 설계에 어려움을 느낀다면 빠르게 목적을 수행하는 기능을 가진 코드를 작성하고 리팩터링을 통해 책임들을 올바른 위치로 이동시키면 된다.
메서드 응집도
긴 메서드는 응집도가 낮기 때문에 이해하기 어렵고 재사용도 어렵고 변경도 어렵다. 이러한 메서드를 몬스터 메서드라고 한다.
이런 몬스터 메서드는 주석을 많이 활용하게 되는데 주석을 쓰는 대신 메서드를 작게 분해해서 응집도를 높이는 게 더 좋은 방법이다.
메서드를 작게 분해하면 다음과 같은 이점을 갖는다.
- 어떤 일을 하는지 한눈에 알아볼 수 있다.
- 전체적인 흐름을 이해하기 쉽다.
- 변경하기 쉽다.
객체의 자율화
메서드의 응집도를 높임과 동시에 클래스의 응집도를 높이기 위해선 메서드를 적절한 위치로 이동시켜야 한다.
그러기 위해선 자신이 소유하는 데이터를 자기 스스로 처리하도록해야한다.
데이터를 사용하는 메서드를, 데이터를 가진 클래스로 이동시키면 캡슐화와 높은 응집도, 낮은 결합도를 갖는 설계를 얻게 된다.
메서드의 응집도를 높이고, 객체에 자율성을 부여하는 방식으로 리팩터링을 하고 나면 책임 주도 설계의 모습과 유사한 결과를 얻을 수 있다. 여기에 다형성과 변경 보호 패턴을 적용하면 최종 설계와 유사한 코드를 얻을 수 있다.
결과적으로 책임 주도 설계에 익숙지 않다면 데이터 중심으로 구현 후 이를 리팩터링 하여 책임 주도 설계와 유사한 코드를 얻을 수 있다.
5장 끝
'객체지향프로그래밍 > [책] Object 오브젝트' 카테고리의 다른 글
| [OBJECT] 오브젝트 Chapter 4 - 설계 품질과 트레이드오프 (0) | 2022.01.16 |
|---|---|
| [OBJECT] 오브젝트 Chapter 3 - 역할, 책임, 협력 (0) | 2022.01.09 |
| [OBJECT] 오브젝트 Chapter 2 - 객체지향 프로그래밍 (part2) (0) | 2022.01.07 |
| [OBJECT] 오브젝트 Chapter 2 - 객체지향 프로그래밍 (0) | 2022.01.03 |
| [OBJECT] 오브젝트 Chapter 1 - 객체, 설계 (0) | 2022.01.02 |