Computer Science/객체지향

역할, 책임, 협력 - 코드로 이해하는 객체지향 프로그래밍

sh1mj1 2023. 7. 21. 19:33

이전 글 https://sh1mj1-log.tistory.com/131 에서

객체 지향 프로그밍을 구성하는 다양한 요소와 구현 기법을 살펴보았습니다.

  • 클래스, 추상 클래스, 인터페이스를 조합하여 객체지향 프로그램을 구조화하는 기본 방법
  • 상속을 이용해 다형성을 구현하는 기법
  • 다형성은 지연 바인딩을 통해 구현됨.
  • 상속은 다형성을 위해 사용. 코드 재사용만 할 때는 캡슐화 측면에서 합성이 더 좋음.
  • 유연한 객체 지향 프로그램을 위해서는 컴파일 타임 의존성과 런타임 의존성이 다름.

 

역할, 책임, 협력

객체지향의 본질은 역할(role), 책임(responsibility), 협력(collaboration) 입니다. 물론 클래스, 상속, 지연 바인딩이 중요하지만 이는 구현 방법일 뿐 본질은 아닙니다. 계속 반복하여 이야기하지만 핵심은 협력을 구성하기 위해 적절한 객체를 찾고 적절한 책임을 할당하는 과정에서 드러납니다. 이 과정을 너무 빨리 넘어가고 구현에 초점을 맞추면 변경하기 어렵고 유연하지 못한 코드를 낳습니다.

협력

영화 예매 시스템 돌아보기

영화 예매 기능을 구현하기 위한 객체들 사이의 상호작용

다양한 객체들이 영화 예매라는 기능을 구현하기 위해서 메시지를 주고받으면서 상호작용합니다.

이러한 상호작용을 협력 이라고 합니다. 객체가 협력에 참여하기 위해 수행하는 로직은 책임이라고 합니다. 협력 안에서 수행하는 책임들이 모여 객체가 수행하는 역할을 구성합니다.

협력

두 객체 사이의 협력은 하나의 객체가 다른 객체에게 도움을 요청할 때 시작됩니다. 메시지 전송(message sending) 은 객체 사이의 협력을 위해 사용할 수 있는 유일한 수단입니다. 다른 객체에 상세한 내부 구현에는 접근할 수 없기 때문에 메시지 전송을 통해서만 요청을 전달합니다.

메시지를 수신한 객체는 메소드을 실행해 요청에 응답합니다. 객체가 메시지를 처리할 방법을 스스로 선택합니다. 객체는 자율적인 존재입니다.

 

Screening 이 Movie 에게 메시지 전송, 협력

Screening 이 Movie 에게 처리를 위임하는 이유는 요금을 계산하는 기본 요금과 할인 정책을 Movie 객체가 잘 알고 있기 때문입니다.

 

만약 요금계산을 Screening 이 수행한다면 Movie 의 인스턴스 변수인 fee, discountPolicy 에 직접 접근해야 합니다. Screening 이 Movie 내부 구현에 결합되는 것입니다.

또한 객체의 자율성을 보장하려면 정보와 행동이 같은 객체에 있어야 합니다. 그런데 이 경우에는 정보와 행동이 Movie 와 Screening 이라는 별도의 객체로 나뉘게 됩니다. Movie 자율성도 훼손합니다.

 

Movie 의 자율성을 위해 자신이 알고 있는 정보로 직접 요금을 계산해야 합니다. 결과적으로 내부 구현을 캡슐화 해서 객체를 자율적으로 만들 수 있습니다. 만약 내부 구현에 직접 접근하게 하면 Movie 의 내부 구현을 바꾸었을 때 Screening 도 영향을 받게 됩니다. 반대로 캡술화하면 결합도를 낮춰서 변경의 여파가 오지 않습니다.

이렇게 객체들 사이의 협력을 구성하는 일련의 요청과 응답의 흐름으로 애플리케이션의 기능 구현됩니다.

 

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

객체의 행동을 결정하는 것은 객체가 참여하고 있는 협력입니다. 협력이 바뀌면 객체가 제공해야 하는 행동 역시 바뀌어야 합니다. 협력은 객체가 필요한 이유이며, 행동의 이유입니다.

 

현재 Movie 행동을 결정하는 것은 영화 예매를 위한 협력입니다. 협력을 고려하지 않고 Movie 의 행동을 결정하는 것은 의미가 없습니다. 협력이 있기 때문에 객체가 있는 것입니다.

