
04장: 역할, 책임, 협력
우리 모두를 합친 것보다 더 현명한 사람은 없다
- Ken Blanchard
인간이 가지고 있는 본연의 특성이라는 관점에서 인간은 이기적이고 합리적인 존재다. 그러나 타인과 관계를 맺는 과정 속에서 인간은 본연의 특성을 배제하고 자신의 이익을 최소화하는 불합리한 선택을 하게 된다.
결국 인간이 어떤 본질적인 특성을 지니고 있느냐가 아니라 어떤 상황에 처해 있느냐가 인간의 행동을 결정한다. 즉, 각 개인이 처해 있는 정황 또는 문맥(context)이 인간의 행동 방식을 결정한다.
여기서 인간의 행동을 결정하는 문맥은 타인과의 협력이다. 협력에 얼마나 적절한지에 따라 행동의 적합성이 결정되며 협력이라는 문맥이 인간의 행동 방식을 결정하는 것이다.
객체 세계에서도 협력이 객체의 행동 방식을 결정한다. 중요한 것은 개별 객체가 아니라 객체들 사이에 이루어지는 협력이다. 객체지향 설계의 전체적인 품질을 결정하는 것은 개별 객체의 품질이 아니라 여러 객체들이 모여 이뤄내는 협력의 품질이다.
협력은 한 사람이 다른 사람에게 도움을 요청할 때 시작된다. 요청을 받은 사람은 일을 처리한 후 요청한 사람에게 필요한 지식이나 서비스를 제공하는 것으로 요청에 응답한다.
객체지향의 세계는 동일한 목적을 달성하기 위해 협력하는 객체들의 공동체다.
앨리스 이야기 속 하트 잭의 재판 과정을 보면, 재판에 참여하는 많은 사람들이 요청하고 응답하는 과정 속에서 이루어진다.

등장인물들이 특정한 요청을 받아들일 수 있는 이유는, 그 요청에 대해 적절한 방식으로 응답하는 데 필요한 지식과 행동 방식을 가지고 있기 때문이다. 그리고 요청과 응답은 협력에 참여하는 객체가 수행할 책임을 정의한다.
객체지향에서는 어떤 객체가 어떤 요청에 대해 대답해줄 수 있거나 적절한 행동을 할 의무가 있는 경우, 해당 객체가 책임을 가진다고 한다.
객체지향 개발에서 가장 중요한 능력은 책임을 능숙하게 소프트웨어 객체에 할당하는 것
- Craig Larman
책임을 어떻게 구현할 것인가는 객체와 책임이 자리 잡은 후 고려하는 것이 좋다.
협력에 참여하는 객체들은 목표를 달성하는 데 필요한 책임을 수행한다. 책임은 객체에 의해 정의되는 응집도 있는 행위의 집합으로, 객체가 알아야 하는 정보와 객체가 수행할 수 있는 행위에 대해 서술한 문장이다. 즉, 객체의 책임은 '객체가 무엇을 알고 있는가(knowing)'와 '무엇을 할 수 있는가(doing)'로 구성된다.
Larman은 객체의 책임을 크게 '하는 것'과 '아는 것'으로 분류한다.
하는 것(doing)
아는 것(knowing)
책임은 객체지향 설계의 품질을 결정하는 가장 중요한 요소다. 객체의 책임은 일반적으로 외부에서 접근 가능한 공용 서비스의 관점에서 이야기한다. 즉, 책임은 객체 외부에 제공해줄 수 있는 정보와 서비스의 목록이다. 따라서 책임은 객체의 공용 인터페이스를 구성한다.
협력 안에서 객체는 다른 객체로부터 요청이 전송됐을 경우에만 자신에게 주어진 책임을 수행한다. 객체가 다른 객체에게 주어진 책임을 수행하도록 요청을 보내는 것을 메시지 전송(message send)이라고 한다. 따라서 두 객체 간 협력은 메시지를 통해 이루어진다.
메시지를 전송해 협력을 요청하는 객체를 송신자, 메시지를 받아 요청을 처리하는 객체를 수신자라고 한다. 메시지는 협력을 위해 한 객체가 다른 객체로 접근할 수 있는 유일한 방법이다.
책임 : 협력 속 요청 수신 객체의 관점에서 무엇을 할 수 있는지 나열
메시지 : 협력에 참여하는 두 객체 사이의 관계를 강조
책임과 메시지의 수준이 같은 것은 아니다. 책임은 객체가 협력에 참여하기 위해 수행해야 하는 행위를 상위 수준에서 개략적으로 서술한 것이다. 책임을 결정한 후 실제로 협력을 정제하면서 이를 메시지로 변환할 때는 하나의 책임이 여러 메시지로 분할되는 것이 일반적이다.
책임과 협력 구조 먼저 설계 후에 책임을 구현하는 것이 좋다.
객체지향 설계는 협력에 참여하기 위해 어떤 객체가 어떤 책임을 수행해야 하고 어떤 객체로부터 메시지를 수신할 것인지를 결정하는 것으로부터 시작된다.
협력의 관점에서 어떤 객체가 어떤 책임의 집합을 수행한다는 것은, 그 객체의 역할을 수행한다는 것을 의미한다.
역할은 재사용 가능하고 유연한 객체지향 설계의 중요한 구성요소다.

