Object

[Object] Chapter05. 책임 할당하기

개발자 문문 2025. 8. 12. 10:57

오늘은 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)라고 부른다.
  • 메서드가 길다고 주석을 추가할게 아니라, 메서드를 작게 분해해서 응집도를 높여야 한다.
  • 목적이 명확한 메서드들로 구성되어 있다면,
  • 변경을 처리하기 위해 어떤 메서드를 수정해야 할 지 쉽게 판단할 수 있다.
  • 재사용하기도 쉽다.
  • 코드를 이해하기도 쉽다.