본문 바로가기

Software Engineering/OOAD

OOAD - 책임, 역할, 협력을 이용한 객체지향 설계

역할, 책임, 협력을 이용한 유연한 설계

이번 시간에는 OOP 설계 방법 (역할, 책임, 협력)에 대해 알아보도록 하겠습니다. 이 포스팅은 오브젝트 (코드로 이해하는 객체지향 설계)라는 책을 토대로 작성되었습니다.

 

 

코드

https://github.com/galid1/oop_study

아래의 설명들은 위의 코드를 토대로 설명을 하고 있습니다.

 

 

 

 

1. 시나리오

2. 에서 앞으로 설명드릴 역할, 책임, 협력이라는 단어들에 대한 효율적인 이해를 돕기 위해, 간단한 영화 예메 시스템시나리오를 살펴보도록 하겠습니다.

 

 

1.1 요구사항 분석

우선, 영화 예매 시스템에서 빠질 수 없는 영화에 대한 요구사항을 분석하도록 하겠습니다.

 

영화

영화는 제목, 상영시간(running time), 가격 정보 등의 영화의 기본적인 정보를 가집니다.

 

상영

상영은 실제 관람객이 관람하는 사건을 나타내며, 상영일자(date), 시간, 순번을 가집니다.

 

 

 

다음은 영화의 관람료에 대한 할인을 결정하는 부분입니다. 보통의 경우 이는 할인 조건할인 정책이라는 2가지 규칙에의해 정해집니다.

 

할인

할인 조건

할인 조건의 경우 할인 여부를 결정합니다. 이는 다시 순서 조건기간 조건으로 분류됩니다. 순서 조건은 상영 순번을 이용해 할인여부를 결정합니다. 기간 조건은 상영 시작 시간을 통해 할인 여부를 결정합니다. 기간 조건의 경우 요일, 시작 시간, 종료 시간의 세부분으로 구성되며, 영화의 상영 시작시간이 해당 기간에 포함될 경우 요금할인이 됩니다.

 

할인 정책

할인 정책의 경우 할인 금액을 결정합니다. 할인 정책에는 금액 할인, 비율 할인 정책이 존재합니다. 금액할인은 특정 금액을 할인하며, 비율 할인은 일정 비율을 할인하는 정책입니다. 영화 별로 하나의 할인 정책만 할당할 수 있습니다.

 

할인 조건의 경우 순번 조건 기간 조건을 혼합하여 사용이 가능하기도하고, 할인 정책의 경우 아예 적용하지 않을 수도 있습니다.

 

 

 

1.2 클래스 구조

위의 요구사항을 토대로 만들어진 개념적 클래스 구조입니다. 여러번 예매가 가능하며, 하나의 예매에는 하나의 상영이 담겨있습니다. 또한 하나의 상영은 하나의 영화를 가지고, 그 영화에는 할인 정책이 포함될 수도, 아닐 수도 있습니다. 할인정책이 존재한다면, 하나 이상의 할인 조건을 가집니다.

 

 

 

* 메시지와 메소드 그리고 다형성

객체지향 설계를 알아보기전, 메시지와 메소드 그리고 다형성에 대해서 간단히 짚고 넘어가는 것이 좋을것 같습니다. 우선 메시지와 메소드는 전혀 다른 말입니다. 메시지는 어떤 객체가 수행할 수있는 책임(행동)을 추상화한 것이며, 메소드는 메시지를 통해 실제로 실행되는 행동을 말합니다.

 

위 그림에서 movie는 DiscountPolicy에게 calculateDiscountAmount 메시지를 전송하게 됩니다. 그렇다면 이것을 통해 실행되는 메소드의 내용은 무엇일까요? 정답은 알 수 없다 입니다. 왜냐하면, 위의 메시지를 통해 실행되는 메소드는 실제 DiscountPolicy를 상속받은 객체가 무엇인지 결정되어야만 즉, 런타임에만 알 수 있기 때문입니다.

 

이를 다형성이라고 일컫습니다. 다형성을 한마디로 정의하면 동일한 메시지를 수신했을때 객체의 타입에 따라 다르게 응답할수 있는 능력을 말합니다.

다형성을 구현하는 방법은 다양하지만, 이들 모두의 공통점은 실행될 메소드가 컴파일 타임이 아닌 런타임에 결정된다는 것 입니다. 다시 말해, 메시지와 메소드가 실행시간에 바인딩 됩니다. 이를 지연 바인딩 또는 동적 바인딩이라고 일컫습니다.

 

 

 

 

2. 역할, 책임, 협력을 이용한 유연한 설계

2.1 협력

객체지향 세계에서는 특정한 책임과 역할을 가진 객체들의 협력을 통해서 프로그램이 설계 및 구축이 됩니다. 위 그림은 영화 예메라는 도메인을 완성하기 위해 협력하는 객체들의 상호작용중 하나를 표현한것 입니다.

 

