개발자는 기록이 답이다
오브젝트 5장 - 책임 할당하기(1) 본문
4장에서는 데이터 중시의 접근법을 취할 경우 직면하는 다양한 문제점을 살펴봤다.
데이터 중심의 설계는 행동보다 데이터를 먼저 결정하고 협력이라는 문맥을 벗어나 고립된 객체의 상태에 초점을 맞추기 대문에 캡슐화를 위반하기 쉽고, 요소들 사이의 결합도가 높아지며,코드를 변경하기 어려워진다.
따라서 책임에 초점을 맞춰야 한다.
책임에 초점을 맞춰서 설계할 때 직면하는 가장 큰 어려움은 어떤 객체에 어떤 책임을 할당하는지 결정하기 쉽지 않다는 것이다.
책임 할당 과정은 일종의 트레이드 오프 활동이다.
동일한 문제를 해결할 수 있는 다양한 책임 할당 방법이 존재하며, 어떤 방법이 최선인지는 상황과 문맥에 따라 달라진다. 따라서 올바른 책임을 할당하기 위해서는 다양한 관점에서 설계를 평가할 수 있어야 한다.
GRSAP 패턴이 책임 할당의 어려움을 해결하기 위한 답을 제시해줄 것입니다.
1. 책임 주도 설계를 향해
데이터 중심 설계에서 책임 중심의 설계로 전환
- 데이터보다 행동을 먼저 결정해라
- 협력이라는 문맥안에서 책임을 결정하라
데이터보다 행동을 먼저 결정하라
객체에게 중요한 것은 데이터가 아니라 외부에 제공하는 행동이다. 클라이언트의 관점에서 객체가 수행하는 행동이란 곧 객체의 책임을 의미한다. 객체는 협력에 참여하기 위해 존재하며 협력 안에서 하는 책임이 객체의 존재 가치를 증명한다.
너무 이른 시점에 데이터에 초점을 맞추면 객체의 캡슐화가 약화괴디 때문에 낮은 응집도와 높은 결합도를 가진 객체들로 넘쳐나게 된다. 그 결과로 얻게 되는 것은 변경에 취약한 설계다. 이를 해결하기 위해 필요한 것은 객체의 데이터에서 행동으로 무게 중심을 옮기는 기법이다.
객체를 설계하기 위한 질문의 순서를 바꿔야 한다.
- 데이터 중심 설계 : "이 객체가 포함해야 하는 데이터가 무엇인가?" → "데이터를 처리하는데 필요한 오퍼레이션은 무엇인가?"
- 책임 중심 설계 : "이 객체가 수행해야 하는 책임은 무엇인가?" → " 이 책임을 수행하는데 필요한 데이터는 무엇인가?"
즉, 책임 중심 설계에서는 객체의 행동, 즉 책임을 먼저 결정한 후에 객체의 상태를 결정한다.
협력이라는 문맨 안에서 책임을 결정하라
객체에게 할당된 책임의 품질은 협력에 적합한 정도로 결졍된다. 객체에게 할당된 책임이 협력에 어울리지 않는다면 그 책임은 나쁜것이다. 객체의 입장에서 책임이 조금 어색해보이더라도 협력에 적합하다면 그 책임은 좋은 것이다. 책임은 객체의 입장이 아니라 객체가 참여하는 협력에 적합해야 한다.
협력을 시작하는 주체는 메시지 전송자이기 때문에 협력에 적합한 책임이란 메시지 수신자가 아니라 메시지 전송자에게 적합한 책임을 의미한다. 다시 말해, 메시지를 전송하는 클라이언트의 의도에 적합한 책임을 할당해야 한다.
객체를 결정한 후 메시지를 선택하는 것이 아니라, 메시지를 결정한 후 객체를 선택해야 한다.
메시지가 존재하기 때문에 그 메시지를 처리할 객체가 필요한 것이다.
객체가 메시지를 선택하는 것이 아니라 메시지가 객체를 선택하게 해야 한다.
메시지가 클라이언트의 의도를 표현한다.
객체를 결정하기 전에 객체가 수신할 메시지를 먼저 결정한다.
클라이언트는 어떤 객체가 메시지를 수신할지 알지 못한다. 클라이언트는 단지 임의의 객체가 메시지를 수신할 것이라는 사실을 믿고 자신의 의도를 표현한 메시지를 전송할 뿐이다. 그리고 메시지를 수신하기로 결정된 객체는 메시지를 처리할 '책임'으르 할당받게 된다.
메시지를 먼저 결정하기 때문에 메시지 송신자는 메시지 수신자에 대한 어떠한 가정도 할 수없다. 메시지 전송자의 관점에서 메시지 수신자가 깔끔하게 캡슐화되는 것이다. 이처럼 처음부터 데이터에 집중하는 데이터 중심의 설계가 캡슐화에 취약한 반면 협력이라는 문맥 안에서 메시지에 집중하는 책임 중심의 설계는 캡슐화의 원리를 지키기가 훨씬 쉬워진다. 책임 중심의 설계가 응집도가 높고 결합도가 낮으며 변경하기 쉽다고 말하는 이유가 여기에 있다.
책임 주도 설계
아래는 3장에서 설명한 책임 주도 설계의 흐름을 다시 나열한 것이다.
- 시스템이 사용자에게 제공해야 하는 기능인 시스템 책임을 파악한다
- 시스템 책임을 더 작은 책임으로 분할한다
- 분할된 책임을 수행할 수 있는 적절한 객체 또는 역할을 찾아 책임을 할당한다
- 객체가 책임을 수행하는 도중 다른 객체의 동무이 필요한 경우 이를 책임질 적절한 객체 또는 역할을 찾는다
- 해당 객체 또는 역할에게 책임을 할당함으로써 두 객체가 협력하게 된다
책임 주도 설계의 핵심은 책임을 결정한 후에 책임을 수행할 객체를 결정하는 것이다. 그리고 협력에 참여하는 객체들의 책임이 어느정도 정리될 때까지는 객체의 내부 상태에 대해 관시믈 가지지 않는 것이다.
2. 책임 할당을 위한 GRASP 패턴
🚩 크레이그 라만(Craig Larman)이 패턴 형식으로 제안한 GRASP 패턴
(General Responsibility Assignment Software Pattern 일반적인 책임 할당을 위한 소프트웨어 패턴)
: 객체에게 책임을 할당할때 지침으로 삼을 수 있는 원칙의 집합
도메인 개념에서 출발하기
설계를 시작하기 전에 도메인에 대한 개략적인 모습을 그려보는 것이 유용하다. 도메인 안에는 무수히 많은 개념들이 존재하며 이 도메인 개념들을 책임 할당의 대상으로 사용하면 코드에 도메인의 모습을 투영하기가 좀 더 수월해진다. 따라서 어떤 책임을 할당해야 할 때 가장 먼저 고민해야 하는 유력한 후보는 바로 도메인 개념이다.
아래 그림은 영화 예매 시스템을 구성하는 도메인 개념과 개념 사이의 관계를 대략적으로 표현한 것이다.
하나의 영화는 여러번 상영될 수 있으며, 하나의 상영은 여러번 예약될 수 있다. 또한 영화는 다수의 할인 조건을 가질 수 있으며 할인 조건에는 순번, 기간 조건이 존재한다. 영화는 금액이나 비율에 따라 할인 될 수 있지만 동시에 두 가지 할인 정책을 적용할 수 없다.
설계를 시작하는 단계에서는 개념들의 의미와 관계가 정확하거나 완벽할 필요가 없다. 단지 우리에게는 출발점이 필요할 뿐이다. 이 단계에서는 책임을 할당받을 객체들의 종류와 관계에 대한 유용한 정보를 제공할 수 있다면 충분하다. 따라서 시작 시점에는 그림 처럼 설계를 시작하기 위해 참고할 수 있는 개념들이 모음 정도로 간주하라. 중요한 것은 설계를 시작하는 것이지 도메인 개념을 완벽하게 정리하는 것이 아니다. 도메인 개념을 정리하는데 너무 많은 시간을 들이지 말고 빠르게 설계와 구현을 진행하라.
🚩 올바른 도메인 모델은 존재하지 않는다.
위의 도메인 모델이 2장에서 설명한 도메인 모델과 약간 다르다. 2장에서는 할인 정책이라는 개념이 하나의 독립적인 개념으로 분리돼 있었지만, 위의 그림에서는 영화의 종류로 표현되어 있다. 두 도메인 모델 모두 올바른 구현을 이끌어낼 수 만 있다면 둘다 올바른 모데인 모델이다. 도메인 모델은 도메인을 개념적으로 표현한 것이지만 그 안에 포함된 개념과 관계는 구현의 기반이 돼야 한다. 이것은 도메인 모델이 구현을 염두에 두고 구조화되는 것이 바람직하다는 것을 의미한다.
정보 전문가에게 책임을 할당하라
책임 주도 설게 방식의 첫 단계는 애플리케이션이 제공해야 하는 기능을 애플리케이션의 책임으로 생각하는 것이다.
이 책임을 애플리케이션에 대해 전송된 메시지로 간주하고 이 메시지를 책임질 첫 번째 객체를 선택하는 것으로 설계를 시작한다.
사용자에게 제공해야 하는 기능은 영화를 예매하는 것이다.이를 책임으로 간주하면 애플리케이션은 영화를 예매할 책임이 있다고 말할 수 있다.
"메시지를 전송할 객체는 무엇을 원하는가?"
협력을 시작하는 객체는 미정이지만 이 객체가 원하는 것은 영화를 예매하는 것이라는게 분명하다
메세지를 결정했으면 메세지에 적합한 객체를 선택해야 한다
"메세지를 수신할 적합한 객체는 누구인가?"
이 질문에 답하기 위해서는 객체가 상태와 행동을 통합한 캡슐화의 단위라는 사실에 집중해야 한다. 객체는 자신의 상태를 스스로 처리하는 자율적인 존재여야 한다. 객체의 책임과 책임을 수행하는 데 필요한 상태는 동일한 객체 안에 존재해야 한다. 따라서 객체에게 책임을 할당하는 첫 번째 원칙은 책임을 수행할 정보를 알고 있는 객체에게 책임을 할당하는 것이다. GRAS에서는 이를 INFORMATION EXPORT(정보 전문가)패턴이라고 부른다.
🚩INFORMATION EXPORT패턴
책임을 객체에게 할당하는 일반적인 원리는 책임을 정보 전문가, 즉 책임을 수행하는데 필요한 정보를 가지고 있는 객체에게 할당하는 것이다. INFORMATION EXPORT패턴은 객체라 자율적인 존재여야 한다는 사실을 다시 한번 상기 시킨다. 정보를 알고 있는 객체만이 책임을 어떻게 수행할지 스스로 결정할 수 있기 때문이다. INFORMATION EXPORT패턴을 따르면 저옵와 행동을 최대한 가까운 곳에 위치시키기 때문에 캡슐화를 유지할 수 있다. 필요한 정보를 가진 객체들로 책임이 분산되기 때문에 더 응집력있고, 이해하기 쉬워진다. 따라서 높은 응집도가 가능하다. 결과적으로 결합도가 낮아져서 간결하고 유지보수하기 쉬운 시스템을 구축할 수 있다.
INFORMATION EXPORT패턴은 객체가 자신이 소유하고 있는 정보와 관련된 작업을 수행한다. 여기서 정보는 데이터와 다르다.
책임을 수행하는 객체가 정보를 '알고'있다고 해서 그 정보를 '저장'하고 있을 필요는 없다. 객체는 해당 정보를 제공할 수 있다는 다른 객체를 알고 있거나 필요한 정보를 계산해서 제공할 수 있다.
INFORMATION EXPORT패턴에 따르면 예매하는데 필요한 정보를 가장 많이 알고 있는 객체가 예매하라 메시지를 처리할 책임을 할당해야 한다. 상영은 영화에 대한 정보와 상영 시간, 상영 순번 처럼 영화 예매에 필요한 다양한 정보를 알고 있기 때문에 영화 예매를 위한 정보 전문자이다.
예매하라 메시지를 수신했을 때 Screening이 수행하는 작업의 흐름을 생각해보자. 외부의 인터페이스가 아닌 Screening의 내부로 들어가 메시지를 처리하기 위해 필요한 절차와 구현을구민해야 한다. 개락적인 수준에서 책임을 결정하는 단계이기 때문에 너무 세세하게 고민할 필요는 없다. 단지 Screening이 책임을 수행하는데 필요한 작업을 구상해보고 스스로 처리할 수 없는 작업이 무엇인지를 가릴 정도의 수준이면 된다.
만약 스스로 처리할 수없는 작업이 있다면 외부에 도움을 요청해야 한다. 이 요청이 외부로 전송해야 하는 새로운 메시지가 되고, 최종적으로 이 메시지가 새로운 객체의 책임으로 할당된다. 이 같은 연쇄적인 메시지 전송과 수신을 통해 협력 공동체가 구성되는 것이다.
예매하라 메시지를 완료하기 위해 여몌 가격을 계산하는 작업이 필요하다. 예매 가격을 영화 한편의 가격을 계산한 금애게 예매 인원 수를 곱한 값으로 구할 수 있다. 따라서 영화 한 편의 가격을 알아야한다. 하지만 Screening은 가격을 계산하는데 필요한 정보를 모르기 때문에 외부의 객체에게 도움을 요청해서 가격을 얻어야 한다.
영화 가격을 계산하는데 필요한 정보를 알고 있는 전문가는 당연히 영화(Movie)이고, 영화 가격을 계산할 책임을 갖게 된다.
이제 가격을 계산하기 위해 Movie가 어떤 작업을 해야 하는지 생각해보자. 요금을 계산하기 위해서는 먼저 영화가 할인 가능한지를 판단한 후 할인 정책에 따라 할인 요금을 제외한 금액을 계산하면 된다. 영화가 스스로 처리할 수 없는 일은 할인 조건에 따라 영화가 할인 가능한지 판단하는 것이다. 따라서 할인 여부를 판단하라 메시지를 전송해서 외부의 도움을 요청해야 한다.
할인 여부를 판단하는데 필요한 정보를 가장 많이 알고 있는 객체는 바로 할인 조건이다. DiscountCondition에게 책임을 할당하자
DiscountCondition은 자체적으로 할인 여부를 판단하는데 필요한 모든 정보를 알고 있기 때문에 외부의 도움 없이도 스스로 할인 여부를 판단할 수 있다. 따라서 DiscountCondition은 외부에 메시지를 전송하지 않는다.
Movie는 DiscountCondition에 전송한 할인 여부를 판단하라 메시지의 결과로 할인 가능 여부를 반환받는다.
DiscountCondition중에서 할인 가능한 조건이 하나라도 존재하면 금액 할인 정책/비율 할인 정책에 정해진 계산식을 따라 요금을 계산한 후 반환한다. 만약 할인 가능 조건이 존재하지 않는다면 영화의 기본 금액을 반환한다.
INFORMATION EXPORT패턴은 객체란 상태와 행동을 함께 가지는 단위라는 객체지향의 가장 기본적인 원리를 책임 할당의관점에서 표현한다.
높은 응집도와 낮은 결합도
설계는 트레이드 오프의 활동이다. 동일한 기능을 구현할 수 있는 무수히 많은 설계가 존재한다. 따라서 실제로 설계를 진행하다보면 몇 가지 설계 중에서 한 가지를 선택해야 하는 경우가 빈번하게 발생하다. 이 경우에는 올바른 책임 할당을 위해 INFORMATION EXPORT패턴 이외의 다른 책임 할당 패턴들을 함께 고려해야 한다.
예를 들어, 영화 예매 시스템에서는 할인 요금을 계산하기 위해 Movie가 DiscountCondition에 할인 여부를 판단하라 메시지를 전송한다. 그렇다면 이 설계의 대안으로 Movie대신 Screening이 직접 DiscountCondition과 협력하게 하는 것은 어떨까? 이를 위해서는 Screening이 DiscountCondition에게 할인 여부를 판단하라 메시지를 전송하고 반환받은 할인 여부를 Movie에게 전송하는 메시지 인자로 전달하도록 수정해야 한다. Movie는 전달된 할인 여부 값을 이용해 기본 금액인지, 할인 정책에 따라 할인 요금을 계산할지 결정할 것이다.
위 설계는 기능적인 측면만 보면 Movie와 DiscountCondition이 직접 상호작용하는 앞의 설계와 동일하다. 차이점이라면 DiscountCondition과 협력하는 객체가 Movie가 아니라 Screening이라는 것뿐이다. 따라서 기능적 측면에서 두 가지 방법 중 어떤 방법을 선택하더라도 차이가 없는 것 처럼 보인다. 그렇다면 왜 우리는 이 설계 대신 Movie가 DiscountCondition와 협력하는 방법을 선택한 것일까?
그 이유는 응집도와 결합도에 있다.
높은 응집도와 낮은 결합도는 객체에 책임을 할당 할때 항상 고려해야 하는 기본 원리이다. 책임을 할당할 수 있는 다양한 대안이 존재한다면 응집도와 결합도의 측면에서 더 나은 대안을 선택하는 것이 좋다. 다시 말해 두 협력 패턴 중에서 높은 응집도와 낮은 결합도를 얻을 수 있는 설계가 있다면 그 설계를 선택해야 한다.
GRASP에서는 이를 LOW COUPLING(낮은 결합도)패턴과 HIGH COHESION(높은 응집도)패턴이라고 부른다.
🚩 LOW COUPLING패턴
어떻게 하면 의존성을 낮추고 변화의 영향을 줄이며 재사용성을 증가 시킬 수 있을까? 설계의 전체적인 결합도가 낮게 유지되도록 책임을 할당하라. 낮은 결합도는 모든 설계 결정에서 염두에 둬야 하는 원리다. 다시 말해 설계 결정을 평가 할 때 적용할 수 있는 평가원리다. 현재의 책임 할당을 검토하거나 여러 설꼐 대안들이 있을 때 낮은 결합도를 유지할 수 있는 설계를 선택하라.
DiscountCondition이 Movie랑 Screening중 어떤 것과 협력하는게 좋을까? 해답의 실마리는 결합도에 있다.
맨 위의 도메인 개념 그림을 다시 살펴보자. 도메인 상으로 Movie는 DiscountCondition의 목록을 속성으로 포함하고 있다. Movie와 DiscountCondition는 이미 결합돼 있기 때문에 Movie를 DiscountCondition과 협력하게 하면 설계 전체적으로 결합도를 추가하지 않고도 협력을 완성할 수 있다.
하지만 Screening이 DiscountCondition와 협력할 경우 새로운 결합도가 추가되서 LOW COUPLING패턴을 위반한다.
🚩 HIGH COHESION 패턴
어떻게 복잡성을 관리할 수 있는 수준으로 유지할 것인가? 높은 응집도를 유지할 수 있게 책임을 할당하라.
낮은 결합도처럼 높은 응집도 역시 모든 설계 결정에서 염두에 둬야 할 원리다. 다시 말해 설계 결정을 평가할 때 적용할 수 있는 평기원리다. 현재의 책임 할당을 검토하고 있거나여러 설계 대안 중 하나를 선택해야 한다면 높은 응집도를 유지할 수 있는 설계를 선택하라.
Screening의 가장 중요한 책임은 예매를 생성하는 것이다. 만약 Screening과 DiscountCondition이 협력 해야 한다면 Screening은 영화 요금 계산과 관련된 책임 일부를 떠안아야 할 것이다. 이 경우 Screening은 DiscountCondition이 할인 여부를 판단할 수 있고, Movie가 할인 여부를 필요로 한다는 사실을 알고 있어야 한다.
다시 말해서 예매 요금을 계산하는 방식이 변경될 경우 Screening도 함께 변경해야 하는 것이다. 결과적으로 Screening과 DiscountCondition이 협력하게 되면 Screening은 서로 다른 이유로 변경되는 책임을 짊어지게 되므로 응집도가 낮아질 수 밖에 없다.
반면 Movie의 주된 책임은 영화 요금을 계산하는 것이다. 따라서 영화 요금을 계산하는데 필요한 할인 조건을 판단하기 위해 Movie가 DiscountCondition과 협력하는 것은 응집도에 아무런 해를 끼치지 않는다. 따라서 HIGH COHESION패턴의 관점에서 Movie가 DiscountCondition와 협력하는게 더 나은 설계 대안이다.
LOW COUPLING패턴과 HIGH COHESION패턴은 설계를 진행하면서 책임과 협력의 품질을 검토하는데 사용할 수 있는 중요한 평가 기준이다.
창조자에게 객체 생성 책임을 할당하라
영화 예매 협력의 최종 결과물은 Reservation인스턴스를 생성하는 것이다. 이것은 협력에 참여하는 어던 객체에게는 Reservation인스턴스를 생성할 책임을 할당해야 한다는 것이다. GRASP의 CREATOR(창조자)패턴은 이 같은 경우에 사용할 수 있는 책임 할당 패턴으로서 객체를 생성할 책임을 어떤 객체에게 할당할지에 대한 지침을 제공한다.
🚩 CREATOR패턴
객체 A를 생성해야 할 때 어떤 객체에게 객체 생성 책임을 할당해야 하는가? 아래 조건을 최대한 많이 만족하는 B에게 객체 생성 책임을 할당해야 한다.
☞ B가 A객체를 포함하거나 참조한다
☞ B가 A객체를 기록한다
☞ B가 A객체를 긴밀하게 사용한다
☞ B가 A 객체를 초기화하는데 필요한 데이터를 가지고 있다(이 경우 B는 A에 대한 정보 전문가이다)
CREATOR패턴의 의도는 어떤 방식으로든 생성되는 객체와 연결되거나 관련될 필요가 있는 객체에 해당 객체를 생성할 책임을 맡기는 것이다. 이미 결합돼 있는 객체에게 생성 책임을 할당하는 것은 설계의 전체적인 결합도에 영향을 미치지 앟는다. 결과적으로 CREATE패턴은 이미 존재하는 객체 사이의 관계를 이용하기 때문에 설계가 낮은 결합도를 유지할 수 있게 된다.
Reservation을 잘 알고 있거나, 긴밀하게 사용하거나 초기화에 필요한 데이터를 가지고 있는 객체는 바로 Screening이다. Screening은 예매 정보를 생성하는데 필요한 영화, 상영시간, 상영 순번등의 정보에 대한 전문가이며, 예매 요금을 계산하는데 필수적인 Movie도 알고 있다 따라서 Screening을 Reservationd의 CREATOR로 선택해야 한다.
이렇게 대략적으로나마 영화 예매에 필요한 책임을 객체들에게 할당했으면, 협력과 책임이 제대로 동작하는지 확인할 수 있도록 코드를 작성하고 실행해봐야 한다.
'기술 서적 > OOP' 카테고리의 다른 글
오브젝트 4장 - 설계 품질과 트레이드오프(2) (0) | 2023.12.12 |
---|---|
오브젝트 5장 - 책임 할당하기(2) (0) | 2023.12.09 |
오브젝트 4장 - 설계 품질과 트레이드오프(1) (1) | 2023.12.02 |
오브젝트 3장 - 역할, 책임, 협력(2) (0) | 2023.11.28 |
오브젝트 3장 - 역할, 책임, 협력(1) (0) | 2023.11.28 |