그리고 객체의 행동은 객체의 상태를 결정합니다. 객체는 자신의 상태를 스스로 결정하고 관리하는 자율적인 존재이기 때문에 객체가 수행하는 행동에 필요한 상태도 갖고 있어야 합니다.

public class Movie {
    ...
    private Money fee;
    private DiscountPolicy discountPolicy;
	...
    public Money calculateMovieFee(Screening screening) {
        return fee.minus(discountPolicy.calculateDiscountAmount(screening));
    }

Moviefee discountPolicy  을 상태의 일부로 포함합니다. 요금계산이라는 행동을 수행하는 데에 이 정보들이 필요하기 때문입니다.

 

결과적으로 객체가 참여하는 협력이 객체를 구성하는 행동과 상태를 모두 결정합니다. 즉, 협력은 객체 설계의 문맥(context) 을 제공합니다.

 

책임

책임이란 무엇인가

협력이 갖춰졌다고 하면 그 다음으로는 협력에 필요한 행동을 수행할 수 있는 적절한 객체를 찾아야 합니다. 이 때 객체가 수행하는 행동을 책임 이라고 합니다.

 

객체의 책임은 하는 것과 아는 것입니다.

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

 

  • Screening
    • 상영 정보를 앎 - Movie
    • 예매 정보 생성
  • Movie
    • 영화 정보를 앎 - DiscountPolicy
    • 가격을 계산

 

  • DiscountPolicy
    • 할인 정책을 앎 - DiscountCondition
    • 할인된 가격을 계산

 

  • DiscountCondition
    • 할인 조건을 앎 - Screening
    • 할인 여부를 판단

 

Screening 은 협력 안에서 영화를 예매할 책임을 수행해야 하기 때문에 reserve 메시지를 수신하고 movie 을 인스턴스 변수로 포함합니다. Movie 는 협력 안에서 가격을 계산할 책임을 할당받았기 때문에 calculateMovieFee 메시지를 수신하고 fee, discountPolicy 을 속성으로 가집니다. 이렇게 협력 안에서 객체의 책임이 외부 인터페이스와 내부의 속성을 결정합니다.

 

책임의 관점에서 '아는 것'과 '하는 것' 은 밀접하게 연관되어 있습니다. 객체는 자신의 책임을 수행하는 데 필요한 정보를 알고 있을 책임과 자신이 할 수 없는 작업을 도와줄 객체를 알고 있음 책임이 있습니다.

 

사실 협력이 중요한 이유는 객체에게 할당할 책임을 결정하는 문맥을 제공하기 때문입니다. 적절한 협력이 적절한 책임을 제공하고 적절한 책임을 적절한 객체에 할당하면 단순, 유연하게 설계할 수 있습니다. 구현 방법보다 책임 결정이 더 중요합니다.

 

책임 할당

자율적인 객체를 만드는 기본적인 방법은 책임을 수행하는데 필요한 정보를 가장 잘 아는 전문가에게 책임을 할당하는 것입니다. 이를 INFORMATION EXPERT(정보 전문가) 패턴 이라고 합니다. 요청에 응답하기 위해 필요한 이 행동이 객체가 수행할 책임으로 이어집니다.

 

협력 설계를 위해 먼저 시스템이 사용자에게 제공하는 기능을 시스템이 담당하는 하나의 책임으로 바라봅니다. 그리고 시스템의 이 책임을 완료하기 위해 더 작은 책임으로 나누어서 객체에게 할당하는 반복적인 과정을 거쳐야 합니다.

 

영화 예매 시스템을 예로 들어서 직접 이 과정을 거쳐봅시다. 시스템의 책임은 영화 예매입니다.  '예매하라' 라는 메시지로 협력을 시작하면 되겠네요.

 

이 메시지를 처리할 적절한 객체가 있어야 합니다. 기본 전략은 정보 전문가에 게 책임을 할당하는 것입니다. 영화 예매를 위해서는 상영 시간과 순서, 영화를 알아야 합니다. 이 정보에 대해서는 Screening 이 전문가입니다.

영화 예매를 위해서는 예매 가격을 알아야 합니다. Screening 은 예매 가격을 계산하는데 필요한 정보를 충분히 알고 있지 않습니다. Screening 은 상영 정보를 알지만 영화 가격 자체에 대한 전문가는 아닙니다.

이번에도 마찬가리조 가격을 계산하는 데 필요한 정보를 가장 많이 아는 전문가를 선택해야 합니다. 가격과 할인 정책이 필요합니다. 이에 대한 전문가는 Movie 입니다. 

가격을 계산하기 위한 할인 요금이 필요하지만 Movie 는 할인 요금 계산에 대한 전문가는 아닙니다. Movie 는 할인 요금 계산을 위한 요청을 외부에 전송해야 합니다. 그러므로 '할인 요금을 계산하라' 라는 메시지가 또 있겠네요. 이러한 방식으로 이루어 집니다.

 

이렇게 협력에 필요한 메시지를 찾고 메시지에 적절한 객체를 선택하는 반복 과정을 조금 해보았습니다. 이 메시지는 메시지를 수신할 객체의 책임을 결정합니다. 이 메시지가 결국 객체의 퍼블릭 인터페이스를 구성합니다. 협력을 설계하면서 객체의 책임을 식별해 나가는 과정에서 최종 결과물은 시스템을 구성하는 객체들의 인터페이스와 오퍼레이션의 목록입니다.

 

물론 실제 책임 할당이 이렇게 단순하지만은 않습니다. 어떤 경우는 응집도, 결합도 관점에서 정보 전문가가 아닌 다른 객체에게 책임을 할당하는 것이 더 적절한 경우도 있습니다. 위 전략은 기본적인 전략이지만 대부분의 경우 정보 전문가에게 책임을 할당하는 것만으로도 상태와 행동을 함께 가지는 자율적 객체를 만들 가능성이 높습니다.

 

책임 주도 설계

책임 주도 설계(Responsibility-Driven Design, RDD) 는 책임을 찾고 책임을 수행할 적절한 객체를 찾아서 책임을 할당하는 방식의 협력 설계입니다. 위에서의 설계 과정이 RDD 의 기본적인 흐름입니다.

 

RDD의 과정

 

