본문 바로가기

IT/오브젝트

[오브젝트] 5장 책임 할당하기

안녕하세요 남갯입니다

 

오늘은 오브젝트 책임 할당하기 부분입니다.

 

일전에 봤던 코드들

4장에서 데이터 중심의 설계를 통해 작성한 코드가 있었습니다.

책임에 중점을 맞추지 않다보니 결합도가 올라가고 캡슐화가 잘 안되다보니 응집도도 내려가는 결과가 발생했습니다.

2장에서 책임 중심의 코드와 함께 4장에서 데이터 중심의 코드도 봤습니다. 

이번에는 책임할당하기 편을 리뷰해보려고 합니다.

 

책임의 할당이란?

책임에 초점을 맞춰 설계할때 가장 큰 어려움은 어떤 객체에 어떤 책임을 할당할지 결정하기 어렵다는점입니다.

저도 객체지향.. 객체지향.. 어떤 책임을 할당할지 결정하기 쉽지 않았습니다. 책임의 할당과정은 트레이드 오프활동이다.

 

GRASP 패턴

GRASP 패턴은 책임할당의 어려움을 해결하기 위한 답을 제시해줄것이다.

GRASP 패턴을 이해하면 응집도 , 결합도, 캡술화 같은 다양한 기준에 따라 책임을 할당하고 결과를 트레이드 오프하는 기준을 배울것이다.

 

데이터중심 -> 책임중심 설계로 전환하기 위한 원칙

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

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

 

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

객체에게 중요한것은 데이터가 아니라 외부에 제공하는 행동이다.  객체지향에 갓 입문한 사람들이 가장 많이 저지르는 실수는 바로 객체의 행동이 아니라 데이터에 초점을 맞추는것이다.

따라서 우리에게 필요한것은 객체의 데이터에서 행동으로 무게중심을 옮기기 위한 기법이다.

 

데이터중심 설계

"이 객체가 포함되어야 하는 데이터는 무엇인가?" -> "데이터를 처리하기 위한 오퍼레이션은 무엇인가?"

책임중심 설계

"이 객체가 수행해야 하는 책임은 무엇인가?" -> "이 책임을 수행하는데 필요한 데이터는 무엇인가?"

 

 

협력에 적합한 책임을 수확하기 위해서는 객체를 결정한 후에 메세지를 선택하는것이 아니라 메세지를 결정한 후 객체를 선택해야한다. 메세지가 존재하기 때문에 그 메세지를 처리할 객체가 필요한 것이다. 객체가 메세지를 선택하는 것이

아니라 메세지가 객체를 선택하게 해야한다.

즉 여기서 중요한거는 메세지를 먼저 선택하기 때문에, 메세지 송신자는 메세지 수신자에 대한 어떤 가정도 할 수 없다.

 

GRASP패턴

크레이그 라만이 패턴형식으로 제안한 패턴이다.

General Responsibility Assignment software Pattern (일반적인 책임 할당을 위한 소프트웨어 패턴)

 

단계

1. 도메인 개념에서 출발하기

설계를 시작하기 전에 도메인에 대한 개략적인 모습을 그려 보는 것이 유용하다. 도메인 안에는 무수히 많은 개념들이

존재하며 이 도메인 개념들을 책임 할당의 대상으로 사용하면 코드에 도메인 모습을 투영하기가 좀 더 수월해진다.

이단계는 단순히 책임을 할당받을 객체들의 종류와 관계에 대한 유용한 정보를 제공할 수 있으면 된다.

 

2. 정보 전문가에데 책임을 할당하라

어플리케이션이 제공해야 하는 기능을 어플리케이션의 책임으로 생각하는것. 우리가 영화예매 시스템을 예시로 들었으니 영화예매 시스템은 "사용자에게 영화를 예매하는 기능을 제공하는것" 이다.

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

객체는 모르지만 메세지는 "예매하라" 라는 이름이 적당하다.

 

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

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

