오늘은 Chapter 05 책임 할당하기를 공부해보겠습니다.
- 데이터 중심 설계의 문제점
- 행동 보다 데이터를 먼저 결정
- 고립된 객체의 상테에 초점
- 캡슐화 위반
- 결합도가 높아짐
- 변경이 어려워짐
- 책임 주도 설계의 두가지 원칙
- 데이터보다 행동을 먼저 결정하라
- 협력이라는 문맥 안에서 책임을 결정하라
- 데이터보다 행동을 먼저 결정하라
- 객체에게 중요한 것은 외부에 제공하는 행동
- 행동이란 객체의 책임을 의미
- 데이터는 객체가 책임을 수행하는 데 필요한 재료를 제공
- 데이터 중심과 책임 중심 설계의 차이
- 데이터 중심 : "이 객체가 포함해야하는 데이터는 무엇인가" ➡️ "데이터를 처리하는 데 필요한 오퍼레이션은 무엇인가"
- 책임 중심 : "이 객체가 수행해야 하는 책임은 무엇인가" ➡️ "이 책임을 수행하는 데 필요한 데이터는 무엇인가"
- 협력이라는 문맥 안에서 책임을 결정하라
- 객체에게 할당된 책임의 품질은 협력에 적합한 정도로 결정됨
- 협력을 시작하는 주체는 메시지 전송자, 협력에 적합한 책임은 메시지 전송자에게 적합한 책임을 의미함
- 협력에 적합한 책임을 수확하기 위해서는 객체가 메시지를 선택하는 것이 아니라 메시지가 객체를 선택하게 해야 함
- 객체를 결정하기 전에 객체가 수신할 메시지를 먼저 결정해야 한다.
- 메시지를 먼저 결정하면 메시지 전송자는 메시지 수신자에 대한 어떠한 가정도 할 수 없다.(메시지 수신자 캡슐화)
- 책임 주도 설계
- 책임 주도 설계의 흐름
- 시스템이 사용자에게 제공해야 하는 기능인 시스템 책임을 파악한다.
- 시스템 책임을 더 작은 책임으로 분할한다.
- 분할된 책임을 수행할 수 있는 적절한 객체 또는 역할을 찾아 책임을 할당한다.
- 객체가 책임을 수행하는 도중 다른 객체의 도움이 필요한 경우 이를 책임질 적절한 객체 또는 역할을 찾는다.
- 해당 객체 또는 역할에게 책임을 할당함으로써 두 객체가 협력하게 된다.
- GRASP 패턴(General Responsibility Assignment Software Pattern)
- 도메인 개념에서 출발하기
- 설계를 시작하는 단계에서는 개념들의 의미와 관계가 정확하거나 완벽할 필요는 없다.
- 이 단계에서는 책임을 할당받을 객체들의 종류와 관계에 대한 유용한 정보를 제공할 수 있으면 충분하다.
- 정보 전문가에게 책임을 할당하라
- 책임 주도 설계 방식의 첫 단계는 애플리케이션이 제공해야 하는 기능을 애플리케이션의 책임으로 생각 하는 것이다.
- 책임 : 애플리케이션에 대해 전송된 메시지, 메시지 : 책임질 첫 번째 객체
- ex) 사용자에게 제공해야하는 기능이 영화를 예매하는 것 이다.
- 애플리케이션은 영화를 예매할 책임이 있다.
- 1. 메시지를 전송할 객체는 무엇을 원하는가?
- 객체가 원하는 것은 영화를 예매하는 것으로 이 책임을 수행할 메시지를 '예매하라'로 결정
- 2. 메시지를 수신할 적합한 객체는 누구인가?
- 객체는 상태와 행동을 통합한 캡슐화의 단위
- 객체에게 책임을 할당하는 첫 번째 원칙은 책임을 수행할 정보를 알고 있는 객체에게 책임을 할당하는 것
- GRASP에서는 이를 INFORMATION EXPERT 패턴이라고 부른다.
- '예매하라' 메시지를 처리할 책임을 할당 받는 객체 : '상영'
- '상영'은 영화에 대한 정보, 상영 시간, 상영 순번 같이 영화 예매에 필요한 정보를 알고 있다.
- '상영'이 '예매하라' 메시지를 수신했을 때 수행해야 하는 작업을 생각해보자.
- 스스로 처리할 수 없는 작업이 있다면 외부에 도움을 요청해야한다.
- 이 요청을 위해 새로운 메시지가 생성되고 이 메시지가 새로운 객체의 책임으로 할당된다.
- '예매하라'메시지를 완료하려면 예매 가격을 계산하는 작업이 필요하다.
- '상영'은 가격 계산에 필요한 정보를 모르기 때문에 외부에 도움을 요청해야 한다.
- 외부 요청시 사용될 새로운 메시지 : '가격을 계산하라'
- 이 메시지를 책임질 객체 : '영화'
- '영화'는 영화 가격을 계산할 책임을 지게된다.
- 이처럼 연쇄적인 메시지 전송과 수신을 통해 협력 공동체가 구성이 되는 것이다.
- 다음 설명을 위해 '영화'에 메시지 '할인 여부를 판단하라'의 책임을 '할인 조건'에게 할당했다.
- 높은 응집도와 낮은 결합도
- LOW COUPLING 패턴
- 이 패턴의 입장에서 '할인 조건'이 '영화'와 '상영'중 어느 것에 협력하는 것이 좋을까?
- 위에 작성한 결합도를 보면 이미 '영화'와 '할인 조건이 결합되어 있기 때문에 결합도를 추가하지 않고 협력을 완성할 수 있다.
- 하지만 '상영'과 협력하려면 새로운 결합도가 추가되어애 하기 때문에 LOW COUPLING 관점에서는 '영화'와 결합하는 것이 좋다.
- HIGH COHESION 패턴
- 만약 '상영'이 '할인 조건'과 협력해야 한다면 '상영'은 영화 요금 계산과 관련된 책임 일부를 떠안아야 할 것이다.
- 즉, 예매 요금 방식이 변경된다면 '상영'도 함께 변경되어야 하는 것이다.
- 이렇게 되면 '상영'은 서로 다른 이유로 변경되는 책임을 짊어지게 되므로 응집도가 낮아질 수 밖에 없다.
- 반면 '영화'와 '할인 조건'이 협력한다면, 이미 '영화'의 주된 책임은 요금을 계산 하는 것 이기 때문에 응집도에 아무런 영향을 주지않는다.
- HIGH COHESION 패턴의 관점에서도 '할인 조건'은 '영화'와 협력하는 것이 더 좋다.
- CREATOR(창조자) 패턴
- 영화 예매 협력의 최종 결과물은 Reservation 인스턴스를 생성하는 것
- 협력에 참여하는 어떤 객체에는 Reservation 인스턴스를 생성하는 책임을 할당해야 한다는 것을 의미한다.
- CREATOR 패턴은 객체를 생성할 책임을 어떤 객체에게 할당할지에 대한 지침을 제공한다.
- CREATOR 패턴의 책임 할당 조건(객체 B에 객체A 생성 책임을 할당)
1. B가 A 객체를 포함하거나 참조한다.
2. B가 A객체를 기록한다.
3. B가 A객체를 긴밀하게 사용한다.
4. B가 A객체를 초기화하는 데 필요한 데이터를 가지고 있다.(이 경우 B는 A에 대한 정보 전문가다)
- 영화 예매 예시에서 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 moviw.calculateMovieFee(this).times(audienceCount);
}
public LocalDateTime getWhenScreened() {
return whenScreened;
}
public int getSequence() {
return sequence;
}
}
//영화
public class Movie {
private String title;
private Duration runningTime;
private Money fee;
private List<DiscountCondition> discountConditions;
private MovieType movieType;
private Money discountMoney;
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_DISOUNT:
return calculateAmountDiscounntAmount();
case PERCENT_DISCOUNT:
return calculatePercentDiscountAmount();
case NONE_DISCOUNT:
return calculateNoneDiscountAmount();
}
}
}
public enum MovieType {
AMOUNT_DISCOUNT, //금액 할인 정책
PERCENT_DISCOUNT, //비율 할인 정책
NONE_DISCOUNT //미적용
}
//할인 조건
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);
}
}
public 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();
}
}
public enum DiscountConditionType {
SEQUENCE,
PERIOD
}
- DiscountCondition 개선
- 새로운 할인 조건 추가 : isSatisfiedBy 메서드의 if~else문을 수정해야 한다. 새로운 할인 조건이 새로운 데이터를 요구하면 DiscountCondition에 속성을 추가하는 작업도 필요
- 순번 조건을 판단하는 로직 변경 : isSatisfiedBySequence 메서드의 내부 구현 수정. 순번 조건판단 필요 데이터가 변경된다면 DiscountCondition의 sequence 속성도 변경
- 기간 조건을 판단하는 로직이 변경되는 경우 : isSatisfiedByPeriod 메서드의 내부 구현 수정. 기간 조건을 판단 필요 데이터가 변경된다면 DiscountConditiom의 dayOfWeek, startTime, endTime 속성 역시 변경
- 위처럼 변경의 이유가 다양하기 때문에 응집도가 낮다고 할 수 있다.
- 이 문제를 해결하려면 변경의 이유에 따라 클래스를 분리해야 한다.
- 그렇다면 변경의 이유는 어떻게 파악을 할까?
- 첫 번째, 인스턴스 변수가 초기화 되는 시점을 살펴본다.
- 응집도가 높은 클래스 : 인스턴스 생성 시 모든 속성 초기화 , 응집도가 낮은 클래스 : 인스턴스 생성 시 일부만 초기화
- 이런 경우에는 함께 초기화되는 속성을 기준으로 코드를 분리해야 한다.
- 두 번째, 메서드들이 인스턴스 변수를 사용하는 방식을 살펴본다.
- 응집도가 높은 클래스 : 모든 메서드가 모든 속성을 사용, 응집도가 낮은 클래스 : 메서드들이 사용하는 속성에 따라 그룹이 나뉜다.
- 이런 경우에는 속성 그룹과 해당 그룹에 접근하는 메서드 그룹을 기준으로 코드를 분리해야 한다.
- 다시 DiscountCondtion으로 와서, DiscountCondtion은 순번 조건과 기간 조건이라는 두 개의 독립적인 타입이 한 클래스에 공존하고 있기 때문에 두 타입을 SequenceCondtion과 PeriodCondition이라는 두 개의 클래스로 분리한다
//기간 조건
public class PeriodCondtion {
private DayOfWeek dayOfWeek;
private LocalTime startTime;
private LocalTime endTime;
public PeriodCondtion(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().getDayWeek()) &&
startTime.compareTo(screening.getWhenScreened().toLocalTime()) <= 0 &&
endTime.compareTo(screening.getWhenScreened().toLocalTime() >= 0);
}
//순번 조건
public class SequenceCondition {
private int sequence;
public SequenceCondition(int sequence) {
this.sequence = sequence;
}
public boolean isSatisfiedBy(Screening screening) {
return sequence == screening.getSequence();
}
}
- 하지만 이렇게 두 개의 클래스로 나누면 새로운 문제점이 생기는데
- Movie에서 PeriodCondition과 SequenceCondition 두 개의 클래스에 결합을 해야한다. 즉 결합도가 올라가게 된다.
- 다형성을 통해 분리하기
- Movie 입장에서는 PeriodCondition이던, SequenceCondition이던 할인 가능 여부만 반환해준다면 상관이 없다.
- 이는 두 클래스 모두 Movie의 입장에서 동일한 역할을 수행하는 것을 의미한다.
- Movie가 구체적인 클래스는 알지 못한 채 오직 역할에 대해서만 결합되도록 의존성을 제한할 수 있다.
- 즉 구현을 알 필요가 없기 때문에 인터페이스를 사용하면 된다.
public interface DiscountCondtion {
boolean isSatisfiedBy(Screening screening);
}
public class PeriodCondition implements DiscountCondition { ... }
public class SequenceCondition implements DiscountCondition { ... }
- GRASP에서는 이를 POLYMORPHISM(다형성) 패턴이라고 부른다.
- 변경으로부터 보호하기
- DiscountCondtion은 Movie로 부터 PeriodCondtion과 SequenceCondtion의 존재를 감춘다.
- 이럴 때 할인 조건이 추가된다면, DiscountCondtion에 구현 클래스를 추가하면된다.
- Movice는 할인 조건이 추가되어도 어떤 수정도 필요가 없다.
- 이처럼 변경을 캡슐화하도록 책임을 할당하는 것을 GRASP에서는 PROTECTED VARIATIONS(변경 보호) 패턴이라고 부른다.
- 메서드 응집도
- 리팩터링(Refactoring) : 이해하기 쉽고 수정하기 쉬운 소프트웨어로 개선하기 위해 겉으로 보이는 동작을 바꾸지 않을 채 내부 구조를 변경 하는 것
- 예매처리를 하는 모든 절차는 ReservationAgency에 집중되어 있었다. (reserve 매서드에 모든 로직이 존재(
- 긴 메서드는 응집도가 낮기 때문에 이해하히고 어렵도 재사용도 어려우며 변경하기도 어렵다.
- 이를 몬스터 메서드(monster method)라고 부른다.
- 메서드가 길다고 주석을 추가할게 아니라, 메서드를 작게 분해해서 응집도를 높여야 한다.
- 목적이 명확한 메서드들로 구성되어 있다면,
- 변경을 처리하기 위해 어떤 메서드를 수정해야 할 지 쉽게 판단할 수 있다.
- 재사용하기도 쉽다.
- 코드를 이해하기도 쉽다.
'Object' 카테고리의 다른 글
[Object] Chapter06. 메시지와 인터페이스 (0) | 2025.08.23 |
---|---|
[Object] Chapter04. 설계 품질과 트레이드오프 (0) | 2025.07.26 |
[Object] Chapter03. 역할, 책임, 협력 (0) | 2025.07.22 |
[Object] Chapter02. 객체지향 프로그래밍 (0) | 2025.07.15 |
[Object] Chapter01. 객체, 설계 (2) | 2025.07.14 |