본문 바로가기

[스터디] OOP - 역할과 책임, 협력

조영호님의 '오브젝트'(2021) 의 3장을 읽으면서 요약한 것입니다.


 

많은 사람들이 객체지향 패러다임을 설계할 때 클래스를 어떻게 정의하고, 어떤 걸 상속할지 등등 구체적인 구현에 초점을 맞춘다.

그러나 객체지향의 핵심은 역할, 책임, 협력에 있다. 다시 말하면 협력을 구성하기 위해 적절한 객체를 찾고 적절한 책임을 할당하는 것이 핵심이다.

 

협력이란 어떤 기능을 구현하기 위해 객체들 간 상호작용을 말한다.

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

이러한 책임들의 집합이 객체의 역할을 구성한다.

 

협력

객체지향 : 자율적인 객체들의 공동체

 

즉, 객체가 다른 객체에게 도움을 요청할 때 협력이 가능하다. ('메세지 전송'이라고도 한다.)

* 메세지 !== 메서드이다. 객체는 메세지를 받고, 적절한 메서드를 스스로 선택하여 호출하는 것이다.

 

2장의 예를 들어, 영화 상영 객체인 Screening이 '영화요금을 계산'하라는 메시지를 영화정보를 담고 있는 Movie 객체에 전송하여 Movie 객체가 영화 요금을 계산하도록 한다.

만약, Screening 객체가 직접 영화요금을 계산한다면 Movie객체의 '요금' 데이터와 '할인 정책' 내부 데이터에 접근해야 하므로 Movie 객체의 자율성이 저하된다. (=== 정보와 행동이 분리된다.)

 

따라서 Movie 객체의 자율성을 지켜주기 위해 Movie 객체가 직접 요금을 계산하도록 한다.

구현 측면에서 가장 쉬운 방법은 캡슐화이다.

 

캡슐화로 내부구현을 은닉하면 변경에 의한 사이드이펙트를 방지할 수 있어 객체의 내부 구현을 변경하기 쉽다.

(=== 결합도를 낮춘다.)

 

즉, 자율적인 객체는 부여된 책임을 스스로 수행하는 도중에 외부 객체의 도움이 필요한 경우, 다른 객체에게 메시지를 전송하여 협력을 요청할 수 있다.

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

 

협력이 문맥을 결정한다.

객체 : 상태와 행동을 캡슐화하는 실행 단위

객체의 존재 이유는 그 객체가 어떤 협력에 참여하고 있기 때문이고, 협력에 참여할 수 있는 이유는 그 객체가 적절한 행동을 가지고 있기 때문이다.

즉 협력이 객체의 행동을 결정한다.

 

즉, 협력을 먼저 고려하여 그 객체의 행동을 결정해야 한다. (선 협력, 후 행동)

이어서, 객체의 행동은 그 객체의 상태를 결정한다. (선 행동, 후 상태)

 

협력 -> 행동 -> 상태 순으로 생각하여 객체를 설계한다.

 

이러한 맥락으로 볼 때 협력은 객체를 설계하는데 일종의 문맥을 제공하는 것이다.

 

책임

어떤 협력이 정해지면 그 협력에 필요한 행동을 할 수 있는 객체를 찾아야 한다. 이 때 객체가 할 수 있는 행동을 책임이라고 한다.

책임은 '아는 것'과 '하는 것'으로 구분한다.

 

하는 것

  • 객체를 생성하거나 계산을 수행하는 등 스스로 수행
  • 다른 객체의 행동을 시작
  • 다른 객체의 활동을 제어

 

아는 것

  • private한 정보를 아는 것
  • 관련된 객체에 대해 아는 것
  • 자신이 계산가능한 것에 대해 아는 것

 

 

예를 들어, Screening의 책임은 상영할 영화를 알고있어야 하며, 영화를 예매할 수 있어야 한다. Movie 또한, 영화의 요금과 할인정보를 알고 있어야 하며, 요금을 계산할 수 있어야 한다는 책임이 있다.

 

위 책임이 객체의 외부 인터페이스(메소드)와 내부 속성(데이터)을 결정한다.

 

여기서 알 수 있는 사실은 아는 것과 하는 것이 연관되어 있다는 것이다. 객체는 책임을 수행하는데 필요한 정보를 알고있어야 하며, 자신이 할 수 없는 작업을 도와줄 객체를 알고 있어야 한다.

 

Q. 메세지 === 책임?

