본문 바로가기
Book

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

by Renechoi 2023. 1. 15.
 
오브젝트
객체지향으로 향하는 첫걸음은 클래스가 아니라 객체를 바라보는 것에서부터 시작한다. 객체지향으로 향하는 두번째 걸음은 객체를 독립적인 존재가 아니라 기능을 구현하기 위해 협력하는 공동체의 존재로 바라보는 것이다. 세번째 걸음을 내디딜 수 있는지 여부는 협력에 참여하는 객체 들에게 얼마나 적절한 역할과 책임을 부여할 수 있느냐에 달려 있다. 객체지향의 마지막 걸음은 앞에서 설명한 개념들을 여러분이 사용하는 프로그래밍 언어라는 틀에 흐트러짐 없이 담아낼 수 있는 기술을 익히는 것이다. 《객체지향의 사실과 오해》가 첫번째 걸음과 두번째 걸음인 객체와 협력에 초점을 맞췄다면 《오브젝트: 코드로 이해하는 객체지향 설계》는 세번째와 네번째 걸음인 책임의 할당과 그 구현에 초점을 맞춘다. 이 책을 읽고 나면 객체에 적절한 역할과 책임을 부여하는 방법과 유연하면서도 요구사항에 적절한 협력을 설계하는 방법을 익히게 될 것이다. 나아가 프로그래밍 언어라는 도구를 이용해 객체지향의 개념과 원칙들을 오롯이 표현할 수 있는 방법 역시 익힐 수 있을 것이다. ★ 이 책에서 다루는 내용 ★ ◎ 역할, 책임, 협력에 기반해 객체지향 프로그램을 설계하고 구현하는 방법 ◎ 응집도와 결합도를 이용해 설계를 트레이드오프하는 방법 ◎ 설계를 유연하게 만드는 다양한 의존성 관리 기법 ◎ 타입 계층을 위한 상속과 코드 재사용을 위한 합성의 개념 ◎ 다양한 설계 원칙과 디자인 패턴
저자
조영호
출판
위키북스
출판일
2019.06.17

 

 

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

 


 

 

책임에 초점을 맞춰서 설계할 때 직면하는 가장 큰 어려움은 어떤 객체에게 어떤 책임을 할당할지를 결정하기가 쉽지 않다는 것이다. 책임 할당 과정은 일종의 트레이드오프 활동이다. 동일한 문제를 해결할 수 있는 다양한 책임 할당 방법이 존재하며, 어떤 방법이 최선인지는 상황과 문맥에 따라 달라진다. 따라서 올바른 책임을 할당하기 위해서는 다양한 관점에서 설계를 평가할 수 있어야 한다. 

- 133p 

 

데이터 중심의 설계에서 책임 중심의 설계로 전환하기 위해서는 다음의 두 가지 원칙을 따라야 한다. 

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

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

=> 객체의 책임과 협력에 초점을 맞추라는 것 

- 134p 

 

 

가장 기본적인 해결 방법은 객체를 설계하기 위한 질문의 순서를 바꾸는 것이다. 데이터 중심의 설계에서는 "이 객체가 포함해야 하는 데이터가 무엇인가"를 결정한 후에 "데이터를 처리하는 데 필요한 오퍼레이션은 무엇인가"를 결정한다. 반면 책임 중심의 설계에서는 "이 객체가 수행해야 하는 책임은 무엇인가"를 결정한 후에 "이 책임을 수행하는 데 필요한 데이터는 무엇인가"를 결정한다. 다시 말해 책임 중심의 설계에서는 객체의 행동, 즉 책임을 먼저 결정한 후에 객체의 상태를 결정한다는 것이다.

- 134p 

 

 

 

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

 

객체에게 할당된 책임의 품질은 협력에 적합한 정도로 결정된다. 객체에게 할당된 책임이 협력에 어울리지 않는다면 그 책임은 나쁜 것이다. 객체의 입장에서는 책임이 조금 어색해 보이더라도 협력에 적합하다면 그 책임은 좋은 것이다. 책임은 객체의 입장이 아니라 객체가 참여하는 협력에 적합해야 한다.

- 135p 

 

