본문 바로가기
Study/Object

object 책임 할당하기

by 소고기 굽는 개발자 2023. 8. 8.

책임 할당하기

이전 장에서 데이터 중심의 설계는 데이터를 시작으로 설계를 진행하기에 객체간의 결합도가 높아져 객체지향적이지 못했습니다.
그리고 객체지향적이지 못한 시스템은 안전성과 유지보수의 악영향을 준다는 것을 알 수 있었습니다.

그렇기에 최대한 객체 지향적으로 개발을 하기위해 데이터 중심의 설계에서 책임 중심의 설계를 할 수 있도록 노력할 필요가 있습니다.

이번장에서는 객체지향적인 개발을 위한 패턴중 GRASP(General Responsibility Assignment Software Patterns)패턴을 배우며 어떻게 하면 좀 더 객체 지향적인 개발을 할 수 있을지 알아보고자 합니다.

책임 주도 설계를 향해

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

앞서 설명했지만 데이터 중심의 설계는 낮은 응집도와 높은 결합도라는 안좋은 결과를 가져옵니다. 이는 4장에서 충분히 설명했지만 객체와 데이터를 구분해 추측에 의한 설계를 진행하게되고 추측으로 만들어진 객체는 일어날 가능성을 모두 염두해 모든 객체들간의 접근을 허용하는 문제가 발생합니다.

그래서 먼저 데이터가 아닌 행동을 중심으로 설계를 진행할 필요가 있습니다. 행동 중심의 설계를 하면 필요한 객체들간의 접근을 허용하게 되고 객체들간의 책임이 적절해집니다.

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

책임 중심의 설계는 객체들간의 책임을 적절히 분산시키는데, 분산을 시킬 수 있는 이유는 협력을 기반으로 어떤 객체에게 책임을 맡겨야 하는지 알 수 있기 때문입니다.
이렇듯 할당된 책임의 품질은 협력에 얼마나 적합한가로 결정되는데 좋은 협력을 위해선 적절한 행동과 메시지 전송자를 매칭시키는 것이 중요합니다.

그리고 적절한 행동과 메시지 전송자를 매칭시키기 위해선 행동을 먼저 결정한 후에 객체를 선택해야합니다. 결국 객체 책임이 적절해지는 것은 필요한 만큼의 기능을 정의하는 것이고 필요한 만큼의 기능을 정의하는 것은 어떤 기능만 사용할지 행동을 정리하고 적용하는 과정을 통해 가능해집니다.

필요한 만큼의 기능만 정의된 객체는 객체들간의 행동을 필요한 만큼만 공유하게되고 이 과정속에서 자연스럽게 캡슐화가 이루어지게 됩니다.

책임 할당을 위한 GRASP 패턴

GRASP는 General Responsibility Assignment Software Patterns(일반적인 책임 할당을 위한 소프트웨어 패턴)의 약자로 객체에게 책임을 할당할 때 지침으로 삼을 수 있는 원칙들의 집합을 패턴형식으로 정리한 것입니다.

그리고 영화 예매 시스템을 통해 GRASP에 대한 내용을 설명하고자 합니다. 먼저 도메인 안에 존재하는 개념을 정리하는 것으로 패턴에 대한 설명을 진행하고자 합니다.

도메인 개념에서 출발하기

//P137 그림 5.1
그림을 통해 하나의 영화는 여러번 상영 될 수 있으며, 하나의 상영은 여러번 예약될 수 있다는 것을 알 수 있습니다.
또한 영화는 다수의 할인 조건을 가질 수 있으며, 할인 조건은 순번 조건과 기간 조건 중 하나를 선택할 수 있다는 것을 알 수 있습니다.
그리고 영화는 금액 할인과 비율 할인을 필요에 따라 선택할 수 있습니다.

이 과정에선 개념들의 의미와 관계가 완벽할 필요가 없고 단지 설계를 시작하기 위해 참고할 수 있는 개념들을 모았다는 것으로도 충분합니다.

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

책임 주도 설계 방식의 첫 단계는 애플리케이션이 제공해야 하는 기능을 애플리케이션의 책임으로 생각하는 것입니다.
이 책임을 애플리케이션에 대해 전송된 메시지로 간주하고 이 메시지를 책임질 첫번째 객체를 선택하는 것으로 설계를 시작합니다.

 

//P140 첫번째 그림
가장 먼저 사용자에게 제공해야하는 기능은 영화를 예매하는 것이고, 이 책임을 메시지로 표현하면 예매하라가 적절해 보입니다.
예매하라는 메시지를 수신 받기 위한 객체의 선택은 책임을 수행함에 있어 필요한 정보를 가장 많이 알고 있는 객체에게 할당하는 것이 가장 좋은 방법으로 보입니다.
그 객체로는 상영이 가장 적합해보입니다. 상영은 영화 정보, 상영 시간, 상영 순번 처럼 영화 예매에 필요한 정보를 알고 있기 때문입니다.

 

