본문 바로가기
그룹 스터디 공부(IT 서적)/오브젝트

05 책임 할당하기_01

by hanyugyeong 2023. 8. 7.
반응형
SMALL

데이터 중심 설게로 인해 발생하는 문제점을 해결할 수 있는 가장 기본적인 방법은 데이터가 아닌 책임에 초점을 맞추는 것이다.
책임에 초점을 맞춰서 설계할 때 직면하는 가장 큰 어려움은 어떤 객체에게 어떤 책임을 할당할지를 결정하기가 쉽지 않다는 것이다.
GRASP 패턴을 이해하고 나면 응집도와 결합도, 캡슐화 같은 다양한 기준에 따라 책임을 할당하고 결과를 트레이오프할 수 있는 기준을 배우게 될 것이다. 

01 책임 주도 설계를 향해

데이터 중심의 설계에서 책임 중심의 설계로 전환하기 위해서는 다음의 두 가지 원칙을 따라야 한다. 
1) 데이터보다 행동을 먼저 결정해라 
2) 협력이라는 문맥 안에서 책임을 결정해라

 

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

객체를 설계하기 위한 질문의 순서를 바꿔라 
데이터 중심의 설계에서는 
"이 객체가 포함해야 하는 데이터가 무엇인가"를 결정한 후에 "데이터를 처리하는 데 필요한 오퍼레이션은 무엇인가"를 결정한다. 

책임 중심의 설계에서는 
"이 객체가 수행해야 하는 책임은 무엇인가"를 결정한 후에 " 이 책임을 수행하는 데 필요한 데이터는 무엇인가"를 결정한다. 

객체지향 설계에서 가장 중요한 것은 적절한 객체에게 적절한 책임을 할당하는 능력이다.

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

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

협력을 시작하는 주체는 메시지 전송자이기 때문에 협력에 적합한 책임이란 메시지 수신자가 아니라 메시지 전송자에게 적합한 책임을 의미한다. 

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

 

메시지를 먼저 결정하기 때문에 메시지 송진자는 메시지 수신자에 대한 어떠한 가정도 할 수 없다. 

메시지 전송자의 관점에서 메시지 수신자가 깔끔하게 캡슐화되는 것이다. 

책임 중심의 설계가 응집도가 높고 결합도가 낮으며 변경하기 쉽다고 말하는 이유가 여기에 있다. 

책임 주도 설계 

책임 주도 설계의 흐름을 다시 나열한 것이다.

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

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

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

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

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

 

책임 주도 설계의 핵심은 책임을 정한 후에 책임을 수행할 객체를 결정하는 것이다.

 

02 책임 할당을 위한 GRASP 패턴

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

도메인 개념에서 출발하기 

어떤 책임을 할당해야 할 때 가장 먼저 고민해야 하는 유력한 후보는 바로 도메인 개념이다.

그림 5.1은 영화 예매 시스템을 구성하는 도메인 개념과 개념 사이의 관계를 대략적으로 표현한 것이다. 

하나의 영화는 여러 번 상영될 수 있으며, 하나의 상영은 여러 번 예약될 수 있다는 사실을 알 수 있다. 

영화는 다수의 할인 조건을 가질 수 있으며 할인 조건에는 순번 조건과 기간 조건이 존재한다. 

 

중요한 것은 설계를 시작하는 것이며 도메인 개념들을 완벽하게 정리하는 것이 아니다. 

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

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

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

 

사용자에게 제공해야 하는 기능은 영화를 예매하는 것이다. 

이를 책임으로 간주하면 애플리케이션은 영화를 애매할 책임이 있다고 말할 수 있다.

 

메시지는 메시지를 수신할 객체가 아니라 메시지를 전송할 객체의 의도를 반영해서 결정해야 한다.

 

따라서 첫 번째 질문은 다음과 같다

 

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

 

협력을 시작하는 객체는 미정이지만 이 객체가 원하는 것은 분명해 보인다. 바로 영화를 예매하는 것이다.

따라서 메시지의 이름으로는 예매하라가 적절하다.

메시지를 결정했으므로 메시지에 적합한 객체를 선택해야 한다. 두 번째 질문은 다음과 같다. 

 

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

 

객체가 상태와 행동을 통합한 캡슐화의 단위라는 사실에 집중해야 한다. 객체는 자신의 상태를 스스로 처리하는 자율적인 존재여야 한다.

