4장은 일부러 잘못 설계했다. 데이터를 먼저 정하고, 그 데이터를 꺼내 쓰는 절차를 한곳에 몰았다. 결과가 ReservationAgency다 — 모든 객체의 게터를 빨아들여 혼자 계산하는 트랜잭션 스크립트. 이 장은 그 코드를 지우지 않고 책임을 재배치해서 되살린다. 기법의 이름은 GRASP(General Responsibility Assignment Software Patterns), 책임을 어느 객체에게 줄지 결정하는 원칙들의 모음이다.

핵심 질문은 하나다. 이 책임을 수행하는 데 필요한 정보를 가장 많이 알고 있는 객체가 누구인가. 그 객체에게 책임을 준다. 이것이 정보 전문가(Information Expert) 패턴이고, 나머지 원칙들은 이 결정이 낳는 결합도와 응집도를 점검하는 장치다.

데이터에서 시작하면 왜 망가지는가

ReservationAgency는 아무 잘못된 문법도 쓰지 않았다. 문제는 모든 결정을 자기가 내린다는 것이다. 할인 조건을 판단하려고 Screening의 상영 시간과 순번을 꺼내고, 요금을 계산하려고 Movie의 타입·금액·비율을 꺼낸다. 데이터의 주인은 따로 있는데 판단은 여기서 한다.

이 구조의 대가는 변경에서 나타난다. 할인 조건 타입을 하나 추가하면 ReservationAgencyif-else를 고쳐야 하고, 요금 정책을 바꿔도 이 클래스를 열어야 한다. 서로 무관한 변경 이유들이 한 클래스에 모여 있다 — 낮은 응집도다. 그리고 이 클래스는 Movie·Screening·DiscountCondition의 내부 구조를 전부 알고 있다 — 높은 결합도다.

판단 기준: “데이터를 어떻게 나눌까”가 아니라 “어떤 행동이 필요하고, 그 행동에 필요한 데이터를 누가 가졌나”를 먼저 묻는다. 함정: 게터가 많아지는 클래스는 대개 자기 책임을 남에게 빼앗긴 클래스다. 게터의 개수는 캡슐화가 새는 구멍의 개수다.

책임을 집으로 돌려보낸다

아래 스텝 플레이어가 이 장의 전부다. 게터를 빨아들이는 절차 덩어리에서 시작해, 각 책임을 그 정보를 가진 객체에게 하나씩 돌려준다. 마지막에는 조건 타입 분기가 다형성으로 사라진다.

Refactoring Step 파탄 — ReservationAgency가 혼자 다 안다
1/4
public class ReservationAgency {    public Reservation reserve(Screening screening, Customer customer, int audienceCount) {        Movie movie = screening.getMovie();        Money discountAmount = switch (movie.getMovieType()) {            case AMOUNT_DISCOUNT -> movie.getDiscountAmount();            case PERCENT_DISCOUNT -> movie.getFee().times(movie.getDiscountPercent());            case NONE_DISCOUNT -> Money.ZERO;        };        boolean discountable = false;        for (DiscountCondition condition : movie.getDiscountConditions()) {            if (condition.getType() == DiscountConditionType.PERIOD) {                discountable = screening.getWhenScreened().getDayOfWeek().equals(condition.getDayOfWeek())                    && condition.getStartTime().compareTo(screening.getWhenScreened().toLocalTime()) <= 0                    && condition.getEndTime().compareTo(screening.getWhenScreened().toLocalTime()) >= 0;            } else {                discountable = condition.getSequence() == screening.getSequence();            }            if (discountable) break;        }        Money fee = discountable ? movie.getFee().minus(discountAmount) : movie.getFee();        return new Reservation(customer, screening, fee, audienceCount);    }}

네 걸음을 GRASP로 다시 읽기

창조자(Creator)Reservation을 누가 만드는가. GRASP은 “생성될 객체를 긴밀히 사용하거나, 그 초기값을 가진 객체”에게 생성 책임을 준다. ReservationScreening·Customer·요금으로 만들어지고, 그 대부분을 아는 건 Screening이다. 그래서 reserveScreening의 메서드가 된다.