//P141 첫번째 그림
예매하라는 메시지를 완료하기 위해선 예매 가격을 계산하는 작업이 필요합니다. 예매 가격은 영화 한편의 가격을 계산한 금액에 예매 인원수를 곱한값으로 구할 수 있습니다. 그런데 상영은 영화에 대한 정보를 알 지 못함으로 외부 객체에 도움을 받아 영화에 대한 값을 계산해야 합니다.

다음은 영화에 대한 값을 계산하기 위해 가격을 계산하라라는 메시지가 나오게 됩니다. 그리고 가격을 계산하기 위한 정보를 가장 많이 알고 있는 객체가 영화임으로 영화가 가격을 계산하라는 책임을 할당하게 됩니다.

 

//P141 세번째 그림
다음은 영화 가격을 계산하기 전 영화가 할인이 가능한지 여부를 확인하기 위해 할인 여부를 판단하라는 메시지를 전송해야 합니다. 그리고 할인 여부를 확인하는 객체인 할인 조건 객체를 통해 할인 여부를 확인합니다.

다음은 영화가 할인 조건에 맞다면 영화의 가격을 비율 할인 혹은 고정 할인으로 선택해 반환하도록합니다.

지금까지 정보 전문가 패턴은 전송할 메시지를 수행함에 있어 해당 정보를 잘 알고 있는 전문가에게 할당하는 작업을 통해 객체들의 협력을 구성한다는 것을 확인했습니다.
그리고 이러한 작업 속에서 자율성이 높은 객체들로 구성된 협력 공동체를 구축되는 것을 확인할 수 있었습니다.

높은 응집도와 낮은 결합도

설계는 트레이드 오프 활동이며, 동일한 기능을 구현할 수 있는 무수히 많은 설계가 존재합니다. 이에 정보 전문가 패턴 이외에 다른 책임 할당 패턴들을 함께 고려할 필요가 있습니다.

에를 들어, 다음과 같은 설계에서 상영이 영화 할인 조건과 결합되는 협력을 보겠습니다.


// P142 그림 5.2
그림을 보면 상영이 할인 여부 조건을 판단하고 영화의 할인된 요금을 계산하게 됩니다. 하지만 그림 5.1의 도메인상으로는 영화와 할인된 요금이 이미 결합을 이루고 있기에 상영과 할인 조건을 결합하는 것은 좋은 방안이 아니게 됩니다.

기존의 상영의 책임은 예매를 생성하는 것이었는데, 만약 할인 조건과 협력하게 된다면 영화 요금계산과 관련된 책임을 일부 떠안게 됩니다. 즉 영화 요금과 관련해 변경사항이 생기면 상영도 변경을 해야된다는 것입니다.
이전까진 상영에서 예매만 신경쓰면 됐었지만 지금은 예매와 영화 요금을 동시에 신경쓰게 됩니다.
그렇기에 그림 5.1의 도메인처럼 객체의 협력을 구성하는 것이 높은 응집도와 낮은 결합도를 유지할 수 있는 방법이 됩니다.

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

영화 예매의 협력의 최종 결과물은 Reservation 인스턴스를 생성하는 것입니다. 이는 협력에 참여하는 어떤 객체에게는 Reservation 인스턴스를 생성할 책임을 할당해야한다는 것을 의미합니다.
GRAPS의 창조자 패턴은 이 같은 책임을 할당할 경우 사용할 수 있고 이를 통해 객체를 생성할 책임이 있는 창조자를 선택할 수 있게 합니다.
그리고 Reservation 인스턴스를 생성할 책임을 가지기에 적절한 객체는 Screening으로 보입니다.
이 과정에서 Screening과 Reservation은 서로 잘 알고 있는 상태에서 연결이 되는 것이기에 설계의 전체적인 결합도에 영향을 미치진 않습니다.

구현을 통한 검증

DiscountCondition 개선하기

현재 DiscountCondition의 구현은 조건이 추가되거나 변경이 생길 때 코드의 변경이 생길 수 있습니다.
하지만 하나의 기능을 변경했다고 해서 다른 유사한 기능에도 영향을 미치는 방식은 결코 좋은 코딩 방식은 아닐 것입니다.
예를 들어, isSatisfiedBySequence를 수정했는데 실행함에 있어 DiscountCondition 안에 다른 메서드들에게 영향을 미치게 되면 응집도가 낮다는 의미가 됩니다.
그렇기에 이러한 낮은 응집도를 초래하는 문제를 해결하기 위해 변경의 이유에 따라 클래스를 분리해야 합니다.

타입 분리하기

변경에 이유에 따라 클래스를 분리하면 SequenceCondition과 PeriodCondition으로 구분해 타입을 분리할 수 있습니다.
이제 SequenceCondition과 PeriodCondition의 변경이 발생하더라도 각 클래스에 영향을 미치진 않게 되었습니다.
이 과정에서 각 클래스의 응집도는 높아지는 효과를 보았습니다. 하지만 Movie에 SequenceCondition과 PeriodCondition이라는 새로운 의존성이 추가되어 DiscountCondition의 입장에선 응집도가 높아졌지만 변경과 캡슐화라는 관점에서 전체적인 설계의 품질이 낮아지는 문제가 발생하였습니다.

다형성을 통해 분리하기