객체의 책임과 책임을 수행하는 데 필요한 상태는 동일한 객체 안에 존재해야 한다. 따라서 객체에게 책임을 할당하는 첫 번째 원칙은 책임을 수행할 정보를 알고 있는 객체에게 책임을 할당하는 것이다. GRASP에서는 이를 INFORMATION EXPERT(정보 전문가) 패턴이라고 부른다. 

 

INFORMATION EXPORT 패턴에 따르면 애매하는데 필요한 정보를 가장 많이 알고 있는 객체에게 예매하라 메시지를 처리할 책임을 할당해야 한다. Screening에게 책임을 할당하자!

 Screening이 책임을 수행하는 데 필요한 작업을 구상해보고 스스로 처리할 수 없는 작업이 무엇인지를 가릴 정도의 수준이면 된다. 

 

만약 스스로 처리할 수 없는 작업이 있다면 외부에 도움을 요청해야 한다. 이 요청이 외부로 전송해야하는 새로운 메시지가 되고, 최종적으로 이 메시지가 새로운 객체의 책임으로 할당된다. 이 같은 연쇄적인 메시지 전송과 수신을 통해 협력 공동체가 구성되는 것이다.

 

여매 가격을 계산하는 작업이 필요한데 Screening은 가격을 계산하는 데 필요한 정보를 모르기 때문에 외부의 객체에게 도움을 요청해서 가격을 알아야한다. 외부에 대한 이 요청이 새로운 메시지가 된다. 

 

영화 가격을 계산하는 데 필요한 정보를 알고 있는 전문가는 영화(Movie)다. 따라서 INFORMATION EXPERT 패턴에 따라 메시지를 수신할 적당한 객체는 Movie가 될 것이다. 이제 Movie는 영화 가격을 계산할 책임을 지게 된다. 

 

할인 조건에 따라 영화가 할인 가능한지를 판단하는 것은 영화가 스스로 처리할 수 없다. 따라서 Movie는 할인 여부를 판단하라 메시지를 전송해서 외부의 도움을 요청해야 한다.

할인 여부를 판단하는 데 필요한 정보를 가장 많이 알고 있는 객체는 무엇인가? 이 정보에 대한 전문가는 바로 할인 조건(DiscountCondition)이다. DiscountCondition에게 이 책임을 할당하자.

DiscountCondition은 자체적으로 할인 여부를 판단하는 데 필요한 모든 정보를 알고 있기 때문에 외부의 도움 없이도 스스로 할인 여부를 판단할 수 있다. 따라서 DiscountCondition은 외부에 메시지를 전송하지 않는다.

 

INFORMATION EXPERT 패턴은 객체란 상태와 행동을 함께 가지는 단위라는 객체지향의 가장 기본적인 원리를 책임 할당의 관점에서 표현한다.

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

높은 응집도와 낮은 결합도

설계는 트레이드오프 활동이라는 것을 기억하라. 

올바른 책임 할당을 위해 INFORMATION EXPERT 패턴 이외의 다른 책임 할당 패턴들을 함께 고려할 필요가 있다. 

 

예를 들어, 방금 전에 설계한 영화 예매 시스템에서는 할인 요금을 계산하기 위해 Movie가 DiscountCondition에 할인 여부를 판단하라 메시지를 전송한다. 그렇다면 이 설계의 대안으로 Movie 대신 Screening이 직접 DiscountCondition과 협력하게 하는 것은 어떨까? 이를 위해서는 Screening이 DiscountCondition에게 할인 여부를 판단하라 메시지를 전송하고 반환받은 할인 여부를 Movie에 전송하는 메시지의 인자로 전달하도록 수정해야 한다.                                                                                                                                                                          

 

차이점이라면 DiscountCondition과 협력하는 객체가 Movie가 아니라 Screening이라는 것뿐이다. 

우리는 이 설계 대신 Movie가 DiscountCondition과 협력하는 방법을 선택한 것일까?

그 이유는 응집도와 겹합도에 있다. 높은 응집도와 낮은 결합도는 객체에 책임을 할당할 때 항상 고려해야 하는 기본 원리다. 다시 말해 두 협력 패턴 중에서 높은 응집도와 낮은 결합도를 얻을 수 있는 설계가 있다면 그 설계를 선택해야 한다는 것이다. 

 

GRASP에서는 이를 LOW COUPLING(낮은 결합도) 패턴과 HIGH COHESION(높은 응집도) 패턴이라고 부른다. 

LOW COUPLING 패턴부터 살펴보자.

 

DiscountCondition이 Movie와 협력하는 것이 좋을까, 아니면 Screening과 협력하는 것이 좋을까? 해답의 실마리는 결합도에 있다. 

