004. (The Essence of Object-Orientation) 4. 객체지향 설계 기법의 기초

4. 객체지향 설계 기법의 기초

효과적으로 객체지향 설계를 수행하기 위한 방법론들은 많이 존재한다.

이번 포스팅에서는 이 객체지향 설계를 역할, 책임, 협력의 관점에서 살펴보도록 하자.

참고 001. (The Essence of Object-Orientation) 1. 협력과 역할 그리고 책임

객체지향을 막 입문한 사람들의 가장 흔한 실수를 협력을 배제하고 객체가 가져야할 상태와 행동부터 고민하는 것이다.

객체의 세계에서는 협력이라는 문맥이 객체의 행동 방식을 결정하기에,

객체지향 설계의 전체적인 품질은 각 객체의 품질이 아닌 여러 객체가 모여 이뤄내는 협력의 품질로 결정된다.

따라서 좋은 객체지향 설계는 객체들간의 요청과 응답 속에서 피어나는 협력에 초점을 맞춰 설계한 것을 의미한다.

즉, 협력이 자리를 잡으면 객체의 행위와 그에 맞는 상태가 저절로 결정되는 것이다.

4.1. 협력 관점에서의 객체지향

협력의 본질은 연쇄적인 요청과 응답으로 연결되는 객체들간의 네트워크이다.

협력은 객체가 다른 객체에 “요청”하는 것으로부터 시작되며, 요청 받은 객체도 요청을 처리하는 도중에 필요하다면 다른 객체에 “요청”을 하기도 한다.

아래 그림과 같이 결과적으로 협력은 다수의 요청과 응답으로 구성되며, 전체적으로 협력은 다수의 연쇄적인 요청과 응답의 흐름으로 구성된다.

graph LR;
    User-->App;
    App-->A;
    A-->B;
    A-->C;
    B-->D;
    C-->D;
    D-->App;
    App-->User;

객체지향의 세계는 동일한 목적을 달성하기 위해 협력하는 객체들의 공동체임을 명심하자.

이번엔 협력의 구성 요소인 요청과 응답에 초점을 맞춰보자.

사용자의 요청이 최초에 객체 A에 전달되었다고 가정한다.

이후 객체 A가 객체 B에게 어떤 메시지를 보내서 “요청”을 하는 경우,

객체 B는 객체 A에게 “응답”을 해야할 의무가 부여된다.

이는 객체 A가 특정 요청을 받아들일 역할 을 가지고 있으며, 그 요청에 대해 객체 B와의 협력 을 통한 어떠한 행위를 할지 정의가 되어있기 때문이다.

객체 B가 객체 A에게, 객체 A가 사용자에게 요청에 대한 응답을 돌려주는 것은 해당 객체가 해당 행위에 대한 책임 을 가지고 있기 때문이다.

4.2. 책임 관점에서의 객체지향

객체지향 개발에서 가장 중요한 능력은 책임을 소프트웨어 컴포넌트에 할당하는 것이다.
(A critical, fundamental ability in OOA/D is to skillfully assign responsibilities to software components.)
ㅡ Applying UML and Patterns(2004), Craig Larman

책임은 객체지향 설계의 가장 중요한 재료다.

중요한 재료이니만큼 객체와 책임이 제 자리를 잡은 후에 어떻게 구현할 것인지 고민해도 늦지않다.

오히려 성급하게 구현을 시작하는 것은 변경에 취약하고 협력성이 저하되는 객체를 만들어낼 수도 있다.

협력에 참여하는 객체들은 목표를 달성하는 데 필요한 책임을 수행하며, 여기서 책임은 객체에 의해 정의되는 응집도 있는 행위의 집합 을 의미한다.

즉 객체의 객침이란 객체가 알아야 하는 정보와 객체가 수행할 수 있는 행위에 대한 서술이며 아는 것과 할 수 있는 것으로 분류할 수 있다.

첫 번째 포스팅에서 미리 언급했던 내용을 다시 가져와보자.

1. 무엇을 알고 있는가?

  • 사적인 정보에 관한 것
  • 관련된 객체에 관한 것
  • 자신이 유도하거나 계산할 수 있는 것

2. 무엇을 할 수 있는가?

  • 객체의 생성
  • 계산 수행
  • 다른 객체의 행동을 시작
  • 다른 객체의 활동을 제어하고 조절

상술했듯 좋은 객체지향 설계는 적절한 객체에게 적절한 책임을 할당하는 데 있다.

책임이 불분명한 객체들은 결국 불분명한 애플리케이션을 만들어내게 된다는 점을 명심하자.

