Published on

오브젝트 Chap3

Authors
  • avatar
    Name
    ywj9811
    Twitter

역할, 책임, 협력

객체지향 패러다임의 관점에서 핵심은 역할, 책임, 협력 이다.

클래스, 상속, 지연 바인딩이 중요하지 않은 것은 아니지만 다분히 구현 측면에 치우쳐 있기 때문에 객체지향 패러다임의 본질과는 거리가 멀다.

협력

영화 예매 시스템 돌아보기

2장에서 다루었던 영화 예매 시스템을 돌아보면 객체지향 원칙을 따르는 어플리케이션의 제어 흐름은 어떤 하나의 객체에 의해 통제되지 않고 다양한 객체들 사이에 균형 있게 분배되는 것이 일반적이다.

이처럼 객체들이 어플리케이션의 기능을 구현하기 위해 수행하는 상호작용을 협력이라고 한다.

객체가 협력에 참여하기 위해 수행하는 로직은 책임이라고 한다.

객체들이 협력 안에서 수행하는 책임들이 모여 객체가 수행하는 역할을 구성한다.

협력에 대해서

두 객체 사이의 협력은 하나의 객체가 다른 객체에게 도움을 요청할 때 시작되는데, 메시지 전송은 객체 사이의 협력을 위해 사용할 수 있는 유일한 커뮤니케이션 수단이다.

그리고 협력을 위해서는 객체를 서로 분리된 인스턴스가 아닌 협력하는 파트너로 인식해야 한다.

예를 들어, Screening과 Movie의 협력 관계를 살펴보면, Screening은 Movie에 calculateMovieFee 메시지를 전송하여 요금 계산을 요청하는데, 이는 요금을 계산하는데 필요한 기본 요금과 할인 정책을 가장 잘 알고 있는 객체가 Movie이기 때문이다. 만약 Screening이 계산을 수행한다면 Movie의 인스턴스 변수인 fee와 discountPloicy에 직접 접근해야 할 것이다. 이는 Movie의 자율성이 훼손된다.

자율적인 객체

자율적인 객체란 자신의 상태를 직접 관리하고 스스로의 결정에 따라 행동하는 객체로, 객체의 자율성을 보장하기 위해서는 필요한 정보와 정보에 기반한 행동을 같은 객체 안에 모아놓아야 한다.

따라서 Screening이 Movie에게 계산을 위임하는 것과 같이 자신이 할 수 없는 일을 다른 객체에게 위임하면 협력에 참여하는 객체들의 전체적인 자율성을 향상시킬 수 있다.

정리하자면, 자율적인 객체는 자신에게 할당된 책임을 수행하던 중에 필요한 정보를 알지 못하거나 외부의 도움이 필요한 경우 적절한 객체에게 메시지를 전송하여 협력을 요청한다. 그리고 메시지를 수신한 객체 역시 메시지를 처리하던 도중 직접 처리할 수 없는 정보나 행동이 필요하다면 다른 객체에게 도움을 요청하는 것이다.

이러한 객체들 사이의 협력을 구성하는 일련의 요청과 응답의 흐름을 통해 어플리케이션의 기능이 구현된다.

협력이 설계를 위한 문맥을 결정한다.

객체의 행동을 결정하는 것은 객체가 참여하고 있는 협력이다.

협력이 바뀌면 객체가 제공해야 하는 행동 역시 바뀌어야 한다. 즉, 협력은 객체가 필요한 이유와 객체가 수행하는 행동의 동기를 제공한다.

객체는 자신의 상태를 스스로 결정하고 관리하는 자율적인 존재이기에 객체가 수행하는 행동에 필요한 상태도 함께 가지고 있어야 한다.

즉, MoviefeediscountPolicy라는 인스턴스 변수를 상태의 일부로 포함하는 이유는 요금 계산이라는 행동을 수행하는데 이정보들이 필요하기 때문이다.

책임