책임이 메세지보다 더 큰 개념으로 더욱 추상적이다. 하나의 책임이 여러 개의 메세지로 쪼개질 수 있고, 하나의 객체가 수행하던 책임이 여러 객체들의 협력으로 수행될 수 있다.

 

적절한 협력이 적절한 책임을 할당하고, 적절한 책임을 적절한 객체에 할당해야 단순, 유연한 설계가 가능하다.

 

책임 할당

이제 책임이 뭔지 알았으니 책임을 할당하는 방법에 대해 소개한다.

 

자율적인 객체를 만드는 가장 기본적인 방법은 책임을 수행하는데 필요한 정보를 잘 알고 있는 '정보 전문가'에게 그 책임을 주는 것이다.

이러한 패턴을 정보전문가 패턴이라고 한다.

 

1. 협력을 정의

책임을 할당하기 위해선 먼저 협력을 정의한다. 협력을 정의할 때 기능을 시스템이 담당하는 하나의 책임으로 간주해본다. 

 

이제부터 실무를 예로 들겠다. 

 

정의한 협력(시스템의 책임, 기능) : '대출심사대상 조회'

 

책임을 할당한다는 것은 곧 메세지를 전송한다는 뜻이다. (책임을 수행한다. === 메세지를 전송한다.)

 

따라서 메세지의 이름을 협력의 이름으로 가져간다.

 

'대출심사대상을 조회하라'

 

2. 시스템 책임을 처리할 객체 선택

대출심사대상을 조회할 객체는 어느 객체가 좋을까? 아까 말했던 정보전문가 패턴대로 대출심사대상조회와 관련한 정보를 많이 알고 있는 객체에게 할당한다. 여기서는 'EvaluationStore'이라고 한다.

 

*이 단계에서 시스템 책임을 처리할 객체를 찾는 것 외에도 먼저 시스템 책임을 더 작은 책임으로 분리하는 것도 방법이다.

 

3. 더 작은 책임 생성 및 객체 선택

대출심사대상을 조회하기 위해서는 사업자 번호를 사용자로부터 입력받아야 한다. EvaluationStore는 사업자번호에 관해서는 전문가가 아니기 때문에 새로운 메시지인 '사업자번호를 입력한다'를 생성한다.

 

2번을 반복하여 새로 생성된 메시지를 처리할 객체를 찾는다. 마찬가지로 정보 전문가를 선택하며 BusinessNumber가 이를 처리하도록 한다.

 

 

사업자번호를 입력받았다면 이제 심사 결과를 조회하여 해당/미해당을 결정해야 한다. 심사 결과에 대해서는 EvaluationStore가 역시나 전문가가 아니기 때문에 또다시 새로운 메시지를 생성하고 적절한 객체를 찾는다.

 

 

위 과정처럼 객체지향 설계는 협력에 필요한 메시지를 찾고, 메시지에 적절한 객체를 선택하는 과정을 반복하는 것이다.

 

메시지가 곧 객체의 책임을 결정하며, 구현적인 측면에서도 외부 인터페이스를 결정한다.

 

위 과정처럼 책임을 기반으로 협력을 설계하는 방식을 책임 주도 설계라고 부른다.

 

메시지가 객체를 결정한다.

메시지를 먼저 생성하고 그 후에 객체를 선택했다는 사실에 주목한다. 두가지 장점이 있다.

 

1. 객체가 최소한의 인터페이스만을 가진다.

2. 객체가 충분한 추상 인터페이스를 가진다.

 

결과적으로 협력을 구성하는 객체들은 추상적이며 최소한의 단위를 가질 수 있다.

 

행동이 상태를 결정한다.

초보자들은 보통 객체지향적 설계를 하면, 객체의 상태를 행동보다 먼저 정의한다. 객체의 상태를 보고 행동을 결정짓게 되면 내부 구현이 외부 인터페이스에 노출될 가능성이 높아져 캡슐화를 저해한다. 이는 곧 변경이 어려운 설계를 야기한다.

 

캡슐화를 지키기 위해서는 협력이라는 관점에서 객체를 생각하자. 협력 속에서 다른 객체에게 무엇을 제공하고, 다른 객체로부터 어떤 것을 얻어야 하는지 고민한다. 

 

다시 말하지만 협력 -> 행동 -> 상태 순으로 생각하여 객체를 설계한다.

 

역할

