좋은 설계를 위한 역할, 책임, 협력
객체지향 패러다임 관점의 핵심인 역할, 책임, 협력에 대해 알아보자
“아 설계를 좀 더 제대로 할걸…” 어디선가 많이 들어본 말이다. 구현하기 전 설계가 중요하다는 말을 많이 들어봤을 것이다. 하지만 어떻게 좋은 설계를 할 수 있을까..? 이번에 역할, 책임, 협력을 통해 한번 알아보자.
애플리케이션의 기능을 구현하기 위해 어떤 협력이 필요하고 그 협력을 위해 어떤 책임과, 역할이 필요한지 충분히 생각하지 않고 바로 구현에 들어가 버리면 변경하기 어렵고 유연하지 못한 구조의 코드를 만들 수 있다.
실제로 우테코 미션을 진행할 때 제대로 설계를 해놓지 않고 들어가 나중에 엄청난 대공사를 거쳤던 경험을 한 적이 있었다… 작은 규모의 미션을 할 때도 이 정도였는데 엄청 큰 서비스였다면 매우 끔찍했을 것이다.
우선 협력과 책임, 역할의 의미는 다음과 같다.
- 협력: 객체들이 애플리케이션의 기능을 구현하기 위해 수행하는 상호작용
- 책임: 객체가 협력에 참여하기 위해 수행하는 로직
- 역할: 객체의 협력안에서 수행하는 책임들이 모여 객체가 수행하는 역할
협력(Collaboration)
객체는 독립적인 존재가 아니라 애플리케이션이라는 큰 목표를 만들기 위해 다른 객체와 협력하는 사회적인 존재이다. 두 객체 사이의 협력은 하나의 객체가 다른 객체에게 메시지 전송을 통해 자신의 요청을 전송한다. 그리고 메시지를 수신한 객체는 메서드를 실행해 요청에 응답한다.
외부의 객체는 오직 메시지만 전송할 수 있으며 메시지를 어떻게 처리할지는 수신한 객체가 직접 결정한다. 이것은 객체가 능동적으로 일을 처리할 수 있는 자율적 존재라는 걸 알 수 있다. 자율적인 객체는 자신에게 할당된 책임을 수행하던 중 외부의 도움이 필요한 경우 적절한 객체에게 메시지를 전송해 협력을 요청한다. 예매 요금을 계산하기 위한 Screening(상영)과 Movie(영화)의 협력 관계를 보면 Screening은 Movie에 calculateMovie 메시지를 전송해 요금 계산을 요청할 수 있다.
협력이 설계를 위한 문맥(Context)을 결정
객체가 가질 수 있는 상태와 행동을 어떻게 결정할 수 있을까? 지금까지 객체를 설계할 때 어떤 상태와 행동을 할당했다면 왜 그렇게 했는지 생각해 보자. 객체의 행동을 결정하는 것은 객체가 참여하고 있는 협력이다. 협력이 바뀌면 객체가 제공해야 하는 행동도 바뀌어야 된다.
객체의 행동을 결정하는 것이 협력이라면 상태를 결정하는 것은 행동이다. 객체의 상태는 그 객체가 행동을 수행하는 데 필요한 정보이다. 객체는 자신의 상태를 스스로 결정하고 결정하는 자율적인 존재이기 때문에 객체가 수행하는 행동에 필요한 상태도 함께 가지고 있어야 한다.
public class Movie {
private Money fee;
private DiscountPolicy discountPolicy;
public Money calculateMovieFee(Screening screening) {
return fee.minus(discountPolicy.calculateDiscountAmount(screening));
}
}
기본요금인 fee와 할인 정책인 discountPolicy를 상태로 포함하는 이유는 요금 계산이라는 행동을 수행하는 데 이 정보들이 필요하기 때문! 그래서 결국 협력에 따라 행동과 상태가 모두 결정되므로 협력은 객체를 설계하는 문맥을 제공한다 할 수 있다.
책임(Responsibility)
객체를 설계하기 위해 필요한 문맥인 협력이 갖춰진 후 다음으로는 협력에 필요한 행동을 수행할 수 있는 적절한 객체를 찾는 것이다. 책임은 객체에 의해 정의되는 응집도 있는 행위의 집합으로, ‘무엇을 알고 있는가’와 ‘무엇을 할 수 있는가’로 구성된다.
하는 것(doing)
- 객체를 생성하거나 계산을 수행하는 등의 스스로 하는 것
- 다른 객체의 행동을 시작시키는 것
- 다른 객체의 활동을 제어하고 조절하는 것
아는 것(knowing)
- 사적인 정보에 관해 아는 것
- 관련된 객체에 관해 아는 것
- 자신이 유도하거나 계산할 수 있는 것에 관해 아는 것
예를 들어 Screening과 Movie의 책임은 무엇일까? Screening의 책임은 영화를 예매하는 것이고 Movie는 예매 가격을 계산할 책임을 진다. 이것은 하는 것과 관련된 책임이다. 그리고 Screening은 자신이 상영할 영화를 알고 있어야 되고 Movie는 가격과 어떤 할인 정책이 적용됐는지도 알고 있어야 한다. 이것은 아는 것과 관련된 책임이라 할 수 있다.
객체지향 설계에서 가장 중요한 것은 책임으로 객체에게 얼마나 적절한 책임을 할당하느냐가 설계의 전체적인 품질을 결정한다.
책임 할당 과정
영화 예매하는 기능의 책임을 할당하는 과정을 살펴보자. 객체가 책임을 수행하게 하는 방법은 메시지를 전송하는 것이므로 예매하라라는 이름의 메시지로 협력을 시작할 수 있겠다.
이제 메시지를 처리할 적절한 객체를 선택해야 되는데 영화 예매와 관련된 정보를 가장 많이 알고 있는 객체에게 책임을 할당하는 것이 좋다. 영화를 예매하기 위해서는 상영 시간과 기본요금을 알아야 하기 때문에 이 정보를 소유하고 있거나 해당 정보의 소유자를 가장 잘 알고 있는 객체는 Screening이라 할 수 있다.
하지만 영화를 예매하기 위해서는 예매 가격을 알아야 하는데 Screening은 가격을 계산하는데 필요한 정보를 충분히 알고 있지 않기 때문에 외부 객체의 도움이 필요하다. 그래서 새로운 메시지가 필요하다.
이제 가격을 계산하라라는 새로운 메시지를 처리할 적절한 객체를 선택해야 되는데 가격을 계산하기 위해서는 가격과 할인 정책이 필요하다. 이것을 가장 잘 알고 있는 객체는 Movie이므로 Movie에게 책임을 할당하자.
여기서 끝이 아니다.. 가격을 계산하기 위해서는 할인 요금이 필요하지만 Movie는 할인 요금을 혼자 계산할 수 없기 때문에 또한 외부에 도움을 요청해야 된다. 즉, 할인 요금을 계산하라라는 새로운 메시지가 또 필요하다. 이렇게 객체지향 설계는 메시지를 찾고, 적절한 객체를 선택하는 반복적인 과정이 필요하다.
모든 책임 할당 과정이 이렇게 단순하게 되지는 않는다. 응집도와 결합도 관점에서 정보 전문가가 아닌 다른 객체에게 책임을 할당하는 것이 더 좋을 때도 있다. 하지만 기본적으로는 정보 전문가에게 책임을 할당함으로써 자율적인 객체를 만들 가능성이 높다.
위와 같은 방법으로 설계하는 방법을 책임 주도 설계(Responsibility-Driven Design, RDD)라고 부른다. 구현이 아닌 책임에 집중하는 것으로 유연하고 견고한 객체지향 시스템을 만들 수 있다. 이제 책임을 할당할 때 고려해야 하는 두 가지 요소를 알아보자.
1. 메시지가 객체를 결정
앞의 과정을 생각해 보면 필요한 메시지를 먼저 식별하고 메시지를 처리할 객체를 나중에 선택했다. 다시 말해 메시지가 객체를 선택했다고 할 수 있는데 그렇게 함으로써 다음과 같은 장점이 있다.
- 객체가 최소한의 인터페이스를 가질 수 있게 된다.
- 필요한 메시지가 식별될 때까지 객체의 퍼블릭 인터페이스에 어떤 것도 추가하지 않기 때문에 최소한의 퍼블릭 인터페이스를 가질 수 있다.
- 객체는 충분히 추상적인 인터페이스를 가질 수 있게 된다.
- 객체의 인터페이스는 무엇(what)을 하는지는 표현해야 하지만 어떻게(how) 수행하는지를 노출해서는 안된다.
- 메시지는 외부의 객체가 요청하는 무언가를 의미하기 때문에 메시지를 먼저 식별하면 무엇을 수행할지에 초점을 맞추는 인터페이스를 얻을 수 있다.
2. 행동이 상태를 결정
객체의 행동은 객체가 협력에 참여할 수 있는 유일한 방법으로 객체가 협력에 적합한지를 결정하는 것은 그 객체의 상태가 아니라 행동이다. 얼마나 적절한 객체를 만들었느냐는 얼마나 적절한 책임을 할당했는냐에 달려있고, 책임이 얼마나 적절한지는 협력에 얼마나 적절한지에 달려있다고 볼 수 있다.
초보자들이 가장 쉽게 하는 실수가 객체의 행동이 아니라 상태에 초점을 맞춰 객체에 필요한 상태가 무엇인지 결정하고, 그 후에 상태에 필요한 행동을 결정한다. 이런 방식은 객체의 내부 구현이 객체의 퍼블릭 인터페이스에 노출되도록 만들기 때문에 캡슐화를 저해한다. 이렇게 되면 객체의 내부 구현을 변경하면 퍼블릭 인터페이스도 변경되고, 결국 객체에 의존하는 클라이언트로 변경이 전파된다.
역할(Role)
객체가 어떤 특정한 협력 안에서 수행하는 책임의 집합을 역할이라고 하는데 실제로 협력을 모델링 할 때는 특정한 객체가 아니라 역할에게 책임을 할당한다고 생각하는 게 좋다. 예를 들어, 앞에서 예매하라라는 메시지를 처리하기 위해 Screening을 선택했는데 이 과정에서는 사실 두 가지 단계가 합쳐진 것이다.
첫 번째는 영화를 예매할 수 있는 적절한 역할이 무엇인가를 찾고, 그 뒤에 두 번째로 역할을 수행할 객체로 Screening 인스턴스를 선택하는 것이다.
그리고 가격을 계산하라라는 메시지를 처리할 때도 역할에 관해 먼저 고민하고 역할을 수행할 객체로 Movie를 선택한 것이다.
위와 같이 설계하는 이유는 뭘까? 괜히 역할이라는 개념을 만들어 더 번거롭게 하는 건 아닐까..?
유연하고 재사용 가능한 협력
역할이 중요한 이유는 역할을 통해 유연하고 재사용 가능한 협력을 이용할 수 있다. 만약 역할이라는 개념을 제외하고 객체에게 책임을 할당한다면 어떻게 될지 한번 살펴보자. Movie가 가격을 계산하기 위해서는 할인 요금이 필요한데 혼자 할 수 없기 때문에 외부의 객체에 도움을 요청해야 된다.
하지만 현재 할인 정책에는 금액 할인 정책과 비율 할인 정책이라는 두 가지 종류의 할인 정책이 존재한다. 그래서 두 가지 종류의 객체가 할인 요금을 계산하라라는 메시지에 응답할 수 있어야 되기 때문에 협력을 개별적으로 생성해 중복적인 코드가 발생하게 된다.
이러한 문제를 해결하기 위해서는 객체가 아닌 책임에 초점을 맞춰서 생각해야 한다. AmountDiscountPolicy, PercentDiscountPolicy 모두 할인 요금 계산이라는 동일한 책임을 수행하기 때문에, 객체라는 존재를 지우고 할인 요금을 계산하라라는 메시지에 응답할 수 있는 대표자를 만들어 놓고 두 할인 정책을 갈아끼우면 두 협력을 하나로 통합할 수 있다.
여기서 역할이 두 종류의 구체적인 객체를 포괄하는 추상화라는 점을 보자. 그래서 구체적인 객체들을 포괄할 수 있는 추상적인 이름 DiscountPolicy를 부여했다. 여기서 가격을 할인하지 않는 NonDiscountPolicy를 추가하더라도 새로운 협력을 생성하지 않고 그냥 위에서 갈아끼우기만 하면 된다. 이제 왜 역할이 책임들의 집합인지 알겠는가? 이렇게 책임과 역할을 중심으로 협력을 바라보면 변경과 확장이 용이한 설계로 나아갈 수 있다.
이론과 실전
대부분의 경우 어떤 것이 역할이고 어떤 것이 객체인지 또렷하게 드러나지 않아 명확한 기준을 세우기 어려울 것이다. 그래서 설계를 처음 할 때는 역할과 객체를 구분하는 것은 신경 쓰지 말고 적절한 책임과 협력을 탐색하는 것에 더 집중하자. 애매하다면 단순하게 객체로 시작해 반복적으로 책임과 협력을 정제해가면서 필요한 순간에 객체로부터 역할을 분리해 내면 된다.
만약 다양한 객체들이 협력에 참여한다는 것이 확실하다면 역할로 시작하면 되겠지만 모든 것이 확실하지 않아 결정하기 어려운 상황이라면 구체적인 객체로 시작하자. 다양한 시나리오를 탐색하고 유사한 협력을 단순화하고 합치다 보면 자연스럽게 역할이 드러날 것이다.
참고:오브젝트
*틀린 부분이 있으면 언제든지 말씀해 주시면 공부해서 수정하겠습니다.