[오브젝트] Ch5 책임 할당하기

Hanjmo·2024년 1월 4일

오브젝트

목록 보기
5/5
post-thumbnail

이번 장에서 사용할 예제인 영화 예매 시스템의 도메인 모델의 구조는 다음과 같다.

책임 중심 설계로 전환하기 위한 두 가지 원칙

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

객체의 데이터에서 행동으로 중점을 옮기기 위해서는 객체를 설계하기 위한 질문의 순서를 바꿔야 한다.

  • 데이터 중심의 설계에서 하는 질문의 순서
    "이 객체가 포함해야 하는 데이터가 무엇인가?" -> "데이터를 처리하는 데 필요한 오퍼레이션은 무엇인가?"
  • 책임 중심의 설계에서 하는 질문의 순서
    "이 객체가 수행해야 하는 책임은 무엇인가?" -> "책임을 수행하는 데 필요한 데이터는 무엇인가?"

이 두 가지 질문의 순서를 보면, 책임 중심 설계로 전환하기 위해서는 객체의 행동을 먼저 결정한 후에 데이터를 결정해야 한다는 사실을 알 수 있다.

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

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

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

이러한 사실은 위에서 살펴본 첫 번째 원칙을 따르면 자연스럽게 따라오는 것 같다.
예매하라 라는 메시지, 즉 행동을 먼저 결정하고나서 메시지를 수신할 객체와 그 객체가 포함할 데이터를 결정하면 자연스럽게 협력에 적합한 책임을 할당할 수 있기 때문이다.

객체를 가지고 있기 때문에 메시지를 보내는 것이 아니다.
메시지를 전송하기 때문에 객체를 갖게된 것이다.

GRASP 패턴

GRASP 패턴이란 크레이그 라만(Craig Larman)이 패턴 형식으로 제안한 책임 할당 기법이다.

GRASP는 “General Responsibility Assignment Software Pattern(일반적인 책임 할당을 위한 소프트웨어 패턴)”의 약자로 객체에게 책임을 할당할 때 지침으로 삼을 수 있는 원칙들의 집합을 패턴 형식으로 정리한 것이다.

총 9가지 원칙이 있지만 이번 장에서는 절반정도만 설명한다.

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

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

영화 예매 시스템의 책임은 당연히 영화를 예매하는 것이다. 애플리케이션은 어떤 메시지를 전송해야 할까?

메시지는 메시지를 수신할 객체가 아니라 전송할 객체의 의도를 반영해서 결정해야 한다. 따라서 애플리케이션이 전송할 메시지를 결정하기 위해 다음과 같은 질문을 던져보자.

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

메시지를 전송하는 애플리케이션이 원하는 것은 영화를 예매하는 것이므로, 메시지의 이름으로 예매하라가 적절한 것 같다.

메시지를 결정했으면, 메시지를 수신하기에 적합한 객체를 선택해야 한다. 이제는 다음과 같은 질문을 던져보자.

“메시지를 수신할 객체는 누구인가?”

메시지를 수신할 객체는 자신의 상태를 스스로 처리하는 자율적인 존재여야 한다. 자율적인 객체가 되기 위한 중요한 조건은 책임과 그 책임을 수행하는 데 필요한 상태가 동일한 객체 안에 있어야 한다는 것이다.

따라서 책임을 수행할 정보를 가장 잘 알고 있는 객체에게 책임을 할당해야 한다. GRASP에서는 이를 INFORMATION EXPERT 패턴이라고 부른다.

책임을 객체에게 할당하는 일반적인 원리는 책임을 수행하는 데 필요한 정보를 가지고 있는 객체, 즉 정보 전문가에게 할당하는 것임을 기억해두자.

그렇다면 예매하라 메시지를 수신할 객체는 누구일까?

영화는 할인 조건과 같이 영화 요금을 계산하는 데 필요한 정보만 알고 있으므로, 적절하지 않다.

하지만 상영은 영화에 대한 정보와 상영 시간, 상영 순번처럼 영화 예매에 필요한 다양한 정보를 알고 있다. 따라서 영화 예매를 위한 정보 전문가는 상영(Screening)이라고 할 수 있다.

public class Screening {

    private Movie movie;
    private int sequence;
    private LocalDateTime whenScreened;
    
    ...
}

