본문 바로가기

BackEnd

오브젝트 5장. 책임 할당하기

반응형

4장 요약

데이터 중심 설계는 고립된 객체 상태에 초점을 맞추어 협력 문맥을 무시하기 쉽다.

따라서 캡슐화 위반, 결합도 증가, 유연성 감소를 가져온다.

데이터가 아닌 책임에 초점을 맞춰야 한다.

책임 주도 설계를 향해

  • 행동(메세지)을 먼저 결정하라.
    • 클라이언트 관점에선 행동이 곧 책임이다.
  • 협력이라는 문맥 안에서 책임을 결정하라
    • 책임은 객체가 참여하는 협력에 적합해야 한다.

메시지를 결정한 후 객체를 선택해야 한다.

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

메시지가 객체를 선택하게 해야 한다.

메세지 기반 설계

메세지는 클라이언트의 의도를 표현한다.

메세지를 먼저 표현하면, 수신자에 대한 어떠한 가정에서도 자유로워진다. (깔끔한 캡슐화)

책임 주도 설계

시스템이 사용자에게 제공해야 하는 기능인 시스템 책임을 파악한다

시스템 책임을 더 작은 책임으로 분할한다

분할된 책임을 수행할 수 있는 적절한 객체 또는 역할을 찾아 책임을 할당한다

객체가 책임을 수행하는 도중 다른 객체의 도움이 필요한 경우 이를 책임질 적절한 객체 또는 역할을 찾는다

해당 객체 또는 역할에게 책임을 할당함으로써 두 객체가 협력하게 된다

GRASP 패턴 by 크레이그 라만

General Responsibility Assign Software Pattern (객체에게 책임을 할당할 때 지침으로 삼을 수 있는 원칙들)

도메인 개념에서 출발하기

책힘 할당 시 가장 먼저 고민해야 하는 유력한 후보는 도메인 개념이다.

  • 도메인 : 소프트웨어가 해결하고자 하는 문제
    • 애그리거트, 엔터티

하나의 영화는 여러번 상영될 수 있으며, 하나의 상영은 여러번 예약될 수 있다는 것을 나타냄. 할인정책과 할인조건은 각각 2개이며, 할인조건은 여러개일 수 있지만 할인정책은 하나만 가능하다

설계 시작 단계에서는 개념들의 의미나 관계가 완벽할 필요가 없다. 우리에게는 출발점이 필요할 뿐이다.

도메인 개념과 관계는 구현의 기반이 된다.

정보 전문가에게 책임을 할당 : 설계의 시작

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

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

  1. 메시지는 메시지를 전송할 객체의 의도를 반영해서 결정한다.
    1. 메세지를 전송할 객체는 무엇을 원하는가

협력을 시작하는 객체는 미정이지만 이 객체가 원하는 것은 분명하다.

2. 메세지에 적합한 객체를 선택한다.

  1. 메세지를 수신할 적합한 객체는 누구인가?

INFORMATION EXPERT 패턴은 객체가 자율적인 존재여야 한다는 것을 상기시킨다.

정보를 알고있는 객체만이 책임을 어떻게 수행할지 스스로 결정할 수 있다.
정보와 행동이 가까운데 뭉쳐 캡슐화가 유지되며, 정보별로 책임이 분산되어 응집도가 높아지고 결합도가 낮아진다.
또한 가독성이 높아진다.

알고있다는 것은 꼭 필드(저장된 데이터)일 필요는 없다. 정보를 제공할 수 있는 다른 객체를 알거나 계산해서 제공할 수 있다.

상영은 상영순번, 상영시간, 영화 정보 등의 가장 많은 정보를 알고 있다.
상영은 영화 가격을 모르기 떄문에(캡슐화되어 있음) 계산을 요청한다
가격은 영화가 가장 잘 알고 있다.
영화는 할인 여부 판단을 못하므로 할인여부 판단을 요청한다.
할인여부는 할인 조건이 가장 잘 알고 있고 스스로 판단할 수 있으므로 메세지를 전송하지 않는다.

영화는 할인정책을 알고 있으므로 할인 정책에 따라 요금을 계산해서 반환한다.

높은 응집도와 낮은 결합도 : 설계 결정 평가

설계는 트레이드오프 활동이다.

몇가지 설계 중 하나를 선택해야 하는 경우 INFORMATION EXPERT 패턴 이외의 다른 책임 할당 패턴들을 함께 고려할 필요가 있다.

반대로 상영이 직접 할인조건이랑 협력한다면?

LOW COPLING 패턴

설계의 전체적인 결합도가 낮게 유지되도록 책임을 할당하라.

상영이 2개와 협력하는 두번째 구조는 결합도가 높아져 좋지 앉은 설계이다.

HIGH COHESION 패턴

높은 응집도를 유지할 수 있게 책임을 할당하라.

두번째 구조는 Screen에게 영화 요금 계산 책임이 분산된다. 즉 Screen의 책임이 늘어난다.

이는 같이 변경될 여지도 증가한다.

첫번째 구조에서 Movie의 책임은 가격 계산이기 때문에, 할인조건과의 협력은 응집도 높은 설계이다.

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

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

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