이와 같은 문제를 해결하기 위해 Movie의 입장에서 SequenceCondition과 PeriodCondition을 바라보고자 합니다.
먼저 두 객체들은 단순히 할인 여부를 판단하는 동일한 책임을 가지고 있다는 것을 확인할 수 있었습니다.
그리고 두 객체의 행동이 같음으로 하나의 역할로 SequenceCondition과 PeriodCondition을 묶을 수 있습니다.
그 역할을 DiscountCondition 인터페이스의 isSatisfiedBy로 표현하게 되면 이전에 발생한 변경과 캡슐화에 취약해지는 문제점을 해결할 수 있게 됩니다.
이렇듯 문제를 해결할 수 있게 된 이유는 암시적인 타입인 DiscountCondition을 명시적인 클래스로 정의하고 타입의 분리를 통해 실행시점에 SequenceCondition과 PeriodCondition의 행동을 선택할 수 있게 만들었기 때문입니다.

변경으로부터 보호하기

Movie 객체의 입장에서 SequenceCondition과 PeriodCondition은 DiscountCondition안에서 존재를 감추고 있습니다.
이는 DiscountCondition의 추상화가 구체적인 클래스인 SequenceCondition과 PeriodCondition을 캡슐화시키기 때문입니다.
그렇기에 새로운 할인 조건 객체가 추가된다 하더라도 DiscountCondition의 추상화를 의존하게 되어 변경에 대한 캡슐화를 지킬 수 있게 됩니다.

Movie 클래스 개선하기 && 변경과 유연성

영화의 할인 금액을 적용하는 기능 또한 AmountDiscount와 PercentDiscount가 서로 다른 이유로 각 코드에 영향을 주게 됩니다.
그래서 응집도를 높이기 위해 Movie의 AmountDiscountMovie와 PercentDiscountMovie를 각각 클래스로 만듭니다.
그리고 Movie는 abstract class로 각 할인 정책의 공통 기능을 구현하도록 합니다.

하지만 할인 정책은 유사한 변경이 빈번히 일어남으로 코드를 유연성하게 만들 필요가 있습니다.
그러기 위해 기존의 할인 정책을 관리하던 abstract class Movie를 대신해 abstract class DiscountPolicy를 만들고 각 AmountDiscountMovie와 PercentDiscountMovie는 DiscountPolicy에서 상속합니다.
그리고 DiscountPolicy는 빈번하게 변경됨으로 Movie에서 changeDiscountPolicy 메서드를 이용해 쉽게 영화 할인 정책을 변경할 수 있도록 구현합니다.

책임 주도 설계의 대안

책임과 객체 사이의 돌파구를 찾기 위해 선택하는 방법은 최대한 빠르게 목적한 기능을 수행하는 코드를 작성해보는 것입니다.
그리고 코드에서 명확하게 드러나는 책임들을 올바른 위치로 이동시키다보면 객체에게 올바른 책임이 주어집니다.
그런데 여기서 주의할 점은 코드를 수정한 후에는 겉으로 드러나는 동작이 바뀌어선 안된다는 것입니다. 이와 같이 겉으로 들어나는 동작을 바꾸지 않은채 내부 구현을 변경하는 것을 리팩터링이라고 부릅니다.

메서드 응집도 && 자율적인 객체

만약 여러가지 기능을 가지고 있는 100-200줄의 긴 메서드가 존재한다면 아마 코드를 읽는 사람들은 내용을 이해하기가 어려울 것입니다.
그 이유는 하나의 메서드안에서 너무 많은 일을 처리하기 때문입니다.
그리고 이러한 메서드를 마이클 페더스는 몬스터 메서드라고 불렀습니다.

이런 가독성 없는 메서드를 조금 더 가독성 있게 만들기 위해선 메서드를 기능을 최소 단위로 쪼갤 필요가 있습니다.
메서드를 기능 단위로 쪼개 유의미한 이름을 붙히며 작업을 이어나간다면 당장은 응집도가 낮겠지만 객체간의 책임을 이동시키는데 있어 훨씬 유연하게 작업을 할 수 있게 됩니다.

객체간의 책임을 올바르게 전달한 과정속에서 객체간의 자율성이 보장되고 객체들의 책임이 명확히 주어지게 됩니다.

읽고 느낀 점

이번장의 요약을 하기에 앞서 4장의 데이터 중심의 설계가 가지는 단점으로 인해 객체 지향적 설계가 어려워진다는 내용을 알게 되었습니다.
그리고 전 너무 당연하게도 데이터 중심의 설계는 만악의 악이라는 생각을 했던 것 같습니다.
하지만 이번 5장의 글을 읽으면서 객체 지향 설계가 어렵다면 데이터 중심의 설계를 먼저 해보고 그 가운데 적절한 책임을 할당하는 과정을 통해 책임 주도 설계에 가까워 지라는 이번장의 마지막 말을 통해 제가 너무 완벽하게 책임 주도 설계만을 하려는 생각을 했던 것 같습니다.
이번 장을 끝내고 이전에 진행한 토이 프로젝트를 다시 돌아보며 어떻게 책임 주도 설계를 할 수 있을지 고민해보는 시간을 가져야겠습니다.