협력을 시작하는 주체는 메시지 전송자이기 때문에 협력에 적합한 책임이란 메시지 수신자가 아니라 메시지 전송자에게 적합한 책임을 의미한다. 다시 말해서 메시지를 전송하는 클라이언트의 의도에 적합한 책임을 할당해야 한다는 것이다.

- 135p 

 


 

contact program e.g. 

 

입력을 받아라 

입력을 의미 있는 커맨드로 해석해라 

커맨드를 처리해라

처리한 값을 출력해라 

 


 

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

 

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

 

협력을 시작하는 객체는 미정이지만 이 객체가 원하는 것은 분명해 보인다. 바로 영화를 예매하는 것이다. 따라서 메시지의 이름으로는 예매하라가 적절한 것 같다. 

 

1. 예매하라 -> 

 

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

 

INFORMATION EXPERT 패턴은 객체가 자신이 소유하고 있는 정보와 관련된 작업을 수행한다는 일반적인 직관을 표현한 것이다. 

 

어떤 방식이건 정보 전문가가 데이터를 반드시 저장하고 있을 필요는 없다는 사실을 이해하는 것이 중요하다. 

 

- 139p 

 

 

상영은 영화에 대한 정보와 상영 시간, 상영 순번처럼 영화 예매에 필요한 다양한 정보를 알고 있다. 따라서 영화 예매를 위한 정보 전문가다. 상영에게 예매를 위한 책임을 할당하자. 

 

예매해라 -> screening 

 

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

 

2. 가격을 계산하라 -> Movie 

 

3. 할인 여부를 판단하라 -> DiscountCondition 

 

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

 

- 140 ~ 141p 

 

이 설계의 대안으로 Movie 대신 Screening이 직접 DiscountCondition과 협력하게 하는 것은 어떨까? 이를 위해서는 Screening이 DiscountCondition에게 할인 여부를 판단하라 메시지를 전송하고 반환받은 할인 여부를 Movie에 전송하는 메시지의 인자로 전달하도록 수정해야 한다. 

 

위 설계는 기능적인 측면에서만 놓고 보면 Movie와 DiscountCondition이 직접 상호작용하는 앞의 설계와 동일하다. 차이점이라면 DiscountCondition과 협력하는 객체가 Movie가 아니라 Screening이라는 것뿐이다. 따라서 기능적인 측면에서는 두 가지 중 어떤 방법을 선택하더라도 차이가 없는 것처럼 보인다. 그렇다면 왜 우리는 이 설계 대신 Movie가 DiscountCondition과 협력하는 방법을 선택한 것일까? 

 

그 이유는 응집도와 결합도에 있다. 높은 응집도와 낮은 결합도는 객체에 책임을 할당할 때 항상 고려해야 하는 기본 원리다. 

 

-> LOW COUPLING (낮은 결합도) 패턴과 HIGH COHESION (높은 응집도) 패턴이라고 부른다. 

 

해답의 실마리는 결합도에 있다. 도메인 상으로 Movie는 DiscountCondition의 목록을 속성으로 포함하고 있다. Movie와 DiscountCondition은 이미 결합돼 있기 때문에 Movie를 DiscountCondition과 협력하게 하면 설계 전체적으로 결합도를 추가하지 않고도 협력을 완성할 수 있다. 

 

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

 

- 143p 

 

 

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

 - 144p 

 

 

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

영화 예매 협력의 최종 결과물은 Reservation 인스턴스를 생성하는 것이다. 이것은 협력에 참여하는 어떤 객체에게는 Reservation 인스턴스를 생성할 책임을 할당해야 한다는 것을 의미한다. GRASP의 CREATOR(창조자) 패턴은 이 같은 경우에 사용할 수 있는 책임 할당 패턴으로서 객체를 생성할 책임을 어떤 객체에게 할당할지에 대한 지침을 제공한다. 

 

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

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

- B가 A 객체를 기록한다

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

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

 

Screening은 예매 정보를 생성하는 데 필요한 영화, 상영 시간, 상영 순번 등의 정보에 대한 전문가이며, 예매 요금을 계산하는 데 필수적인 Movie도 알고 있다. 따라서 Screening을 Reservation의 CREATOR로 선택하는 것이 적절해 보인다. 

 

- 145p 

 


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

 