각각의 객체들은 협력을 위해서 서로에게 메시지를 전송합니다. 위 그림에서 ScreeningMovie에게 영화 요금 계산 메시지를 전송하는 이유는 Movie가 요금 계산을 위한 정보들을 잘 알고있는 객체이기 때문입니다.

반대로 Screening에서 영화 요금을 계산한다고 가정한다면, Movie의 인스턴스 변수들(fee, discountPolicy)에 접근해야 할 것이고 이는, Movie의 내부구현에 Screening이 강하게 결합됨을 의미합니다.

달리말해 Movie 객체의 자율성이 훼손되게됩니다. 객체란 자신의 상태를 직접 관리하고 스스로의 결정에 따라 행동합니다. 하지만 Screening에서 영화요금을 계산하게된다면 Movie가 수동적인 객체가 됩니다.

 

결국 객체를 자율적으로 만들기 위해서는 내부의 구현을 캡슐화해야합니다. 그리고 이 자율적인 객체를 사용하기 위해 적절한 메시지를 전송하면 됩니다. 이를 통해 변경이 수월한 설계가 만들어집니다.

 

객체의 행동을 결정하는 방법

객체란 상태와 행동을 함께 캡슐화하는 실행단위 입니다. 그렇다면 이 객체가 가질 상태와 행동을 어떻게 결정하게 될까요? 원초적인 개념으로 접근을 해보겠습니다. 객체가 필요하다는 것은 어떤 의미일까요? 어떤 협력에 그 객체가 참여를 하고 있다는 의미입니다. 즉, 협력에 필요한 적절한 행동을 가지고 있다는 의미일 것입니다.

결론적으로, 객체의 행동을 결정하는 것은 객체가 참여하는 협력입니다. 객체가 참여하는 협력이 바뀐다면 행동역시 그에 맞게 바뀌어야 합니다. 마치 DDD에서 같은 엔티티라도 바운디드 컨텍스트에 따라 그에 맞는 다른 이름으로 결정되는것과 비슷한 이치입니다.

예를들어, Movie에는 calculateMovieFee()라는 행동이 존재합니다. 이는 영화 예매 시스템이라는 협력에 맞추어 Movie의 행동이 결정된 것입니다. 보통의 경우 사람들은 영화를 상영하는 장면을 상상할 것이고 그에 맞추어 Movie는 play라는 행동을 가질 것으로 예상이 될것입니다. 하지만 영화 예메 시스템에서는 Movie는 Screening에 포함된 단지 영화의 정보를 나타내는 객체입니다. 다시한번 말씀드리자면, 객체의 행동을 결정하는 것은 그 객체가 속한 협력입니다.

 

객체의 상태를 결정하는 방법

결론부터 말씀드리자면, 객체의 상태를 결정하는 것은 객체의 행동입니다. 객체는 자율적인 존재여야 한다고 앞서 말씀드렸습니다. 다시 말해 객체가 자율적이기를 원한다면 자신이 가진 행동에 필요한 상태를 가지고 있어야 함을 의미합니다. 따라서 객체의 상태를 결정하는 것은 객체가 가져야 하는 행동에 의해 결정됩니다.

 

 

 

2.2 책임

앞서 설계를 위해 필요한 문맥인 협력을 갖춰졌다고 가정하겠습니다. 우리가 다음으로 해야할 일은 협력에 필요한 행동을 할 수 있는 적절한 객체를 찾는 일입니다. 책임이란, 객체가 유지해야하는 정보와 행동에 대해 서술한 문장을 말합니다. 즉, 책임은 하는 것, 아는 것 으로 구성됩니다.

 

영화 예매 시스템에서 Screening의 책임은 영화를 예매하는 것입니다. 이것은 하는 것과 관련된 책임입니다. 또한 상영할 Movie를 알아야합니다. 이는 아는 것과 관련된 책임입니다.

 

이러한 책임은 협력에 의해 결정됩니다. 객체에게 할당할 책임을 결정할 문맥을 제공하기 때문입니다.

 

 

책임할당과 설계과정

자율적인 객체를 만들기 위해서는 앞서 말씀드렸듯이, 책임을 수행하기 위한 정보를 잘 알고있는 객체에게 책임을 할당하는 것입니다. 이를 정보 전문가 패턴이라고 말합니다. 일상생활에서도 그렇듯 우리는 어떤 도움이 필요할때에는 그 분야의 전문가에게 도움을 청합니다. 객체 역시 협력에서 필요한 행동에대한 지식과 방법을 잘알고 있는 객체에게 도움을 요청합니다.

 

