[Object] 5장

5. 책임 할당하기

책임에 초점을 맞춰서 설계할 때 직면하는 가장 큰 어려움은 어떤 객체에게 어떤 책임을 할당할지를 결정하기가 쉽지 않다는 것이다. 책임 할당 과정은 일종의 트레이드오프 활동이다.

책임 주도 설계를 향해

데이터 중심의 설계에서 책임 중심의 설계로 전환하기 위해서는 다음의 두 가지 원칙을 따라야 한다.

  • 데이터보다 행동을 먼저 결정하라
  • 협력이라는 문맥 안에서 책임을 결정하라

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

객체의 데이터에서 행동으로 무게 중심을 옮겨야 한다. 가장 기본적인 해결 방법은 객체를 설계하기 위한 질문의 순서를 바꾸는 것이다. 책임 중심의 설계에서는 "이 객체가 수행해야 하는 책임은 무엇인가"를 결정한 후에 "이 책임을 수행하는 데 필요한 데이터는 무엇인가"를 결정한다.

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

책임은 객체의 입장이 아니라 객체가 참여하는 협력에 적합해야 한다. 협력에 적합한 책임이란 메시지 수신자가 아니라 메시지 전송자에게 적합한 책임을 의미한다. 협력에 적합한 책임을 수확하기 위해서는 객체를 결정한 후에 메시지를 선택하는 것이 아니라 메시지를 결정한 후에 객체를 선택해야 한다.

메시지가 클라이언트의 의도를 표현한다는 사실에 주목하라. 클라이언트는 단지 임의의 객체가 메시지를 수신할 것이라는 사실을 믿고 자신의 의도를 표현한 메시지를 전송할 뿐이다. 메시지를 수신하기로 결정된 객체는 메시지를 처리할 책임을 할당받게 된다.

책임 주도 설계

책임 주도 설계의 흐름은 다음과 같다.

  • 시스템이 사용자에게 제공해야 하는 기능인 시스템 책임을 파악한다.
  • 시스템 책임을 더 작은 책임으로 분할한다.
  • 분할된 책임을 수행할 수 있는 적절한 객체 또는 역할을 찾아 책임을 할당한다.
  • 객체가 책임을 수행하는 도중 다른 객체의 도움이 필요한 경우 이를 책임질 적절한 객체 또는 역할을 찾는다.
  • 해당 객체 또는 역할에게 책임을 할당함으로써 두 객체가 협력하게 한다.

책임 할당을 위한 GRASP 패턴

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

도메인 개념에서 출발하기

설계를 시작하기 전에 도메인에 대한 개략적인 모습을 그려 보는 것이 유용하다. 어떤 책임을 할당해야 할 때 가장 먼저 고민해야 하는 유력한 후보는 바로 도메인 개념이다. 설계를 시작하는 단계에서는 개념들의 의미와 관계가 정확하거나 완벽할 필요가 없다.

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

책임 주도 설계 방식의 첫 단계는 애플리케이션이 제공해야 하는 기능을 애플리케이션의 책임으로 생각하는 것이다. 메시지는 메시지를 수신할 객체가 아니라 메시지를 전송할 객체의 의도를 반영해서 결정해야 한다.

객체에게 책임을 할당하는 첫 번째 원칙은 책임을 수행할 정보를 알고 있는 객체에게 책임을 할당하는 것이다. GRASP에서는 이를 INFORMATION EXPERT(정보 전문가) 패턴이라고 부른다. INFORMATION EXPERT 패턴을 따르면 정보와 행동을 최대한 가까운 곳에 위치시키기 때문에 캡슐화를 유지할 수 있다.

만약 스스로 처리할 수 없는 작업이 있다면 외부에 도움을 요청해야 한다. 연쇄적인 메시지 전송과 수신을 통해 협력 공동체가 구성되는 것이다.

높은 응집도와 낮은 결합도

책임을 할당할 수 있는 다양한 대안들이 존재한다면 응집도와 결합도의 측면에서 더 나은 대안을 선택하는 것이 좋다. GRASP에서는 이를 LOW COUPLING(낮은 결합도) 패턴과 HIGH COHESION(높은 응집도) 패턴이라고 부른다.

LOW COUPLING 패턴

어떻게 하면 의존성을 낮추고 변화의 영향을 줄이며 재사용성을 증가시킬 수 있을까? 설계의 전체적인 결합도가 낮게 유지도되록 책임을 할당하라.

HIGH COHESION 패턴

어떻게 복잡성을 관리할 수 있는 수준으로 유지할 것인가? 높은 응집도를 유지할 수 있게 책임을 할당하라.

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

GRASP의 CREATOR(창조자) 패턴은 객체를 생성할 책임을 어떤 객체에게 할당할지에 대한 지침을 제공한다.

CREATOR 패턴

객체 A를 생성해야 할 때 어떤 객체에게 객체 생성 책임을 할당해야 하는가? 아래 조건을 최대한 많이 만족하는 B에게 객체 생성 책임을 할당하라.

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