객체를 설계하기 위해 필요한 문맥인 협력이 갖춰졌다면, 협력에 필요한 행동을 수행할 수 있는 적절한 객체를 찾는 것인데, 이때 협력에 참여하기 위해 객체가 수행하는 행동을 책임이라 한다.

책임은 크게 하는 것, 아는 것 이렇게 나뉠 수 있다.

  • 하는 것
    • 객체를 생성하거나 계산을 수행하는 등 스스로 하는 것
    • 다른 객체의 행동을 시작하는 것
    • 다른 객체의 활동을 제어하고 조절하는 것
  • 아는 것
    • 사적인 정보에 대해 아는 것
    • 관련된 객체에 대해 아는 것
    • 자신이 유도하거나 계산할 수 있는 것에 대해 아는 것

그렇다면 Screening의 책임을 살펴보자.

영화를 예매할 수 있어야 하며 예매 가격을 계산할 책임이 있다. 이것은 하는 것과 관련된 책임이다.

그리고 자신이 상영할 영화를 알아야 하며 가격과 어떤 할인 정책이 적용 되었는지 알아야 한다. 이것은 아는 것과 관련된 책임이다.

이를 통해 협력 안에서 객체에게 할당한 책임이 외부의 인터페이스와 내부의 속성을 결정할 수 있다.

다만 책임은 객체가 수행할 수 있는 행동을 종합적이고 간략하게 서술하기 때문에 메시지보다 추상적이고 개념적으로도 더 크다. 즉, 하나의 책임이 여러 개의 메시지로 분할되기도 하고 하나의 객체가 수행할 수 있다고 생각한 책임이 나중에는 여러 객체들이 협력해야 하는 커다란 책임으로 자라나기도 한다.

이러한 책임은 객체지향 설계의 핵심이다.

객체지향 개발에서 가장 중요한 능력은 책임을 능숙하게 소프트웨어 객체에 할당하는 것이라고 하기도 한다.

협력 → 책임을 결정할 수 있는 문맥을 제공하기에 중요한 것이다.

책임 할당

자율적인 객체를 만드는 가장 기본적인 방법은 책임을 수행하는 데 필요한 정보를 가장 잘 아는 전문가에게 그 책임을 할당하는 것이다. 이를 INFORMATION EXPERT(정보 전문가) 패턴 이라고 한다.

협력을 설계하는 출발점은 시스템이 사용자에게 제공하는 기능을 시스템이 담당할 하나의 책임으로 보는 것이다.

객체지향 설계는 시스템의 책임을 완료하는 데 필요한 더 작은 책임을 찾아내고 이를 객체에게 할당하는 반복적인 과정을 통해 모양을 갖춘다.

2장의 시스템을 예시로 다시 들어보겠다.

시스템이 사용자에게 제공할 기능은 영화를 예매하는 것이다. 그리고 이것이 시스템의 책임이다.

객체가 책임을 수행하게 하는 유일한 방법은 메시지를 전송하는 것으로 책임을 할당하는 것은 메시지의 이름을 결정하는 것과 같은데, 따라서 예매하라 라는 메시지로 협력을 시작하자.

그렇다면 이 메시지를 처리할 적절한 객체는 무엇일까? 정보 전문가에게 책임을 할당하는 것이 바람직 할 것이다.

상영 시간과, 기본 요금을 알고 있거나 해당 정보의 소유자를 가장 잘 알고 있는 전문가는 Screening이다.

하지만 Screening은 예매 가격을 계산하는데 필요한 정보를 충분히 알고 있지 않다.

따라서 외부 객체에게 가격 계산을 요청하기 위한 메시지가 하나 더 필요하다.

가격을 계산하라 라는 메시지를 날려서 협력을 해야 한다. 이는 Movie가 가장 많은 정보를 알고 있으니 Movie에게 요청을 한다.

이처럼 객체지향 설계는 협력에 필요한 메시지를 찾고 메시지에 적절한 객체를 선택하는 반복적인 과정을 통해 이루어지며 이런 메시지가 메시지를 수신할 객체의 책임을 결정한다.