정보 전문가(Information Expert) — 요금 계산에 필요한 정보(기본 요금, 할인 정책)는 Movie가 가졌다. 그래서 Screening은 요금을 직접 계산하지 않고 movie.calculateMovieFee(this)시킨다. 마찬가지로 개별 할인 조건의 만족 여부는 그 조건 객체가 가장 잘 안다 — condition.isSatisfiedBy(screening). 게터로 데이터를 꺼내 밖에서 판단하던 흐름이, 데이터를 가진 쪽에게 판단을 맡기는 흐름으로 뒤집혔다.

낮은 결합도(Low Coupling)ReservationAgency는 세 클래스의 내부를 전부 알았다. 재배치 후 ScreeningMoviecalculateMovieFee 하나만 알고, MovieDiscountConditionisSatisfiedBy 하나만 안다. 각자 이웃의 인터페이스만 알지 내부 필드는 모른다. 결합의 폭이 게터 다발에서 메시지 하나로 줄었다.

높은 응집도(High Cohesion) — 할인 조건이 바뀌면 조건 클래스만, 요금 정책이 바뀌면 Movie만 열면 된다. 변경의 이유가 클래스마다 하나씩으로 정렬됐다. 한 클래스에 모여 있던 서로 무관한 변경 이유들이 각자의 집을 찾았다.

판단 기준: 책임을 줄 후보가 여럿이면 정보 전문가를 1순위로 놓되, 그 배치가 결합도를 높이거나 응집도를 깨면 후보를 재검토한다. GRASP은 규칙이 아니라 서로를 견제하는 힘의 균형이다. 함정: 정보 전문가를 기계적으로 적용하면 데이터를 가진 클래스가 만능이 되어 다시 God 클래스로 돌아간다. “정보를 가진 객체”와 “그 정보로 하는 일이 그 객체의 본분인가”를 함께 본다.

다형성으로 타입 분기를 흩는다

마지막 걸음이 가장 중요하다. if (condition.getType() == PERIOD) 같은 타입 검사 분기는 “새 타입이 생기면 여기도 고쳐야 한다”는 신호다. 조건 타입을 인터페이스와 구현 클래스로 나누면, 각 조건이 자기 판단(isSatisfiedBy)을 스스로 안다. 분기문이 사라진 자리에 다형성이 들어선다.

이것이 POLYMORPHISM 패턴이다 — 타입에 따라 달라지는 행동을 조건문이 아니라 타입 계층으로 표현한다. 순번 할인 조건(SequenceCondition)을 새로 추가해도 기존 코드는 한 줄도 건드리지 않는다. List<DiscountCondition>에 인스턴스를 하나 더 넣으면 끝이다 — 확장에는 열리고 변경에는 닫힌다.

판단 기준: switch/if-else같은 기준(여기선 조건 타입)으로 여러 곳에서 반복되면 다형성으로 흩을 후보다. 한 곳에서 한 번만 분기한다면 굳이 클래스로 쪼갤 필요는 없다. 함정: MoviecalculateDiscountAmount에는 아직 movieType 스위치가 남아 있다. 이것도 같은 냄새다 — 이 분기는 상속으로 흩는 게 자연스럽고, 10장에서 AmountDiscountMovie·PercentDiscountMovie로 갈라진다. 한 장에서 모든 분기를 없애려 들지 않는다.

정리

  • 데이터를 먼저 나누면 절차가 한곳에 몰린다. 행동을 먼저 묻고, 그 행동에 필요한 정보를 가진 객체에게 책임을 준다 — 정보 전문가.
  • 게터의 개수는 캡슐화가 새는 구멍의 개수다. 데이터를 꺼내 밖에서 판단하는 대신, 데이터를 가진 객체에게 시킨다.
  • GRASP의 네 원칙(창조자·정보 전문가·낮은 결합·높은 응집)은 규칙이 아니라 서로를 견제하는 힘이다. 정보 전문가를 기계적으로 밀면 다시 God 클래스가 된다.
  • 같은 기준으로 반복되는 타입 분기는 다형성으로 흩는다. 확장은 새 클래스 추가로, 기존 코드 수정 없이.
  • movieType 분기는 아직 남았다. 이 실은 10장의 상속에서 마저 풀린다.

다음장으로 6장