  1. 시스템이 사용자에게 제공해야 하는 기능인 시스템 책임을 파악
  2. 시스템의 책임을 더 작은 책임으로 나눔.
  3. 분할된 책임을 수행할 수 있는 적절한 객체 or 역할을 찾아서 책임을 할당.
  4. 객체가 책임 수행 도중 다른 객체의 도움이 필요하면 또 적절한 객체 or 역할을 찾음.
  5. 해당 객체 or 역할에게 책임 할당. 두 객체가 협력하게 됨.  3~5 과정을 반복 

RDD 는 객체의 구현이 아닌 책임에 집중할 수 있게 합니다.

 

책임을 할당할 때는 '메시지가 객체를 결정한다'는 것과 '행동이 상태를 결정한다'는 요소를 잘 고려해야 합니다.

 

메시지가 객체를 결정.

메시지가 객체를 선택하도록 하면

 

1. 객체가 최소한의 인터페이스(minimal interface)를 갖게 됩니다.

필요한 메시지가 식별될 때까지 객체의 퍼블릭 인터페이스에 아무것도 추가하지 않습니다. 그래서 객체는 꼭 필요한 퍼블릭 인터페이스를 갖게 됩니다.

 

2. 객체는 충분히 추상적인 인터페이스(abstract interface)를 가질 수 있게 됩니다.

객체의 인터페이스는 무엇(what)을 하는지는 표현해야 하지만 어떻게(how) 수행하는지를 노출해서는 안 됩니다. 객체가 무엇(what)을 수행할지에 초점을 맞출 수 있습니다. 

 

영화 예매 시스템도 '예매하라' 라는 메시지로 설계를 시작했습니다. 그 메시지를 수신할 적절한 객체로 Screening 을 선택했습니다.그 후 Screening 은 '가격을 계산하라' 라는 메시지를 전송해야 한다는 사실을 결정하고 그 메시지의 수신은 Movie 가 하도록 했습니다.

 

행동이 상태를 결정

객체는 협력에 참여하기 위해 존재합니다. 그러므로 객체는 상태보다 행동이 더 중요합니다. 객체의 행동으로 협력에 참여할 수 있습니다.  우리는 객체의 상태보다 행동에 집중해야 합니다. 이로써 더 좋은 캡슐화가 가능해집니다. 내부 구현에 초점을 맞추어 설계하면 내부 구현 변경 시 퍼블릭 인터페이스가 함께 바뀌고, 객체에 의존하는 클라이언트로 변경의 영향이 전파됩니다. 이 설계 방법은 데이터 주도 설계(Data-Drive Design) 이라고 합니다.

 

중요한 것은 객체의 상태가 아닌 행동입니다. 객체가 가질 수 있는 상태는 행동을 결정하고 나서야 결정할 수 있습니다. 이 행동이 바로 객체의 책임입니다.

 

역할

역할과 협력

객체가 어떤 특정한 협력 안에서 수행하는 책임의 집합을 역할 이라고 부릅니다. 실제로 협력을 모델링할 때는 특정 객체가 아닌 역할에게 책임을 할당한다고 생각하는 것이 더 좋습니다.

 

위 영화 예매 협력에서 '예매하라' 라는 메시지를 처리하기에 적합한 객체로 Screening 을 선택했었죠? 이것은 사실 두 개의 단계가 합쳐진 것입니다. 영화를 예매할 수 있는 적절한 역할이 무엇인지를 찾는 것과, 역할을 수행할 객체로 Screeneing 인스턴스를 선택하는 것입니다.

Screening 과 Movie 의 협력 역시 마찬가지입니다. 역할에 특별한 이름을 부여하지는 않았지만 객체를 받아들일 수 있는 위치로서 역할이라는 개념은 여전히 존재합니다. 

 

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

역할을 통해 유연하고 재사용 가능한 협력을 얻을 수 있습니다.

 

역할을 고려하지 않고 객체에게 책임을 할당한다고 해봅시다. Movie 가 '할인 요금을 계산하라' 라는 메시지를 전송해서 외부의 객체에게 도움을 요청합니다.

영화 예매 도메인에는 금액 할인, 비율 할인 정책이 있습니다. AmountDiscountPolicy 인스턴스와 PercentDiscountPolicy 인스턴스라는 두 종류의 객체가 메시지에 응답할 수 있어야 합니다.

 

이렇게 두 협력을 구현하면 대부분의 코드가 중복될 것입니다. 

 

문제를 해결하기 위해서는 객체가 아닌 책임에 초점을 맞추어야 합니다. 책임의 관점에서 AmountDiscountPolicy 와 PercentDiscountPolicy 는 동일한 책임을 수행합니다. 객체라는 존재를 지우고 '할인 요금을 계산하라' 라는 메시지에 응답할 대표자를 생각하면 두 협력을 하나의 협력으로 통합할 수 있습니다. 이 대표자를 두 종류의 객체를 교대로 바꿔 끼는 일종의 슬롯이라고 하면 이 슬롯이 바로 역할입니다.

역할은 다른 거승로 교체할 수 있는 책임의 집합입니다.

여기서 역할은 두 종류의 구체적인 객체를 포괄하는 추상화 라는 것입니다. 역할의 이름으로 추상적인 DiscountPolicy 라고 지었습니다. 

 

책임을 수행하는 역할을 기반으로 두 개의 협력을 하나로 통합했습니다. 역할을 이용하여 불필요한 중복 코드를 제거했고 협력이 더 유연해졌습니다.

 

역할을 구현하는 가장 일반적인 방법은 추상 클래스 와 인터페이스를 사용하는 것입니다.
협력의 관점에서 둘 모두 역할을 정의할 수 있는 구현 방법이라는 공통점을 공유합니다.
이들은 동일한 책임을 수행할 수 있는 객체들을 협력 안에 수용할 수 있는 역할입니다.

역할은 다양한 종류의 객체를 수용할 수 있는 슬롯이자 구체적인 객체들의 타입을 캡슐화하는 추상화입니다.

 

NoneDiscountPolicy 역시 DiscountPolicy 역할을 수행하는 객체의 한 종류입니다.

 

객체 대 역할

협력에 적합한 책임을 수행하는 대상이 한 종류라면 간단하게 객체로 간주합니다. 만약 여러 종류의 객체들이 참여할 수 있다면 역할이지요.

 

협력은 역할들의 상호작용으로 구성되고, 협력을 구성하기 위한 역할에 적합한 객체가 선택되고 객체는 클래스를 이용해서 구현되고 생성됩니다.

린스카우의 설명

도메인 모델에서는 개념과 객체, 역할이 어지럽게 섞여있습니다. 그래서 처음부터 완벽하게 도메인 모델을 설계할 수는 없습니다. 설계 초반에는 적절한 책임과 협력의 큰 그림을 탐색하는 것이 가장 중요한 목표여야 합니다. 역할과 객체를 명확히 구분하는 것은 그 다음이지요. 

다양한 객체들이 협력에 참여한다는 것이 확실하다면 역할로 시작하고 아직 정확한 결정을 내리기 어렵다면 구체적인 객체로 시작하는 것이 좋습니다. 그리고 여러 시나리오를 탐색하고 유사한 협력을 단순화하다 보면 자연스럽게 역할이 보일 것입니다.

 

협력을 구체적인 객체가 아닌 추상적인 역할의 관점에서 설계하면 협력이 유연하고 재사용 가능해집니다. 설계의 구성요소를 추상화할 수 있는 것입니다.

 

역할과 추상화

추상화의 장점을 다시 되짚어 봅시다.