위에서 말한 책임들이 곧 객체의 '역할'을 결정한다. 다른말로 객체가 특정 협력 속에서 수행하는 책임들의 집합이 역할이다.

 

정보 전문가 패턴에서 메시지를 결정하고, 바로 객체를 선택했는데 정확히는 메시지를 받을 역할을 결정하고 그 역할을 수행할 객체를 선택한 것이다.

 

그렇다면 왜 굳이 객체를 두고 역할이라는 새로운 개념을 도입하는 걸까?

역할을 도입하면 유연하고 재사용가능한 협력을 얻을 수 있다.

 

역할이 없다고 가정하고 예시를 보자.

심사대상 여부를 판단해야 할 대상이 캐피탈과 우리은행 두 객체이므로 협력이 두 개로 나뉜다.

 

이렇게 구현하면 중복 코드가 생기는 것이므로 좋지 않다.

 

위 문제를 해결하기 위해 다시 '책임'에 초점을 맞춰보자. 두 객체 모두 심사대상여부를 판단하는 책임을 수행하므로 이 메시지를 받는 공통 슬롯을 생각해볼 수 있다.

 

이 슬롯이 곧 역할이다. 동일한 책임을 수행하는 객체를 역할을 기반으로 하나의 협력으로 통합할 수 있다.

 

역할의 구현

역할은 추상 클래스와 인터페이스로 구현이 가능하다.

추상 클래스는 책임을 일부 구현한 것이고, 인터페이스는 일체 구현 없이 책임의 집합만을 나열한다.

역할의 구현 뿐만 아니라 구체적인 객체들의 타입을 캡슐화한 추상화에 해당한다. 따라서 우선 협력 내에서 어떤 책임을 수행할지를 먼저 결정하자. 역할의 구현은 그다음 문제이다.

 

객체 vs 역할

기능에 따라 한 종류의 객체만이 협력에 참여하는 경우가 있다. 이런 경우에는 굳이 역할이 필요없는데 역할이라는 개념이 유용할까?

책에 따르면 책임을 수행하는 대상이 한 종류라면 객체로, 여러 객체가 책임을 수행할 수 있다면 역할로 구분짓는다.

 

또다른 견해로 협력, 역할, 객체, 클래스 간의 관계를 설명하기도 한다.

협력은 역할들의 상호작용으로 구성되고, 역할을 수행할 적합한 객체가 선택되며, 객체는 클래스를 통해 구현된다.

 

그래도 중요한 것은 책임이다.

설계 시 어떤 것을 객체로 보고, 역할로 볼지 불명확하다. 그러나 책임을 수행할 후보가 역할인지 객체인지는 먼저 고려되지 않아도 된다.

설계 초반에는 책임과 협력을 기반으로 큰 그림을 먼저 그리고, 반복적으로 정제해나가면서 그 후보가 역할인지 객체인지 정해도 된다.

더 단순하게는 먼저 모두 객체로 정의하고, 두 종류 이상의 객체가 동일한 책임을 수행할 때 그 객체를 역할로 변경한다.

 

역할과 추상화

2장에서 추상화의 장점에 대해 언급했었다.

1. 요구사항의 정책을 높은 수준에서 단순하게 서술할 수 있다.

2. 설계가 유연해진다.

 

이 두 장점을 역할의 장점으로 보아도 무방하다. 역할을 객체의 추상화로 볼 수 있기 때문이다.

 

1번 장점부터 알아보자.

위의 예시에서 WhiteList는 구체적으로 어떤 은행, 증권사인지 나열하지 않고도 설명이 가능하다. 

그저, 은행과 증권사들이 WhiteList 역할을 수행할 수 있다는 것만 알고 있어도 된다. 

이렇게 구체적인 조건을 떼놓고 상위 수준에서 단순하게 협력을 설명할 수 있다.

 

역할이 동일한 책임을 수행하는 객체들을 추상화할 수 있었기 때문에 가능하다.

 

두번째 장점은 설계가 유연해진다는 것이다.

만약, C은행이 새롭게 추가되어 심사대상을 조회해야할 경우, 동일한 책임을 가지고 있는 새로운 객체를 생성하여 위 협력에 참여시킬 수 있다.

따라서 역할은 다양한 환경에서 다양한 객체들을 수용할 수 있게 해주므로 협력을 유연하게 만든다.

 


스터디 발표자료

03.pdf
1.65MB

 

참고하면 좋은 글들

- 변경에 유연한 컴포넌트