이 패턴을 통해 객체는 자율적인 존재여야하고 그 객체만이 스스로 책임을 어떻게 수행할지 결정할 수 있다.

 

따라서 위에서 정한 메세지인 "예매하라" 라는 메세지는 "상영(Screening)" 이라는 도메인 개념이 적합하다. "상영"은 영화예매를 하기 위한 상영시간, 상영순번 처럼 영화 예매를 위한 정보전문가이다. 

 

책임 할당과정

"예매하라"라는 도메인의 메세지를 완료하기 위해서는 예매 가격을 계산하는 작업이 필요하다.

예매가격은 영화 한편의 가격을 계산한 금액에 예매 인원수를 곱한 값으로 구할 수 있다. 

"상영(Screening)"은 모르기때문에 외부의 객체에게 도움을 요청해서 가격을 얻어야한다.

"계산하라" 라는 메세지를 수신하기 적당한 객체는 "Movie"가 될것이다. 영화 가격을 계산할 책임을 지게 된다.

"할인 요금을 계산하라" 라는 책임을 외부의 객체에게 도움을 요청해서 가격을 얻어야한다.

"영화(Movie)"은 모르기 때문기 또한 외부의 객체에게 도움을 요청해서 가격을 얻어야한다.

따라서 "할인여부(DiscountCondition)"을 통해 할인 책임을 할당한다.

 

 

높은 응집도와 낮은 결합도

설계 자체는 트레이드 오프 활동이다.

동일한 기능을 구현할 수 있는 무수히 많은 설계가 존재한다.

 

 

 

이렇게 설계도 가능하다.

 

우리는 여기서 설계를 할때 높은 응집도와 낮은 결합도를 얻을 수 있는 설계를 선택해야한다.

 

결합도관점

이제 우리는 어떤 설계가 더 낮은 결합도를 갖는지를 보면된다.

도메인상 에서 봤을때, Movie는 DiscountCondition의 목록을 속성으로 포함하고 있다. Movie와 DiscountCondition은

이미 결합되어 있기 때문에 이 둘을 협력하면 결합도를 다른 객체와 추가하지 않고 협력을 완성 가능하다

하지만 Screening과의 협력할 경우 Screening과 DiscountCondition 사이에 새로운 결합도가 추가된다.

다라서 낮은 결합도 관점에서는 전자의 설계가 더 나은 설계인 응셈 이다.

 

응집도관점

Screeing의 책임은 예매를 생성이지만 Screening이 DiscountConditin과 협력해야 한다면 요금 계산의 책임의 일부를

떠안아야 한다. 따라서 Movie가 할인여부를 필요로 한다는 사실 역시 알고 있어야한다.

따라서 응집도 관점에서도 전자의 설계가 낫다.

 

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

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

객체 A를 생성해야할때 어떤 객체에게 객체 생성 책임을 할당해야하는가?

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

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

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

4. B가 A객체를 초기화하는데 필요한 데이터를 가지고 있다. (B는 A에 대한 정보전문가다)

 

위의 조건이 많은 객체에게 생성 책임을 할당하라.

 

따라서 Reservation을 잘 알고있거나 긴밀하게 사용하고 초기화에 필요한 데이터를 가지고 있는것은 Screening이다.

따라서 

 

 

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

또한 책임이 결정되었으므로 책임을 수행하는 데 필요한 인스턴스 변수를 결정해야한다. Screening은 상영시간과 상영 순번을 인스턴스 변수로 포함한다.

 

public class Screening {
    private Movie movie;
    private int sequence;
    private LocalDateTime whenScreend;
    
    public Reservation reseve(Customer customer, int audienceCount){
        
    }
}

 

계산하라라는 메세지를 전송해서 계산된 영화요금을 반환받아야한다.

 

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

 

송신자 Screeing의 의도를 표현한다.

Screening이 Movie의 내부구현에 대한 어떤 지식도 없이 전송할 메세지를 결정했다는 것이다.

