• oop
  • design
  • java
  • book

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

책임 중심 설계와 GRASP의 CREATOR 패턴, 그리고 코드에서 변경의 이유를 찾아 클래스를 분리하는 방법을 정리한다.

시리즈 · BackEnd 서적12 / 17
  1. SQL 레벨업 Ch.1 — DBMS 아키텍처
  2. SQL 레벨업 Ch.3 — SQL의 조건 분기
  3. SQL 레벨업 Ch.4~5 — 집약과 자르기, 반복문
  4. SQL 레벨업 Ch.6 — 결합
  5. SQL 레벨업 Ch.7 — 서브쿼리
  6. SQL 레벨업 Ch.8 — SQL의 순서
  7. SQL 레벨업 Ch.9 — 갱신과 데이터 모델
  8. SQL 레벨업 Ch.10 — 인덱스 사용
  9. 오브젝트 Ch.1 — 객체, 설계
  10. 오브젝트 Ch.2 — 객체지향 프로그래밍
  11. 오브젝트 Ch.3~4 — 역할, 책임, 협력 / 설계 품질과 트레이드오프
  12. 오브젝트 Ch.5 — 책임 할당하기
  13. 오브젝트 Ch.6 — 메시지와 인터페이스
  14. 오브젝트 Ch.8 — 의존성 관리하기
  15. 오브젝트 Ch.9 — 유연한 설계
  16. 오브젝트 Ch.10, 12 — 상속과 코드 재사용, 다형성
  17. 오브젝트 Ch.13 — 서브클래싱과 서브타이핑

내가 생각하는 5장의 핵심은 코드를 통해 변경의 이유를 파악하는 방법이다.

객체지향 설계의 핵심은 책임에 초점을 맞추는 것이지만, 어떤 책임을 어디에 할당할지 결정하기는 쉽지 않다. 책임 할당은 트레이드오프 활동이고, 최선의 방법은 상황과 문맥에 따라 달라진다. 그래서 다양한 관점에서 설계를 평가할 수 있어야 한다.

데이터 중심 설계에서 책임 중심 설계로

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

질문의 순서가 달라진다.

  • 데이터 중심: 이 객체에 필요한 데이터는 무엇인가 → 그 데이터를 처리하는 데 필요한 오퍼레이션은 무엇인가
  • 책임 중심: 이 객체가 수행해야 하는 책임은 무엇인가 → 그 책임을 수행하는 데 필요한 데이터는 무엇인가

책임의 품질은 협력에 적합한 정도로 결정된다. 협력을 시작하는 주체는 메시지 전송자이므로, 협력에 적합한 책임이란 수신자가 아닌 전송자에게 적합한 책임을 뜻한다. 메시지를 먼저 결정하기 때문에 전송자는 수신자에 대해 어떤 가정도 할 수 없고, 그만큼 수신자는 캡슐화된다.

GRASP: CREATOR 패턴

GRASP(General Responsibility Assignment Software Pattern)는 책임 할당을 위한 패턴 모음이다. 그중 CREATOR 패턴은 객체 생성 책임을 누구에게 줄지에 대한 지침이다. 다음 조건을 만족하는 B에게 A의 생성 책임을 할당한다.

  • B가 A 객체를 포함하거나 참조한다.
  • B가 A 객체를 기록한다.
  • B가 A 객체를 긴밀하게 사용한다.
  • B가 A 객체를 초기화하는 데 필요한 데이터를 가지고 있다.

이미 결합돼 있는 객체에게 생성 책임을 맡기는 것이므로 설계 전체의 결합도에 영향을 주지 않는다.

코드에서 변경의 이유를 찾는 방법

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

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

순번 조건일 때는 sequence만 초기화되고 dayOfWeek, startTime, endTime은 초기화되지 않는다. 기간 조건일 때는 그 반대다. 클래스의 속성이 서로 다른 시점에 초기화되거나 일부만 초기화된다는 것은 응집도가 낮다는 증거다.

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

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

모든 메서드가 객체의 모든 속성을 사용한다면 응집도가 높다. 반대로 메서드들이 사용하는 속성에 따라 그룹이 나뉜다면 응집도가 낮다. isSatisfiedBySequence와 isSatisfiedByPeriod가 정확히 이 경우다.

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

다형성을 통해 분리하기

분리된 SequenceCondition과 PeriodCondition은 둘 다 할인 여부를 판단하는 동일한 책임을 수행한다. 여기에 역할 개념을 적용하면 사용하는 쪽이 구체 클래스를 모른 채 역할에만 결합되도록 의존성을 제한할 수 있다.

public interface DiscountCondition {
    boolean isSatisfiedBy(Screening screening);
}

public class PeriodCondition implements DiscountCondition { /* ... */ }

public class SequenceCondition implements DiscountCondition { /* ... */ }

이제 DiscountCondition을 이용하는 객체는 협력 대상의 구체적인 타입을 몰라도 된다. 상대가 DiscountCondition 역할을 수행할 수 있고 isSatisfiedBy 메시지를 이해한다는 사실만 알면 충분하다.