Screening을 구현하는 과정에서 Movie에 전송하는 메시지의 시그니처를 calculateMovieFee(Screening screening)으로 선언했다는 사실에 주목하라. 이 메시지는 수신자인 Movie가 아니라 송신자인 Screening의 의도를 표현한다. 여기서 중요한 점은 Screening이 Movie의 내부 구현에 대한 어떤 지식도 없이 전송할 메시지를 결정했다는 것이다. 이처럼 Movie의 구현을 고려하지 않고 필요한 메시지를 결정하면 Movie의 내부 구현을 깔끔하게 캡슐화할 수 있다.

- 147p 

 

코드를 통해 변경의 이유를 파악할 수 있는 첫 번째 방법은 인스턴스 변수가 초기화되는 시점을 살펴보는 것이다. 응집도가 높은 클래스는 인스턴스를 생성할 때 모든 속성을 함께 초기화한다. 반면 응집도가 낮은 클래스는 객체의 속성 중 일부만 초기화하고 일부는 초기화되지 않은 상태로 남겨둔다. 

 

public class DiscountCondition {
	private DiscountConditionType tpe;
    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);
     } 
     
     ...

 

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

 

코드를 통해 변경의 이유를 파악할 수 있는 두 번째 방법은 메서드들이 인스턴스 변수를 사용하는 방식을 살펴보는 것이다. 모든 메서드가 객체의 모든 속성을 사용한다면 클래스의 응집도는 높다고 볼 수 있다. 반면 메서드들이 사용하는 속성에 따라 그룹이 나뉜다면 클래스의 응집도가 낮다고 볼 수 있다. 

 

DiscountCondition의 isSatisfiedBySequence 메서드와 isSatisfiedByPeriod 메서드가 이 경우에 해당한다. isSatisfiedBySequence 메서드는 sequence는 사용하지만 dayOfWeek, startTime, endTime은 사용하지 않는다. 반대로 isSatisfiedByPeriod 메서드는 dayOfWeek, startTime, endTime은 사용하지만 sequence는 사용하지 않는다. 이 경우 클래스의 응집도를 높이기 위해서는 속성 그룹과 해당 그룹에 접근하는 메서드 그룹을 기준으로 코드를 분리해야 한다. 

 

- 153p 

 

Period Condtion과 Sequence Condition 분리 => Movie 클래스 안에서 목록을 따로 유지 

=> 새로운 문제 야기 

=> 결합도가 높아짐 

 

 

다형성을 통해 분리하기 

 

역할을 사용하면 객체의 구체적인 타입을 추상화할 수 있다. 

- > 추상클래스나 인터페이스 

역할을 대체할 클래스들 사이에서 구현을 공유해야 할 필요가 있다면 추상 클래스를 사용하면 된다. 구현을 공유할 필요 없이 역할을 대체하는 객체들의 책임만 정의하고 싶다면 인터페이스를 사용하면 된다. 

- 156p 

 

 

DiscountCondtion이라는 이름의 인터페이스를 이용해 역할 구현 

-> 이제 Movie는 협력하는 객체의 구체적인 타입을 몰라도 상관 없다. 

 

 

DiscountCondition이라는 역할이 Movie로부터 PeriodCondition과 SequenceCondition의 존재를 감춘다는 사실에 주목하라. DiscountCondition이라는 추상화가 구체적인 타입을 캡슐화한다. Movie의 관점에서 DiscountCondition의 타입이 캡슐화된다는 거은 새로운 DiscountCondition 타입을 추가하더라도 Movie가 영향을 받지 않는다는 것을 의미한다. Movie에 대한 어떤 수정도 필요 없다. 오직 DiscountCondition 인터페이스를 실체화하는 클래스를 추가하는 것으로 할인 조건의 종류를 확장할 수 있다. 

- 159p 

 

 

마이클 페더스는 이런 메서드를 몬스터 메서드라고 부른다.

- 168p 

 

 

어떤 메서드를 어떤 클래스로 이동시켜야 할까? 객체가 자율적인 존재여야 한다는 사실을 떠올리면 쉽게 답할 수 있을 것이다. 자신이 소유하고 있는 데이터를 자기 스스로 처리하도록 만드는 거이 자율적인 객체를 만드는 지름길이다. 따라서 메서드가 사용하는 데이터를 저장하고 있는 클래스로 메서드를 이동시키면 된다.

- 172p 

 

 

반응형