  • 추상화 계층만을 이용하면 중요한 정책을 상위 수준에서 단순화 가능.
  • 설계가 더 유연해짐.

 

역할은 공통의 책임을 바탕으로 객체의 종류를 숨기기 때문에 역할을 객체의 추상화로 볼 수 있습니다. 추상화의 두 장점은 협력의 관점에서 역할에도 동일하게 적용됩니다.

 

위 그림은 영화 예매 시스템에 존재하는 할인 정책과 할인 조건의 종류를 파악하기 위한 목적에는 적합합니다. 하지만 할인 정책과 할인 조건의 종류라는 너무 세부적인 사항 때문에 객체들 간의 핵심 관계와 관련된 큰 그림을 파악하는 것이 방해됩니다.

 

정적 클래스 관계 표현한 다이어그램

추상화는 할인 정책과 할인 조건이 조합되어 영화의 예매 요금을 결정한다는 사실입니다. 

협력이라는 관점에서는 세부적인 사항을 무시하고 추상화에 집중하는 것이 유용합니다.

협력 관계를 표현

 

지금은 협력에 참여하는 할인 정책과 할인 조건의 종류는 중요하지 않고 단지 구체적인 할인 정책과 할인 조건이 DiscountPolicyDiscountCondition 의 자리를 대체할 것이라는 것만 알고 있어도 충분합니다. 구체적인 조합을 고려하지 않고도 상위 수준에서 협력을 충분히 설명할 수 있습니다. 복잡성을 제거하고 단순화해서 표현할 수 있죠.

'가격 할인 정책과 함께 2개의 순번 규칙과 1개의 비율 규칙을 적용' 하거나 '비율 할인 정책과 함께 3개의 순번 규칙을 적용' 한다는 표현이 '할인 정책과 여러 개의 할인 조건으로 적용한다' 로 표현되는 것입니다.

 

협력 안에서 동일한 책임을 수행하는 객체들은 동일한 역할을 수행하기 때문에 서로 대체 가능합니다. 따라서 역할은 다양한 환경에서 다양한 객체들을 수용할 수 있게 하여 협력을 유연하게 만듭니다.

우리는 다양한 종류의 할인 정책과 할인 조건에도 적용될 수 있는 협력을 만들었습니다. 역할이라는 추상화로 기존 코드를 바꾸지 않고 새로운 행동을 추가할 수 있습니다. 즉, 유연한 설계가 가능한 것이죠. 역할은 프레임워크나 디자인 패턴처럼 재사용 가능한 코드나 설계 아이디어를 구성하는 핵심이기도 합니다.

 

배우와 배역

배우는 연극 상영동안 자신이 연기해야 하는 배역의 가면을 씁니다. 관객은 연극동안은 배우를 배역으로 바라보고 무대가 끝나면 본래의 배우로 바라봅니다.