따라서 책임할당을 위해서는 먼저 협력이라는 문맥이 결정되어야 합니다. 영화 예매시스템을 예로들어 책임을 할당하는 과정을 살펴보겠습니다.

 

영화를 예매하기 위해서 예메하라라는 메시지가 필요할 것같습니다. 그렇다면 영화를 예매할 책임을 어떤 객체에게 할당하는것이 좋을까요? 아마도 영화를 예매하기 위한 정보를 모두 알고있는 전문가에게 할당하면 될것 같습니다.

 

바로 Screening이라는 객체가 될것 같습니다. Screening은 예매를 위해 필요한 정보인, 상영 시작 시간과 영화에 대한 정보를 모두 알고있기 때문입니다. 하지만 문제가 있습니다. 예매를 위해서는 가격을 계산하는 방법을 알아야 합니다. 하지만 Screening은 가격을 계산하는 방법을 알고 있지 않습니다.

 

다시 가격을 계산하라라는 새로운 메시지가 필요할것 같습니다. 이 역시 같은 방법으로 가격을 계산하기 위한 정보를 모두 알고 있는 정보 전문가에게 책임을 할당하면 될 것같습니다.

 

가격 계산을 위해 필요한 할인 정책, 영화 가격을 알고있는 Movie가 정보 전문가가 될것 같습니다. 하지만 Movie는 할인 요금을 계산하기 위한 정보전문가가 아니므로 할인요금을 계산하라라는 새로운 메시지가 필요할것이고 다시 정보전문가에게 책임을 할당할 것입니다.

 

위와같이 객체지향 설계는 협력을 통해 문맥을 결정하고 문맥에 필요한 메시지들을 찾고 그 메시지를 처리하는 책임을 적절한 객체에게 할당하는 과정을 반복하며 이루어집니다.

 

 

 

메시지가 객체를 결정

위 과정에서 우리는 하나의 큰 사실을 알 수 있습니다. 바로 객체가 메시지를 선택하는 것이 아니라, 메시지를 통해 적절한 객체를 선택했다는 것 입니다. 이를 통해 우리는 최소한의 인터페이스추상적인 인터페이스를 갖추게 됩니다. 필요한 메시지가 나타나기 전까지 퍼플릭 인터페이스에 어떠한 것도 추가하지 않았기 때문에 꼭 필요한 크기의 인터페이스를 가질수 있던 것입니다. 또 무엇을 하는지를 나타내지만 어떻게 수행하는지를 나타내지 않음으로써 낮은 결합도를 가진 설계를 할수 있었습니다.

 

 

 

2.3 역할

객체가 특정한 협력안에서 수행하는 책임의 집합을 역할이라고 합니다. 영화예매 협력안에서, 예매하라라는 메시지를 처리할 객체로 Screening을 선택했었습니다. 하나의 과정처럼 보이는 이 책임할당 과정은 사실 두개의 단계가 하나로 합쳐진것입니다. 첫번째는 영화를 예매할 수 있는 적절한 역할을 찾는것이고, 두번째로는 해당 역할을 수행할 객체로 Screening 객체를 선택하는 것입니다.

 

그렇다면 어떤 이유로 역할이라는 개념을 통해서 설계과정을 더 복잡하게 만드는 것일까요? 그것은 역할을 통해 유연하고 재사용 가능한 협력을 얻을 수 있기 때문입니다.

 

예를 들어 할인 요금을 계산하라 라는 메시지에 응답할 수 있는 객체는 AmountDiscountPolicyPercentDiscountPolicy가 존재합니다. 그렇다면 이런 경우 두 객체가 참여하는 협력을 개별적으로 2개를 만들어야 할까요?? 만약 두개의 협력을 만들어 낸다면 대부분의 코드가 중복이 되고 말것입니다.

위와 같은 문제는 책임의 관점에서 바라보면 해결할 수 있습니다. 책임의 관점에서 본다면, AmountDiscountPolicy, PercentDiscountPolicy두 객체 모두 할인 요금을 계산하는 동일한 책임을 수행하는 것을 알 수 있습니다. 따라서 할인 요금을 계산하라라는 메시지를 응답할 수있는 대표자를 생각한다면 두 협력을 하나로 통합 할 수 있습니다. 이러한 대표자를 역할 이라고 합니다.

 

따라서 이러한 여러 구체적인 객체를 포괄하는 역할에 추상적인 이름을 부여해야 합니다. DiscountPolicy가 적절해 보입니다. 요점은 동일한 책임을 수행하는 것들을 역할을 기반으로 하나로 통합할 수 있다는 것입니다. 이를 통해 불필요한 코드를 제거할 수 있습니다. 더욱 좋은 소식은 유연한 협력이 되어, 할인 요금 계산방법이 달라지더라도 새로운 협력을 추가할 필요가 없어졌다는 것입니다.