이처럼 INFORMATION EXPERT 패턴을 따르는 것만으로도 자율성이 높은 객체들로 구성된 협력 공동체를 구축할 수 있다.

높은 응집도와 낮은 결합도

설계는 트레이드오프 활동이다. 동일한 기능을 구현할 수 있는 무수히 많은 설계가 존재하는데, 우리는 오직 한 가지를 선택해야 한다.

여기서 우리는 높은 응집도와 낮은 결합도를 얻을 수 있는 설계를 선택한다. GRASP에서는 이를 HIGH COHESION(높은 응집도) 패턴과 LOW COUPLING(낮은 결합도) 패턴이라고 부른다.

HIGH COHESION(높은 응집도) 패턴
복잡성을 관리할 수 있는 수준으로 유지하기 위해서는 높은 응집도를 유지할 수 있게 책임을 할당해야 한다.

LOW COUPLING(낮은 결합도) 패턴
의존성을 낮추고 변화의 영향을 줄이며 재사용성을 증가시키기 위해서는 설계의 전체적인 결합도가 낮게 유지되도록 책임을 할당해야 한다.

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

CREATOR(창조자) 패턴은 객체를 생성할 책임을 어떤 객체에게 할당할지에 대한 지침을 제공한다.
어떤 방식으로든 생성되는 객체와 연결되거나 관련될 필요가 있는 객체에게 해당 객체를 생성할 책임을 할당한다.

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

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

다형성을 통해 분리하기

객체의 타입에 따라 변하는 행동이 있다면 타입을 분리하고 변화하는 행동을 각 타입의 책임으로 할당함으로써 응집도 문제를 해결할 수 있다.
GRASP에서는 이를 POLYMORPHISM(다형성) 패턴이라고 부른다.

Movie의 입장에서 할인 여부를 판단하기 위해 어떤 방법을 사용할지는 중요하지 않다.

인터페이스를 사용하면 Movie가 구체적인 클래스는 알지 못한 채 오직 역할에 대해서만 결합되도록 의존성을 제한할 수 있다.

public interface DiscountCondition {

    boolean isSatisfiedBy(Screening screening);
}
public class Movie {
	
    ...

	private boolean isDiscountable(Screening screening) {
        return discountConditions.stream()
                .anyMatch(condition -> condition.isSatisfiedBy(screening));
    }

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

변경으로부터 보호하기

DiscountCondition이라는 추상화가 구체적인 타입을 캡슐화함으로써, 새로운 DiscountCondition 타입을 추가하더라도 Movie에 대한 어떤 수정도 필요 없다.

오직 DiscountCondition 인터페이스를 실체화하는 클래스를 추가하는 것만으로 할인 조건의 종류를 확장할 수 있다.

이처럼 변경을 캡슐화하도록 책임을 할당하는 것을 GRASP에서는 PROTECTED VARIATIONS(변경 보호) 패턴이라고 한다.

클래스 안에 숨겨진 변경의 이유를 알아내는 방법

변경의 이유가 하나 이상인 클래스에는 위험 징후를 또렷하게 드러내는 몇 가지 패턴이 존재한다.

  1. 인스턴스 변수가 초기화되는 시점을 살펴보자.

    응집도가 높은 클래스는 인스턴스를 생성할 때 모든 속성을 함께 초기화한다.
    반면 응집도가 낮은 클래스는 객체의 속성 중 일부만 초기화하고 일부는 초기화되지 않은 상태로 남겨진다.

    e.g. DiscountCondition이 순번 조건을 표현하는 경우 sequence는 초기화되지만 dayOfWeek, startTime, endTime은 초기화되지 않는다.

    클래스의 속성이 서로 다른 시점에 초기화되거나 일부만 초기화된다는 것은 응집도가 낮다는 증거다.
    따라서 함께 초기화되는 속성을 기준으로 코드를 분리해야 한다.

  2. 메서드들이 인스턴스 변수를 사용하는 방식을 살펴보자.

    모든 메서드가 객체의 모든 속성을 사용한다면 클래스의 응집도가 높다고 볼 수 있다.
    반면 메서드들이 사용하는 속성에 따라 그룹이 나뉜다면 클래스의 응집도가 낮다고 볼 수 있다.

    e.g. DiscountCondition의 isSatisfiedBySequence 메서드는 sequence는 사용하지만 dayOfWeek, startTime, endTime은 사용하지 않는다.

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

0개의 댓글