객체의 책임을 이야기할 때는 일반적으로 외부에서 접근 간으한 공용 서비스의 관점에서 다뤄진다.

다시말해 책임은 객체에 외부에 제공해줄 수 있는 정보와 제공해줄 수 있는 서비스로 구성되며, 이는 객체의 공용 인터페이스(public interface)로 표현된다.

4.3. 역할 관점에서의 객체지향

어떤 객체가 수행하는 책임의 집합은 객체가 협력 안에서 수행하는 역할을 암시한다.

현실 세계의 선박, 비행기, 자동차, 기차 등을 떠올려보자.

전혀 다를 것 같지만 위 4개의 사물은 “무언가를 실어 나른다” 라는 책임을 가지고 있고, 이를 통해 “운송 수단”이라는 역할을 가지고 있음을 암시한다.

우리가 운송 수단을 이용해 서울에서 제주도를 간다고 가정해보자.

비행기를 타고 가는 방법, 그리고 배를 타고 가는 방법이 떠오를 것이다.

결국 제주도까지 운송해주는 역할을 맡아, 책임지고 제주도로 갈 수 있게 해준다.

여기서 중요한 것은 배를 타나 제주도를 타나 목적지까지 데려다 준다는 것이다.

즉 역할은 협력 내에서 다른 객체로 대체할 수 있다는 일종의 표식으로 쓰일 수 있다.

물론 역할을 대체할 수 있는 객체는 동일한 요청의 메시지를 이해할 수 있는 객체로 한정될 것이다.

동일한 역할을 수행하는 객체들이 동일한 메시지를 수신할 수 있기에, 동일한 책임을 수행할 수 있다는 것은 매우 중요한 개념이다.

결론적으로 역할의 개념을 사용하면 유사한 협력 관계를 추상화할 수 있고, 다양한 객체 간의 좀 더 유연한 협력을 통해 재사용성을 높여주게 된다.

즉, 역할은 객체지향 설계의 단순성(simplicity), 유연성(flexibility), 재사용성(reusability) 를 뒷받침하는 핵심 개념이다.

4.3.1. 협력의 추상화

역할의 가장 큰 가치는 하나의 협력 안에 여러 종류의 객체가 참여할 수 있게하여 협력을 추상화할 수 있다는 데 있다.

협력의 추상화는 설계자가 다뤄야하는 협력의 개수를 줄이는 동시에 구체적인 객체를 추상적인 역할로 대치하여 협력의 양상을 단순화해준다.

이는 결과적으로 애플리케이션의 설계를 이해하고 기억하기 쉽게 만들어준다.

4.3.2. 대체 가능성

거듭 말하듯이 역할은 다른 객체에의 의한 대체 가능성을 의미한다.

객체가 역할을 대체하기 위해서는 협력 안에서 역할이 수행하는 모든 책임을 동일하게 수행할 수 있어야한다.

여기서 주목할 것은 객체는 역할에 주어진 책임 이외에 다른 책임을 수행할 수도 있다는 사실이다.

객체는 역할이 암시하는 책임보다 더 많은 책임을 가질 수 있기에 대부분의 경우 객체의 타입과 역할 사이에 일반화/특수화 관계가 성립한다.

4.4. 객체의 설계는 협력이 결정한다

이번엔 객체지향에 대한 선입견들에 대해 나열해보자.

  1. 시스템에 필요한 데이터를 저장하기 위해 객체가 존재한다.

데이터는 단지 객체가 행위를 수행하기 위해 필요한 재료일 뿐이며, 객체의 존재 의의는 행위를 통해 협력에 참여하기 위해서이다.

정말 중요한 것은 객체의 행위 즉, 책임이다.

  1. 객체지향은 클래스와 클래스간의 관계를 표현하는 것이다.

클래스는 단지 시스템에 필요한 객체를 표현하고 생성하기 위한 구현 매커니즘이다.

객체지향의 핵심은 클래스의 구현이 아닌, 협력 안에서 객체가 어떤 책임과 역할을 수행할지를 결정하는 것이다.

4.4.1. 협력을 먼저 설계하라

위와 같은 선입견이 존재하는 이유는 애플리케이션을 설계하면서 데이터나 클래스를 중심으로 바라보며, 각 객체를 독립적으로 생각하기 때문이다.

올바른 객체를 설계하기 위해서는 먼저 견고하고 깔끔한 협력을 설계해야 한다.