연관을 필요로 하는 객체에 생성 책임을 맞긴다. 필요로 한다면 어떻게든 결합해야 하기 때문이다.

이미 결합되어있는 객체가 생성하는 것은 결합도가 변하지 않는다.

Screening은 Reservation을 생성하는데 필요한 가장 많은 정보를 알고 있다. (정보 전문가)

DiscountCondition 개선하기

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();
    }
}

변경에 취약한 클래스란 코드를 수정해야 하는 이유를 하나 이상 가지는 클래스다.

  • 새로운 할인 조건 추가
    • issatisfiedBy 메서드 안 if~else 구문 추가 
    • DiscountCondition에 속성 추가
  • 순번 조건 판단 로직 변경
    • isSatisfiedBySequence 내부 구현 수정
    • DiscountCondition의 sequence 속성 변경
  • 기간 조건 판단 로직 변경
    • isSatisfiedByPeriod 메서드 내부 구현 수정
    • dayOfWeek, startTime, endTime 변경

즉, 위 클래스는 하나 이상의 변경 이유를 가지기 때문에 응집도가 낮다.

변경의 이유에 따라 클래스를 분리해야 한다. 

서로 다른 시점, 서로 다른 이유로 변경되는 코드들을 분리한다.

코드를 통해 변경의 이유 파악하기.

  • 인스턴스 변수가 초기화되는 시점.
    • 응집도가 높은 클래스는 모든 속성을 함께 초기화한다.
    • 일부가 null로 남겨지는 클래스나, 나중에 set되는 클래스는 의심해본다.

위의 사례에서는 sequence, dayOfWeek, startTime, endTime

> 함께 초기화되는 속성을 기준으로 코드를 분리한다.

  • 메서드들이 인스턴스 변수를 사용하는 방식
    • 메소드가 모든 속성을 사용한다면 응집도가 높다
    • 메서드들이 사용하는 속성에 따라 그룹이 나뉜다면 클래스의 응집도가 낮다

위 사례에서는 isSatisfiedBySequence, isSatisfiedByPeriod가 된다.

속성 그룹과 해당 그룹에 접근하는 메서드 그룹을 기준으로 코드를 분리한다.

클래스 응집도 판단하기

타입 분리하기

순번 조건과 기간 조건이라는 두개의 독립적인 타입이 하나의 클래스 안에 공존하고 있다. 두 개의 클래스로 분리한다.

sequence 속성만 사용하는 메서드 => SequenceCondition 이동

dayOfWeek, startTime, endTime => PeriodCondition 이동

모든 메서드가 동일한 인스턴스 변수 그룹을 사용하도록 하여 응집도를 높인다.

 

이제 Movie는 두 클래스와 협력한다.

해결방법 1. 목록을 따로 유지한다.

  • 아는 클래스가 2개로 증가하여 결합도가 높아짐.
  • 목록 별로 넣는 로직이 필요해 구현이 귀찮아짐.

해결방법 2. 다형성을 통해 분리

Movie 입장에서 둘은 할인 여부를 판단하는 동일한 책임을 수행할 뿐이다.

즉 동일 책임을 수행하는 동일한 역할을 한다.

역할을 기반으로 유연해진 협력

객체의 타입에 따라 변하는 행동이 있다면 (if) 타입을 분리하고 변화하는 행동을 각 타입의 책임으로 할당하라.

객체의 암시적인 타입에 따라 행동을 분기해야 한다면 암시적인 타입을 명시적 클래스로 정의하고 행동을 나눔으로써 응집도 문제를 해결할 수 있다.

POLYMORPHISM 패턴 : 타입별 분리

객체의 타입에 따라 변하는 로직이 있을 때, 변하는 로직을 담당할 책임을 어떻게 할당해야 하는가?

타입을 명시적으로 정의하고 각 타입에 다형적으로 행동하는 책임을 할당하라.

if~else 또는 switch-case등의 조건 논리를 사용해서 설계한다면 새로운 변화가 일어난 경우 조건 논리를 수정해야 한다.

이것은 프로그램을 수정하기 어렵고 변경에 취약하게 만든다.

객체의 타입을 검사해서 타입에 따라 여러 대안들을 수행하는 조건적인 논리를 사용하지마라.

PROTECTED VARIATIONS 패턴 : 변경되는 필드 별 분리

이제 서로 다른 이유로 변경되는 클래스들이 각자의 변경을 캡슐화한다.

Movie와  3개의 클래스 간 협력에서, 변경이 필요한 클래스는 단 하나다.

설계에서 변하는 것이 무엇인지 고려하고 변하는 개념을 캡슐화하라.

우리가 캡슐화해야 하는 것은 변경이다.

변경이 될 가능성이 높은가? 그렇다면 캡슐화하라.

클래스를 변경에 따라 분리하고 인터페이스를 이용해 변경을 캡슐화하는 것은 설계의 결합도와 응집도를 향상시키는 매우 강력한 방식이다.

중간 정리

하나의 클래스가 여러 타입의 행동을 구현한다면, 클래스를 분리하고, POLYMORPHISM 패턴에 따라 책임을 분산시켜라.