구현을 통한 검증

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.isAfter(screening.getWhenScreened().toLocalTime()) >=0;
    }

    private boolean isSatisfiedBySequence(Screening screening) {
        return sequence == screening.getSequence();
    }
}

가장 큰 문제점은 변경에 취약한 클래스를 포함하고 있다는 것이다. DiscountCondition은 하나 이상의 변경 이유를 가지기 때문에 응집도가 낮다. 낮은 응집도가 초래하는 문제를 해결하기 위해서는 변경의 이유에 따라 클래스를 분리해야한다.

코드를 통해 변경의 이유를 파악할 수 있는 첫 번째 방법은 인스턴스 변수가 초기화되는 시점을 살펴보는 것이다. 응집도가 낮은 클래스는 객체의 속성 중 일부만 초기화하고 일부는 초기화되지 않은 상태로 남겨진다. 함께 초기화되는 속성을 기준으로 코드를 분리해야 한다.

코드를 통해 변경의 이유를 파악할 수 있는 두 번째 방법은 메서드들이 인스턴스 변수를 사용하는 방식을 살펴보는 것이다. 이 경우 클래스의 응집도를 높이기 위해서는 속성 그룹과 해당 그룹에 접근하는 메서드 그룹을 기준으로 코드를 분리해야 한다.

다형성을 통해 분리하기

역할을 사용하면 객체의 구체적인 타입을 추상화할 수 있다. 객체의 타입에 따라 변하는 행동이 있다면 타입을 분리하고 변화하는 행동을 각 타입의 책임으로 할당해야 한다. GRASP에서는 이를 POLYMORPHISM(다형성) 패턴이라고 부른다.

변경으로부터 보호하기

변경을 캡슐화하도록 책임을 할당하는 것을 GRASP에서는 PROTECTED VARIATIONS(변경 보호) 패턴이라고 부른다. PROTECTED VARIATIONS 패턴은 책임 할당의 관점에서 캡슐화를 설명한 것이다."설계에서 변하는 것이 무엇인지 고려하고 변하는 개념을 캡슐화하라."

변경과 유연성

설계를 주도하는 것은 변경이다. 개발자로서 변경에 대비할 수 있는 두 가지 방법이 있다.

  1. 코드를 이해하고 수정하기 쉽도록 최대한 단순하게 설계한다.
  2. 코드를 수정하지 않고도 변경을 수용할 수 있도록 코드를 더 유연하게 만든다.

새로운 할인 정책이 추가될 때마다 인스턴스를 생성하고, 상태를 복사하고, 식별자를 관리하는 코드를 추가하는 일은 번거로울뿐만 아니라 오류가 발생하기도 쉽다. 이 경우 코드의 복잡성이 높아지더라도 할인 정책의 변경을 쉽게 수용할 수 있게 코드를 유연하게 만드는 것이 더 좋은 방법이다. 해결 방법은 상속 대신 합성을 사용하는 것이다.

유연성은 의존성 관리의 문제다. 요소들 사이의 의존성의 정도가 유연성의 정도를 결정한다.

책임 주도 설계의 대안

책임과 객체 사이에 방황할 때 돌파구를 찾기 위해 선택하는 방법은 최대한 빠르게 목적한 기능을 수행하는 코드를 작성하는 것이다. 일단 실행되는 코드를 얻고 난 후에 코드 상에 명확하게 드러나는 책임들을 올바른 위치로 이동시키는 것이다. 이처럼 이해하기 쉽고 수정하기 쉬운 소프트웨어로 개선하기 위해 겉으로 보이는 동작은 바꾸지 않은 채 내부 구조를 변경하는 것을 리팩터링(Refactoring) 이라고 부른다.

메서드 응집도

긴 메서드는 다양한 측면에서 코드의 유지보수에 부정적인 영향을 미친다.

  • 어떤 일을 수행하는지 한눈에 파악하기 어렵기 때문에 코드를 전체적으로 이해하는 데 너무 많은 시간이 걸린다.
  • 하나의 메서드 안에서 너무 많은 작업을 처리하기 때문에 변경이 필요할 때 수정해야 할 부분을 찾기 어렵다.
  • 메서드 내부의 일부 로직만 수정하더라도 메서드의 나머지 부분에서 버그가 발생할 확률이 높다.
  • 로직의 일부만 재사용하는 것이 불가능하다.
  • 코드를 재사용하는 유일한 방법은 원하는 코드를 복사해서 붙여넣는 것뿐이므로 코드 중복을 초래하기 쉽다.

마이클 패더스(Michael Feathers)는 이런 메서드를 몬스터 메서드(monster method) 라고 부른다.

주석을 추가하는 대신 메서드를 작게 분해해서 각 메서드의 응집도를 높여라. 코드를 작은 메서드들로 분해하면 전체적인 흐름을 이해하기도 쉬워진다.

Leave a comment