따라서 Screening과 Movie를 연결하는 유일한 열결고리는 메세지 뿐이다. 따라서 메세지가 변경되지 않은 한

Movie에 어떤 수정을 가하더라도 Screening에는 영향을 미치지 않는다.

 

할인정책은 Movie의 일부로 구현되어 있기때문에 할인 비율과 할인금액을 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
    }

 

DiscountCondition이 변경 이유는 세가지를 갖고있다.

- 새로운 할인 조건추가

- 순번 조건을 판단하는 로직 추가

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

 

하나이상의 변경의 이유를 가지기 때문에 응집도가 낮다.

 

코드를 통해 변경의 이유를 파악할 수 있는

첫번째 방법은 인스턴스 변수가 초기화 되는 시점을 살펴보는것이다.

응집도가 높은 클래스는 인스턴스를 생성할때 모든 속성을 함께 초기화 한다.

클래스 속성이 서로다른 시점에 초기화 되거나 일부만 초기화 된다는것은 응집도가 낮다는 증거다. 함께 초기화 되는 속성을 기준으로 코드를 분리해야 한다.

 

두번째 방법은 메서드들이 인스턴스 변수를 사용하는 방식을 살펴보는 것이다.

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

 

클래스의 응집도를 높이기 위해서는 속성그룹과 해당 그룹에 접근하는 메서드 그룹을 기준으로 코드를 분리해야한다.

 

클래스의 응집도 판단

1. 클래스가 하나 이상의 이유로 변경되야 한다면 응집도가 낮은것이다. 변경이유를 기준으로 클래스를 분리하라.

2. 클래스의 인스턴스를 초기화 하는 시점에 경우에 따라 서로 다른 속성들을 초기화 하고있다면 응집도가 낮은것이다.

초기화 되는 속성의 그룹을 기준으로 클래스를 분리하라.

3. 메서드 그룹이 속성 그룹을 사용하는지 여부로 나뉜다면 응집도가 낮은 것이다. 이들 그룹을 기준으로 클래스를 분리하라.

 

타입 분리하기

DiscountCondition의 순번 조건과 기간조건이라는 두 개의 독립적인 타입이 하나의 클래스 안에 공존하고 있다는 점이다. (SequenceCondition , DiscountCondition)

그래서 두개의 타입으로 분리해야한다. 클래스를 분리하면 문제점들이 모두 해결된다.

클래스를 분리 한 후에 두개의 클래스로 증가하면서 Movie 인스턴스는 두개의 클래스(SequenceCondition , DiscountCondition)이라는 두개의 서로 다른 클래스의 인스턴스 모두와 협력할 수 있어야한다.

 

리스트를 두개를 생성할경우 둘 다의 클래스에 결합된다.

 

 

다형성을 통해 분리하기

Movie 입장에서 보면 둘다 차이가 없기 떄문에 다형성을 이용해서 동일한 책임을 만들어야 한다.

객체의 타입에 따라 변하는 행동이 있다면 타입을 분리하고 변화하는 행동을 각 타입의 책임으로 할당하는것을

"다형성 패턴" 이라고 부른다.

 

변경으로부터 보호하기

두 클래스(SequenceCondition , DiscountCondition)는 다른 이유로 변경된다.

 

변경과 유연성

개발자로서 변경에 대비하는 방법 두가지

1. 코드를 이해하고 수정하기 쉽도록 최대한 단순하게 설계하는것이다.

2. 코드를 수정하지 않고도 변경을 수용할 수 있도록 코드를 더 유연하게 만드는것이다.

 

책임주도 설계 방법이 익숙하지 않다면 일단 데이터 중심으로 구현한 뒤 이를 리펙터링을 통해 유사한 결과를 얻을 수 있다는 점이다. 처음부터 책임 주도 설계방법을 따르는것보다, 동작하는 코드를 작성한 후 리펙터링 하는것이 더 훌륭한 결과물을 낳을수 있다.