위와 같이 협력에 참여하는 등장인물들을 제외한 나머지 과정이 유사해 하나의 협력으로 다루고 싶을 경우, '판사', '증인'이라는 역할(role)을 사용하면 세 가지 협력을 모두 포괄할 수 있는 하나의 협력으로 추상화할 수 있다.

역할을 이용해 협력을 추상화했기 때문에 '판사'나 '증인'의 역할을 수행할 수 있는 어떤 객체라도 협력에 참여할 수 있다. 협력 안에서 역할은 "이 자리는 해당 역할을 수행할 수 있는 어떤 객체라도 대신할 수 있습니다"라고 말하는 것과 같다. 단, 역할을 대체하기 위해서는 각 역할이 수신할 수 있는 메시지를 동일한 방식으로 이해해야 한다.
즉, 동일한 역할을 수행하는 객체들이 동일한 메시지를 수신할 수 있기 때문에 동일한 책임을 수행할 수 있다.
역할의 개념을 사용하면 다양한 객체들이 협력에 참여할 수 있어 좀 더 유연해지며 재사용성이 높아진다. 따라서 역할은 객체지향 설계의 단순성(simplicity), 유연성(flexibility), 재사용성(resuability)을 뒷받침하는 핵심 개념이다.
역할의 가장 큰 가치는 하나의 협력 안에 여러 종류의 객체가 참여할 수 있게 함으로써 협력을 추상화할 수 있다는 것이다. 협력의 추상화는 설계자가 다뤄야 하는 개수를 줄이는 동시에, 구체적인 객체를 추상적인 역할로 대체함으로써 협력의 양상을 단순화한다.
역할은 협력 안에서 구체적인 객체로 대체될 수 있는 추상적인 협력자다. 따라서 본질적으로 역할은 다른 객체에 의해 대체 가능함을 의미한다.
객체가 역할을 대체하기 위해서는 협력 안에서 역할이 수행하는 모든 책임을 동일하게 수행할 수 있어야 한다. 객체는 역할에 주어진 책임 이외에 다른 책임을 수행할 수도 있다. 보통 객체의 타입과 역할 사이에는 일반화/특수화 관계가 성립한다.
역할의 대체 가능성 = 행위 호환성 = 동일한 책임 수행
많은 사람들은 시스템에 필요한 데이터를 저장하기 위해 객체가 존재한다는 선입견을 갖고 있다. 물론 객체가 상태의 일부로 데이터를 포함하는 것은 맞지만, 데이터는 단지 객체가 행위를 수행하는 데 필요한 재료일 뿐이다.
객체가 존재하는 이유는 행위를 수행하며 협력에 참여하기 위해서다. 실제로 중요한 것은 객체의 행동 = 책임이다.
또 다른 선입견은 객체지향이 클래스와 클래스 간 관계를 표현하는 정적인 측면에 중점을 둔다는 것이다. 중요한 것은 정적 클래스가 아닌, 협력에 참여하는 동적 객체이며, 클래스는 단지 시스템에 필요한 객체를 표현하고 생성하기 위해 프로그래밍 언어가 제공하는 구현 매커니즘일 뿐이다.
객체지향 입문자들이 데이터나 클래스를 중심으로 애플리케이션을 설계하는 이유는 '협력'이라는 문맥을 고려하지 않고 각 객체를 독립적으로 바라보기 때문이다.
올바른 객체를 설계하기 위해서는 견고하고 깔끔한 협력을 설계해야 한다. 협력을 설계한다는 것은 설계에 참여하는 객체들이 주고 받을 요청과 응답의 흐름을 결정한다는 것이다. 이렇게 결정된 요청과 응답의 흐름은 객체가 협력에 참여하기 위해 수행될 책임이 된다.
객체에 책임을 할당하면, 그 책임은 객체가 외부에 제공하게 될 행동이 된다. 행동을 결정한 후에 행동을 수행하는 데 필요한 데이터를 고민해야 한다. 그리고 객체가 협력에 참여하기 위해 필요한 데이터와 행동이 결정된 후에 클래스 구현 방법을 정해야 한다.
협력에 필요한 책임들을 식별하고 적합한 객체에게 책임을 할당하는 방식으로 애플리케이션을 설계한다.
객체지향 시스템의 목적은 사용자의 요구를 만족시킬 수 있는 기능을 제공하는 동시에 이해하기 쉽고, 단순하며, 유연한 상호작용을 제공하는 객체들의 공동체를 구축하는 것이다.
결국 객체지향 설계란 애플리케이션의 기능을 구현하기 위한 협력 관계를 고안하고, 협력에 필요한 역할과 책임을 식별한 후 이를 수행할 수 있는 적절한 객체를 식별해 나가는 과정이다.
객체지향 설계의 핵심은 올바른 책임을 올바른 객체에게 할당하는 것이다. 시스템 기능을 더 작은 규모의 책임으로 분할하고 각 책임을 수행할 적절한 객체에게 할당한다. 객체가 책임 수행 도중에 스스로 처리할 수 없는 정보나 기능이 필요한 경우 적절한 객체를 찾아 필요한 작업을 요청한다.
책임 주도 설계에서는 시스템의 책임을 객체의 책임으로 변환하고, 각 객체가 책임을 수행하는 중에 필요한 정보나 서비스를 제공해줄 협력자를 찾아 해당 협력자에게 책임을 할당하는 순차적인 방식으로 객체들의 협력 공동체를 구축한다. 이 방식은 개별적인 객체의 상태가 아니라 객체의 책임과 상호작용에 집중한다.
디자인 패턴은 특정 문제를 해결하기 위해 미리 식별해 놓은 역할, 책임, 협력의 템플릿이다. 패턴을 알고 있다면 바퀴를 반복적으로 발명할 필요가 없다. 즉, 패턴은 특정한 상황에서 설계를 돕기 위해 모방하고 수정할 수 있는 과거의 설계 경험이다.
패턴은 해결하려고 하는 문제가 무엇인지를 명확하게 서술하고, 패턴을 적용할 수 있는 상황과 적용할 수 없는 상황을 함께 설명한다.
중요한 것은 클래스와 메서드가 아니라 '협력'에 참여하는 '역할'과 '책임'이다.
테스트를 먼저 작성하고 테스트를 통과하는 구체적인 코드를 추가하면서 애플리케이션을 완성해가는 방식이다. 테스트가 아니라 설계를 위한 기법이다.
테스트-주도 개발의 기본 흐름은 실패하는 테스트를 작성하고, 테스트를 통과하는 가장 간단한 코드를 작성한 후, 리팩토링을 통해 중복을 제거하는 것이다.
이 설계 기법은 객체가 이미 존재한다고 가정하고 객체에게 어떤 메시지를 전송할 것인지에 관해 먼저 생각하라고 충고한다. 하지만 역할, 책임, 협력의 관점에서 객체를 바라보지 않을 경우 이 충고는 무의미하다.
테스트를 작성하는 것이 아니라, 책임을 수행할 객체가 메시지를 수신할 때 어떤 결과를 반환하고 그 과정에서 어떤 객체와 협력할 것인지에 대한 기대를 코드 형태로 작성하는 것이다.
테스트 작성 시 객체의 메서드를 호출하고 반환값을 검증하는 것은 객체가 수행해야 하는 책임에 관해 생각한 것이다. 테스트에 필요한 값을 제공하기 위해 스텁(stub)을 추가하거나, 값을 검증하기 위해 목(mock) 객체를 사용하는 것은 객체와 협력해야 하는 협력자에 대해 코드로 표현한 것이다.