협력을 설계한다는 것은 설계에 참여하는 객체들이 주고받을 요청과 응답의 흐름을 결정한다는 것을 의미한다.

이렇게 결정된 요청과 응답의 흐름은 객체가 협력에 참여하기 위해 수행할 책임으로 이어진다.

객체에 책임을 할당하고나면 책임은 객체가 외부에 제공하게 될 행위로 이어지므로, 기결정된 행위에 필요한 데이터를 고민하는 것이 순서다.

협력에 참여하기 위한 데이터와 행위가 결정되고나서야 비로소 클래스의 구현 방법을 결정할 순서가 오게 된다.

객체의 행위에 초점을 맞추기 위해서는 협력이라는 실행 문맥 안에서 책임을 분배해야 한다.

따라서 각 객체가 가져야하는 상태와 행위에 대해 고민하기 전에 그 객체가 참여할 협력을 정의한 뒤, 그 문맥안에서 객체를 자율적으로 만들도록 진행해야 한다.

4.5. 객체지향 설계 기법

지금까지 객체지향을 협력, 책임, 역할에 관점에서 살펴보았다.

이제 동일한 관점에서 애플리케이션을 설계하는 세 가지 방법에 대해서 살펴보자.

4.5.1. 책임-주도 설계(Responsibility-Driven Design)

책임-주도 설계 방법은 협력에 필요한 책임들을 식별하고, 적합한 객체에게 책임을 할당하는 방식으로 애플리케이션을 설계한다.

이 방법은 객체지향 패러다임을 추종하는 개발자들이 애플리케이션을 개발할 때 어떤 방식으로 사고하고 무엇을 기반으로 의사결정을 내리는 지 잘 보여준다.

시스템의 기능은 더 작은 규모의 책임으로 분할되고, 각 책임은 책임을 수행할 적절한 객체에게 할당된다.

객체가 책임을 수행하는 도중에 스스로 처리할 수 없는 정보나 기능이 필요한 경우 적절한 객체를 찾아 필요한 작업을 요청한다.

요청한 작업을 수행하는 일은 작업을 위임받은 객체의 책임으로 변환된다.

이때 객체와 다른 객체간에 작업을 요청하는 행위를 통해 객체들간의 협력 관계가 만들어지게 된다.

책임-주도 설계 방법하에 시스템을 설계하는 절차는 아래와 같다.

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

4.5.2. 디자인 패턴(Design Pattern)

디자인 패턴은 개발자들이 어떤 문제에 대해 반복적으로 사용하는 문제 해결 방법을 정의해 놓은 설계 템플릿의 모음이다.

일반적으로 디자인 패턴은 반복적으로 발생하는 문제와 그 문제에 대한 해법의 쌍으로 정의되며,

먼저 해결하려고 하는 문제가 무엇인지를 명확하게 서술하고 디자인 패턴을 적용할 수 있는 상황과 적용할 수 없는 상황을 함께 설명한다.

다시 말해 특정 문제를 해결하기 위해 이미 식별해놓은 역할, 책임, 협력의 모음이라고 볼 수 있다.

4.5.3. 테스트-주도 개발(Test-Driven Development)

테스트-주도 개발은 테스트를 먼저 작성하고 테스트를 통괗라는 구체적인 코드를 추가하면서 애플리케이션을 완성해가는 방식을 따른다.

이름은 개발 방법론같지만, 사실은 설계를 위한 기법으로 테스트를 통과하는 구체적인 코드를 작성해나가면서 역할, 책임, 협력을 식별하고

사전에 작성된 테스트 코드를 통해 식별된 역할, 책임, 협력이 적합한지를 피드백받는 것이 목적이다.

이 테스트-주도 개발이 응집도가 높고 결합도가 낮은 클래스로 구성된 시스템을 개발할 수 있는 것은 사실이나, 객체지향에 대한 경험이 적은 경우

어떤 테스트를 어떤 식으로 작성해야하는지 결정하는 데 큰 비용이 들게 된다.

이를 최소화하려면 먼저 역할, 책임, 협력의 관점에서 객체를 바라보고, 이 객체가 실제로 존재한다고 가정한 뒤 어떤 메시지를 전송할 것인지 생각해보는 습관이 필요하다.

테스트-주도 개발의 목적은 테스트 코드를 작성하는 것이 아니라 책임을 수행할 객체 또는 클라이언트가 기대하는 객체의 역할이

메시지를 수신할 때 어떤 결과를 반환하고 그 과정에서 어떤 객체와 협력할 것인지에 대한 기대를 코드로 작성하는 것이기 목적이기 때문이다.