책임 주도 설계

이처럼 책임을 찾고 책임을 수행할 적절한 객체를 찾아 책임을 할당하는 방식으로 협력을 설계하는 방식을 책임 주도 설계(Responsibility-Driven Design, RDD) 라고 한다.

이전에 살펴본 영화 예매 시스템은 이러한 책임 주도 설계 방식의 기본적인 흐름을 따른 것이다.

아래는 책임 주도 설계 방법의 과정을 정리한 것이다.

  1. 시스템이 사용자에게 제공해야 하는 기능인 시스템 책임을 파악한다.
  2. 시스템 책임을 더 작은 책임으로 분할한다.
  3. 분할된 책임을 수행할 수 있는 적절한 객체 또는 역할을 찾아 책임을 할당한다.
  4. 객체가 책임을 수행하는 도중 다른 객체의 도움이 필요한 경우 이를 책임질 적절한 객체 또는 역할을 찾는다.
  5. 해당 객체 또는 역할에게 책임을 할당 함으로써 두 객체가 협력하게된다.

메시지가 객체를 결정한다

책임을 할당하는 데 필요한 메시지를 먼저 식별하고 메시지를 처리할 객체를 나중에 선택했다는 것이 중요하다.

즉, 객체가 메시지를 선택하는 것이 아닌 메시지가 객체를 선택하게 했다는 것이다.

이는 다음 두가지 중요한 이유가 있다.

  1. 객체가 최소한의 인터페이스를 가질 수 있게 한다.
  2. 객체는 충분히 추상적인 인터페이스를 가질 수 있게 한다.

행동이 상태를 결정한다

객체를 객체답게 만드는 것은 객체의 상태가 아니라 객체가 다른 객체에게 제공하는 행동이다.

객체의 행동은 객체가 협력에 참여할 수 있는 유일한 방법이다.

객체지향 패러다임에 갓 입문한 사람들이 쉽게 할 수 있는 실수는 객체의 행동이 아닌 상태에 초점을 맞추는 것이다. 그 결과 객체의 내부 구현이 객체의 퍼블릭 인터페이스에 노출되도록 만들어 캡슐화를 저해한다.

이러한 경우 객체의 내부 구현을 변경하면 퍼블릭 인터페이스도 함께 변경되고, 결국 객체에 의존하는 클라이언트로 변경의 영향이 전파되게 된다. 이러한 객체의 내부 구현에 초점을 맞춘 설계 방법을 데이터-주도 설계 (Data-Driven Design)이라고 한다.

이를 피하기 위해서는 우리는 객체의 행동을 결정하고 그 이후에 객체가 가질 수 있는 상태를 결정해야 한다.

즉, 협력이 객체의 행동을 결정하고 행동은 상태를 결정한다. 그리고 그 행동이 바로 객체의 책임이 되는 것이다.

역할

역할과 협력

객체는 협력이라는 주어진 문맥 안에서 특정한 목적을 가지게 된다. 객체의 목적은 협력 안에서 객체가 맡게 되는 책임의 집합으로 표시되는데, 이처럼 객체가 어떤 특정한 협력 안에서 수행하는 책임의 집합을 역할이라 한다.

실제로 협력을 모델링 할 때는 특정한 객체가 아닌 역할에게 책임을 할당한다고 생각하는 것이 좋다.

예를 들어, 영화 예매 협력에서 예매하라는 메시지를 처리하기 적합한 객체로 Screening을 선택했다.

이러한 하나의 단계처럼 보이는 이 책임 할당 과정은 실제로는 두 개의 독립적인 단계가 합쳐진 것이다.

  1. 영화를 예매할 수 있는 적절한 역할이 무엇인가 찾기
  2. 역할을 수행할 객체로 특정한 인스턴스 선택

이러한 방식과 마찬가지로 Screening과 Movie의 협력 관계도 진행됐다.