Movie와 DiscountCondition은 이미 결합돼 있기 때문에 Movie를 DiscountCondition과 협력하게 하면 설계 전체적으로 결합도를 추가하지 않고도 협력을 완성할 수 있다.

 

하지만 Screening이 DiscountCondition과 협력할 경우에는 Screening과 DiscountCondition 사이에 새로운 결합도가 추가된다. 따라서 LOW COUPLING 패턴의 관점에서는 Screening이 DiscountCondition과 협력하는 것보다는 Movie가 DiscountCondition과 협력하는 것이 더 나은 설계 대안인 것이다.      

 

HIGH COHESION 패턴의 관점에서도 설계 대안들을 평가할 수 있다. 

Screening의 가장 중요한 책임은 예매를 생성하는 것이다. 만약 Screening이 DiscountCondition과 협력해야 한다면 Screening은 영화 요금 계산과 관련된 책임 일부를 떠안아야 할 것이다. 이 경우 Screening은 DiscountCondition이 할인 여부를 판단할 수 있고 Movie가 이 할인 여부를 필요로 한다는 사실 역시 알고 있어야 한다. 

 

예매 요금을 계산하는 방식이 변경될 경우 Screening도 함께 변경해야 하는 것이다. 결과적으로 Screening과 DiscountCondition이 협력하게 되면 Screening은 서로 다른 이유로 변경되는 책임을 짊어지게 되므로 응집도가 낮아질 수밖에 없다. 

 

반면 Movie의 주된 책임은 영화 요금을 계산하는 것이다. 따라서 영화 요금을 계산하는 데 필요한 할인 조건을 판단하기 위해 Movie가 DiscountCondition과 협력하는 것은 응집도에 아무런 해도 끼치지 않는다. 

 

LOW COUPLING 패턴과 HIGH COHESION 패턴은 설계를 진행하면서 책임과 협력의 품질을 검토하는 데 사용할 수 있는 중요한 평가 기준이다. 

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

GRASP의 CREATOR(창조자) 패턴은 이 같은 경우에 사용할 수 있는 책임 할당 패턴으로서 객체를 생성할 책임을 어떤 객체에게 할당할지에 대한 지침을 제공한다. 

 

CREATOR 패턴 

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

1) B가 A객체를 포함하거나 참조한다. 

2) B가 A객체를 기록한다.

3) B가 A 객체를 긴밀하게 사용한다. 

4) B가 A객체를 초기화하는 데 필요한 데이터를 가지고 있다. 

 

CREATOR 패턴의 의도는 어떤 방식으로든 생성되는 객체와 연결되 있거나. 관련될 필요가 있는 객체에 해당 객체를 생성할 책임을 맡기는 것이다. 

 

Screening은 예매 정보(Reservation)를 생성하는 데 필요한 영화,상영시간,순번 등의 정보에 대한 전문가이다. 

=> Screening을 Reservation의 CREATOR로 결정 

03 구현을 통한 검증

Screening은 예매에 대한 정보 전문가인 동시에 Reservation의 창조자다.

 

협력의 관점에서 Screening은 예매하라 메시지에 응답할 수 있어야 한다. 따라서 이 메시지를 처리할 수 있는 메서드를 구현하자

 public Reservation reserve(Customer customer, int audienceCount) {
        return new Reservation(customer, this, calculateFee(audienceCount), audienceCount);
    }

Screeing은 상영시간(whenScreened)과 상영 순번(sequence)을 인스턴스 변수로 포함한다. 또한 Movie에 가격을 계산하라 메시지를 전송해야 하기 때문에 영화(movie)에 대한 참조도 포함해야 한다. 

public class Screening {
    private Movie movie;
    private int sequence;
    private LocalDateTime whenScreened;

    public Reservation reserve(Customer customer, int audienceCount) {
        return new Reservation(customer, this, calculateFee(audienceCount), audienceCount);
    }
}

영화를 예매하기 위해서는 movie에게 가격을 계산하라 메시지를 전송해서 계산된 영화 요금을 반환받아야 한다. 

calculateFee 메서드는 이렇게 반환된 요금에 예매 인원 수를 곱해서 전체 예매 요금을 계산한 후 Reservation을 생성해서 반환한다. 

public class Screening {
    private Movie movie;
    private int sequence;
    private LocalDateTime whenScreened;

    public Reservation reserve(Customer customer, int audienceCount) {
        return new Reservation(customer, this, calculateFee(audienceCount), audienceCount);
    }

    private Money calculateFee(int audienceCount) {
        return movie.calculateMovieFee(this).times(audienceCount);
    }