예측 가능한 변경으로 여러 클래스들이 불안정해진다면, PROTECTED VARIATION 패턴에 따라 안정적인 인터페이스 뒤로 변경을 캡슐화하라.

Movie 클래스 개선하기

public class Movie {
    private String title;
    private Duration runningTime;
    private Money fee;
    private List<DiscountCondition> discountConditions;

    private MovieType movieType;
    private Money discountAmount;
    private double discountPercent;

    public Money calculateMovieFee(Screening screening) {
        if (isDiscountable(screening)) {
            return fee.minus(calculateDiscountAmount());
        }

        return fee;
    }

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

    private Money calculateDiscountAmount() {
        switch(movieType) {
            case AMOUNT_DISCOUNT:
                return calculateAmountDiscountAmount();
            case PERCENT_DISCOUNT:
                return calculatePercentDiscountAmount();
            case NONE_DISCOUNT:
                return calculateNoneDiscountAmount();
        }

        throw new IllegalStateException();
    }

    private Money calculateAmountDiscountAmount() {
        return discountAmount;
    }

    private Money calculatePercentDiscountAmount() {
        return fee.times(discountPercent);
    }

    private Money calculateNoneDiscountAmount() {
        return Money.ZERO;
    }
}

calculateDiscountAmout함수를 보니 switch가 보인다.

이는 두가지 할인 정책을 하나의 클래스 안에서 구현하고 있기 때문이다.

POLYMOPHISM 패턴으로 타입을 분리한다.

PROTECTED VARIATIONS 패턴을 이용해 Movie내의 속성들을 분리한다.

데이터가 아닌 책임 중심으로 설계하라. 상태가 아니라 행동이다.

상태가 아니라 행동. 책임과 협력

코드의 구조를 이끌어내는 도메인을 설계하라

변경과 유연성

개발자가 변경에 대비하는 방법

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

대부분 전자가 좋지만 유사한 변경이 반복적으로 발생한다면 후자를 택한다.

영화의 할인 정책을 실행 중에 변경해야 한다면...

유연성은 의존성 관리의 문제다.

요소들 사이의 의존성의 정도가 유연성의 정도를 결정한다.

유연성의 정도에 따라 결합도를 조절할 수 있는 능력은 객체지향 개발자가 갖춰야 하는 중요한 기술 중 하나다.

도메인 모델은 코드에 대한 가이드를 제공하며, 코드의 변화에 발맞춰 함께 변화해야 한다.

책임 할당은 원래 어려운 것이다.

책임 주도 설계의 대안

책임 주도 설계에 익숙해지는데는 충분한 경험과 학습이 필요하지만 숙련자에게도 어렵다.

빠르게 개발하고 리팩터링하는 방법을 사용해보자.

하지만 코드를 수정한 후에 겉으로 드러나는 동작은 그대로 유지해야 함을 잊지 말자.

리팩터링 : 이해하기 쉽고 수정하기 쉬운 코드로 개선하면서 겉으로 보이는 동작은 빠꾸지 않은 채 내부 구조를 변경하는것

메서드 응집도

나는 다음과 같은 이유로 짧고 이해하기 쉬운 이름으로 된 메서드를 좋아한다. 첫째, 메서드가 잘게 나뉘어 있을 때 다른 메서드에서 사용될 확률이 높아진다. 둘쨰, 고수준의 메서드를 볼때 주석을 읽는 느낌을 준다. 셋째, 메서드가 짧으면 오버라이딩도 쉽다. 작은 메서드는 이름을 잘 지어야 의미가 잘 드러난다. 중요한 것은 메서드의 이름과 몸체적 의미의 차이다. 코드가 더 명확해진다면 메서드 이름이 더 길어져도 뽑아낸다.

1. 긴 메서드를 작고 응집도 높은 메서드로 분리한다.

2. 각 메서드가 사용하는 데이터를 정의하고 있는 클래스로 옮긴다.

from

getter와 setter를 주시하면 보인다.

자신의 데이터를 캡슐화하고 이용할 수 있는 메서드를 외부로 제공한다.

to

그 다음 Movie의 책임을 계산으로 정의하고, 해당 메소드를 통째로 옮겨보자.

최종 정리

응집도 :

  • 한 객체 내의 필드를 모든 메소드에서 다 사용하나? 아닌 메소드가 있으면 분리를 생각해보자.
  • 객체 내 초기화되는 시점이 다른 변수가 있다면 분리를 생각해보자.
  • 하나 이상의 이유로 변경된다면 분리를 생각한다. (switch-if, 다른 객체의 필드 노출)

결합도 : 최대한 적은 객체들과 협력하면 좋다.

캡슐화 : 내가 갖고 있는 필드를 안과 밖에서 숨겨라. (파라미터와 getter, setter)

 

책임 주도 설계 방법에 익숙하지 않다면, 데이터 중심으로 구현한 후 리팩터링을 해보자.

(응집도, 결합도, 캡슐화를 상상 생각해야 한다.)

(도메인 객체 설계 => 구현, GRASP 패턴.)

 

반응형