유연하고 재사용 가능한 협력

예를 들어 Movie에게 가격 계산 메시지를 보낼 때 AmountDiscountPolicy 인스턴스와 PercentDiscountPolicy 두가지 종류의 객체가 모두 참여하게 한다면 중복 코드가 발생하는 나쁜 코드가 될 것이다.

이러한 문제를 해결하기 위해서는 객체가 아닌 책임에 초점을 맞춰야 한다. 순수하게 책임의 관점에서 두 협력을 바라보면 AmountDiscountPolicy와 PercentDiscountPolicy 모두 할인 요금 계산이라는 동일한 책임을 수행한다는 사실을 알 수 있다.

즉, 객체라는 존재를 지우고 할인 요금을 계산하라는 메시지에 응답할 수 있는 대표자를 생각한다면 두 협력을 하나로 통합할 수 있을 것이다. 이 대표자를 협력 안에서 두 종류의 객체를 낄 수 있는 슬롯으로 생각할 수 있고, 이 슬롯이 바로 역할이다.

이때 역할이 두 종류의 구체적인 객체를 포괄하는 추상화라는 점에 주목하자.

이처럼 역할을 수행할 수 있다면 어떤 객체라도 이 협력에 참여할 수 있고, 유연한 설계로 나아갈 수 있을 것이다.

→ 역할을 구현하는 가장 일반적인 방법은 추상 클래스와 인터페이스를 활용하는 것이다.

객체 대 역할

역할은 객체가 참여할 수 있는 일종의 슬롯이다.

하지만 오직 한 종류의 객체만 협력에 참여하는 상황에서 역할이라는 개념을 고려하는 것이 유용할까?

이 부분에 대해서 생각해보자면, 협력에 적합한 책임을 수행하는 댓아이 한 종류라면 간단하게 객체로 간주하지만, 만약 여러 종류의 객체들이 참여할 수 있다면 역할로 보면 된다.

하지만 처음부터 하나의 객체만 참여하는지 여러개의 객체가 참여하는지 알기 어렵기에, 만약 여러개의 객체가 참여한다는 것이 확실하다면 역할로 보지만 처음에는 객체로 시작해서 책임과 협력을 정제해가면서 필요한 순간에 객체로 부터 역할을 분리해 내는 것이 좋은 방법이다.

역할과 추상화

2장에서 추상화를 이용한 설계가 가질 수 있는 두가지 장점을 살펴보았다.

역할은 공통의 책임을 바탕으로 객체의 종류를 숨기기 때문에 이러한 관점에서 역할을 객체의 추상화로 볼 수 있다.

따라서 추상화의 두가지 장점은 역할에도 마찬가지로 적용할 수 있다.

  1. 세부 사항에 억눌리지 않고도 상위 수준의 정책을 쉽고 간단하게 표현할 수 있다.
    • 다시 말해서, 각각의 할인 정책을 표현하지 않아도 DiscountPolicy하나로 표현하고, 각각의 할인 조건을 표현하지 않아도 DiscountCondition으로 표현하면 되는 것이다.
  2. 설계를 유연하게 만들 수 있다.
    • 이 또한 역할이 다양한 종류의 객체를 넣을 수 있는 일종의 슬롯이라는 점에 착안하면 쉽게 이해할 수 있을 것이다.

배우와 배역

배우와 배역에 대해서 생각해보면, 연극에 참여하는 그 순간 배우들은 사라지고 배역만 남는다. 그리고 무대의 막이 내리면 배역은 사라지고 다시 본래의 연극 배우로 돌아온다.

또한 하나의 배역은 여러명의 배우들이 수행할 수 있다.

예를 들면 로미오라는 배역은 그동안 수많은 배우들이 수행했을 것이다.

이러한 관점에서 바라보면 배우와 배역은 객체와 역할과 굉장히 유사하다.

배우와 배역을 생각하며 객체와 역할을 이해한다면 이해하기 쉬울 것이다.