    public LocalDateTime getWhenScreened() {
        return whenScreened;
    }

    public int getSequence() {
        return sequence;
    }
}

Screeing이 Movie에 전송하는 메시지의 시그니처를 calculateMovieFee(Screening screening)으로 선언했다. 

이 메시지는 수신자인 Movie가 아니라, 송신자인 Screening의 의도를 표현한다. 

 

여기서 핵심은, Screening은 Movie의 내부 구현에 대한 어떤 사전 지식도 없이 전송할 메시지를 결정했다는 점이다. 

Movie의 구현을 고려하지 않고 필요한 메시지를 결정하면 Movie 내부 구현을 깔끔하게 캡슐화 할 수 있다. 

 

이제 Screening과 Movie를 연결하는 유일한 연결 고리는 메시지뿐이다. 따라서 메시지가 변경되지 않는한 Movie에 어떤 수정을 가하더라도 Screening에는 영향을 미치지 않는다. 메시지가 객체를 선택하도록 책임 주도 설계의 방식을 따르면 캡슐화와 낮은 결합도라는 목표를 비교적 손쉽게 달성할 수 있다. 

 

Screening은 Movie와 협력하기 위해 calculateMovieFee 메시지를 전송한다. Movie는 이 메시지에 응답하기 위해 calculateMovieFee 메서드를 구현해야 한다.

 

Movie 요금계산 메서드를 구현하자 

public class Movie { 
    public Money calculateMovieFee(Screening screening) {

    }
}

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) {
       
    }  
}

 

MovieType 단순 열거형

public enum MovieType {
    AMOUNT_DISCOUNT,	//금액 할인 정책
    PERCENT_DISCOUNT,	//비율 할인 정책
    NONE_DISCOUNT		//미적용
}

 

Movie는 먼저 discountConditions의 원소를 차례대로 순회하면서 DiscountCondition 인스턴스에게 isSatisfiedBy 메시지를 전송해서 할인 여부를 판단하도록 요청한다. 만약 할인 조건을 만족하는 DiscountCondition 인스턴스가 존재한다면 할인 요금을 계산하기 위해 calculateDiscountAmount 메서드를 호출한다. 만약 만족하는 할인 조건이 존재하지 않을 경우에는 기본 금액인 fee를 반환한다. 

public class Movie {
    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));
    }
}

실제로 할인 요금을 계산하는 calculateDiscountAmount 메서드는 movieType의 값에 따라 적절한 메서드를 호출한다. 

public class Movie {
       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;
    }
}

Movie는 각 DiscountCondition에 할인 여부를 판단하라 메시지를 전송한다. DiscountCondition은 이 메시지를 처리하기 위해 isSatisfiedBy 메서드를 구현해야 한다. 

public class DiscountCondition {
    public boolean isSatisfiedBy(Screening screening) {
    
    }
}

DiscountCondition은 기간 조건을 위한 요일(dayOfWeek), 시작 시간(startTime), 종료 시간(endTime)과 순번 조건을 위한 상영 순번(sequence)을 인스턴스 변수로 포함한다. 추가적으로 할인 조건의 종류(type)를 인스턴스 변수로 포함한다. isSatisfiedBy 메서드는 type의 값에 따라 적절한 메서드를 호출한다. 

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

 DiscountCondition은 할인 조건을 판단하기 위해 Screening의 상영 시간과 상영 순번을 알아야 한다. 두 정보를 제공하는 메서드를 Screening에 추가하자

public class Screening {
    public LocalDateTime getWhenScreened() {
        return whenScreened;
    }

    public int getSequence() {
        return sequence;
    }
}

DiscountConditionType은 할인 조건의 종류를 나열하는 단순한 열거형 타입이다.

public enum DiscountConditionType {
    SEQUENCE,       // 순번조건
    PERIOD          // 기간 조건
}

DiscountCondition 개선하기 

현재의 코드에서 변경의 이유가 다양한 클래스는 무엇인가? 바로 DiscountCondition이다. DiscountCondition은 다음과 같이 서로 다른 세 가지 이유로 변경될 수 있다.

1) 새로운 할인 조건 추가 

2) 순번 조건을 판단하는 로직 변경 

3) 기간 조건을 판단하는 로직이 변경되는 경우 

 

DiscountCondition은 하나 이상의 변경 이유를 가지기 때문에 응집도가 낮다. 낮은 응집도가 초래하는 문제를 해결하기 위해서는 변경의 이유에 따라 클래스를 분리해야 한다. 

 