  • 배역은 연극 배우가 특정 연극에서 연기하는 역할
  • 배역은 연극이 상영되는 동안에만 존재하는 일시적인 개념
  • 연극이 끝나면 배역이라는 역할에서 벗어나 원래의 연극 배우로 돌아옴.

 

또 배역은 여러 명의 배우들이 연기할 수 있습니다. 

  • 서로 다른 배우들이 동일한 배역을 연기할 수 있음.
  • 하나의 배우가 다양한 연극 안에서 서로 다른 배역을 연기할 수 있음

 

이 은유는 협력 안에서 역할을 수행하는 객체에 대한 설명으로 아주 좋습니다. 협력이 연극이고, 코드는 극본인 것이죠.

객체(배우)는 협력(연극)이 끝나고 협력에서의 역할(배역)을 잊고 원래 객체로 돌아옵니다. 역할은 객체가 협력에 참여하는 잠시 동안에만 존재하는 일시적인 개념입니다. 역할은 오직 시스템의 문맥 안에서 무엇을 하는지에 의해서만 정의될 수 있습니다.

 

  • 동일한 역할을 수행하는 객체들은 서로 대체 가능합니다.
  • 객체는 다양한 역할을 가질 수 있습니다.
  • 하지만 특정한 협력 안에서는 일시적으로 오직 하나의 역할만 보여집니다.

 

역할은 특정한 객체의 종류를 캡슐화하기 때문에 동일한 역할을 수행하고 계약을 준수하는데 대체 가능한 객체들은 다형적입니다.