DiscountCondition 안에 구현된 isSatisfiedBySequence 매서드와 isSatisfiedByPeriod 메서드는 서로 다른 이유로 변경된다.

isSatisfiedBySequence 메서드는 순번 조건에 대한 요구사항이 달라질 경우 구현이 변경된다. 

isSatisfiedByPeriod 메서드는 기간 조건에 대한 요구사항이 달라질 경우 구현이 변경된다. 

 

서로 다른 이유로 변경되는 두 개의 메서드를 가지는 DiscountCondition 클래스의 응집도는 낮아질 수밖에 없는 것이다. 

 

1) 코드를 통해 변경의 이유를 파악할 수 있는 첫 번째 방법은 인스턴스 변수가 초기화되는 시점을 살펴보는 것이다.

클래스의 속성이 서로 다른 시점에 초기화되거나 일부만 초기화된다는 것은 응집도가 낮다는 증거다.

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

 

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

모든 메서드가 객체의 모든 속성을 사용한다면 클래스의 응집도는 높다고 볼 수 있다.

매서드들이 사용하는 속성에 따라 그룹이 나뉜다면 클래스의 응집도는 낮다고 볼 수 있다.

 

타입 분리하기 

DiscountCondition의 가장 큰 문제는 순번 조건과 기간 조건이라는 두 개의 독립적인 타입이 하나의 클래스 안에 공존하고 있다는 점이다. 가장 먼저 떠오르는 해결 방법은 두 타입을 SequenceCondition과 PeriodCondition이라는 두 개의 클래스로 분리하는 것이다. 

 

분리된 후의 PeriodCondition 클래스다. 

public class PeriodCondition {
    private DayOfWeek dayOfWeek;
    private LocalTime startTime;
    private LocalTime endTime;

    public PeriodCondition(DayOfWeek dayOfWeek, LocalTime startTime, LocalTime endTime) {
        this.dayOfWeek = dayOfWeek;
        this.startTime = startTime;
        this.endTime = endTime;
    }

    public boolean isSatisfiedBy(Screening screening) {
        return dayOfWeek.equals(screening.getWhenScreened().getDayOfWeek()) &&
                startTime.compareTo(screening.getWhenScreened().toLocalTime()) <= 0 &&
                endTime.compareTo(screening.getWhenScreened().toLocalTime()) >= 0;
    }
}

SequenceCondtion은 하나의 인스턴스 변수만을 포함하는 간단한 클래스로 분리될 수 있다. 

public class SequenceCondition {
    private int sequence;

    public SequenceCondition(int sequence) {
        this.sequence = sequence;
    }

    public boolean isSatisfiedBy(Screening screening) {
        return sequence == screening.getSequence();
    }
}

클래스를 분리하면 앞에서 언급했던 문제점들이 모두 해결된다. SequenceCondition과 PeriodCondition은 자신의 모든 인스턴스 변수를 함께 초기화할 수 있다. 

sequence 속성만 사용하는 메서드는 SequenceCondition으로, dayOfWeek, startTime, endTime을 사용하는 메서드는 PeriodCondition으로 이동했기 때문에 클래스에 있는 모든 메서드는 동일한 인스턴스 변수 그룹을 사용한다. 결과적으로 개별 클래스들의 응집도는 향상됐다

그러나 수정 후에 Movie의 인스턴스는 SequenceCondition과 PeriodCondition이라는 두 개의 서로 다른 클래스의 인스턴스 모두와 협력할 수 있어야 한다. 

Movie 클래스 안에서 SequenceCondition의 목록과 PeriodCondition의 목록을 따로 유지하는 것이다. 

public class Movie {
    private List<PeriodCondition> periodConditions;
    private List<SequenceCondition> sequenceConditions;

    private boolean isDiscountable(Screening screening) {
        return checkPeriodConditions(screening) ||
                checkSequenceConditions(screening);
    }

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

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

클래스를 분리한 후에 설계의 관점에서 전체적인 결합도가 높아진 것이다. 

두번째 문제는 수정 후에 새로운 할인 조건을 추가하기가 더 어려워졌다 

클래스를 분리하기 전에는 DiscountCondition의 내부 구현만 수정하면 Movie에는 아무런 영향도 미치지 않았다 

하지만 수정 후에는 할인 조건을 추가하려면 Movie도 함께 수정해야 한다. DiscountCondition의 입장에서 보면 응집도가 높아졌지만 변경과 캡슐화라는 관점에서 보면 전체적으로 설계의 품질이 나빠졌다.

반응형
LIST