[오브젝트] 오브젝트를 읽고, 3~4장
3장 : 역할, 책임, 협력
2장까지는 객체지향 프로그래밍의 다양한 요소와 구현 기법을 설명했다. 추상화를 통한 유연성 확보라든지, 상속에서는 인터페이스를 상속 받는 것이 핵심이라든지의 내용을 살펴봤다. 지금까지는 객체지향을 구현하는 도구에 대해 살펴보았다면, 이제부터는 객체지향의 본질이 무엇인지 소개한다.
객체지향 패러다임의 핵심은 '역할', '책임', '협력'이다. 앞서 나온 개념들은 구현 측면에 치우친 것이기에, 패러다임의 본질이라고 할 수는 없다. 책을 읽기 전에는 다형성이나 캡슐화 같은 것이 객체지향의 핵심이라고 생각했는데, 그 저변을 관통하는 무엇인가가 있는 것이다. 저자는 객체지향의 본질이 '협력하는 객체들의 공동체를 창조'하는 것이라고 말한다.
협력, 책임, 역할
협력 : 객체들의 어플리케이션의 기능을 구현하기 위해 수행하는 상호작용
책임 : 협력에 참여하기 위해 수행하는 로직
역할 : 협력 안에서 수행하는 책임이 모여 객체가 수행하는 역할
협력
협력은 기능을 구현하기 위한 유일한 방법이고, 객체는 협력을 수행하기 위해 다른 객체에게 메시지를 전송한다. 메시지를 받은 객체는 맡은 일에 대해 전적인 권한을 맡아 처리한다.
각 객체에게는 책임과 역할이 정해져 있다. 한 객체가 기능을 수행하다가 자신의 역할을 넘어가는 상황이 발생하면, 이 때 적절한 책임을 수행할 수 있는 객체에게 도움을 요청한다. 도움을 요청 받은 객체는 이 일에 대해 가장 잘 알고, 가장 잘 할 수 있는 객체이다. 도움의 요청은 메시지의 형태로 전달되며, 메시지를 받은 객체는 메시지를 보낸 객체가 누구인지에 관계없이 자신의 일을 할 뿐이다.
'협력을 요청하는 객체는 메시지를 보낼 뿐이고, 메시지를 받은 객체는 스스로 그 일을 처리한다.' 이처럼 객체들은 메시지에 의해 연결되어 있으며, 메시지를 처리하는 방식, 즉 구현은 서로에게 드러나 있지 않다. 이는 객체 사이의 결합도를 낮춰주고 변경에 대한 유연성을 확보하는 길을 제공한다.
그리고 캡슐화는 '가장 잘 아는 객체, 가장 잘 할 수 있는 객체'를 만들어 준다. 캡슐화를 통해 책임을 수행하기 위한 상태를 숨기고, 구현을 숨긴다면 외부 객체는 퍼블릭 인터페이스를 통해 협력을 요청할 수 있을 뿐이다.
객체가 하는 행동을 기준으로 생각할 때, 협력이란 객체의 행동을 결정하는 Context이다. 협력을 하기 위해 객체가 존재하는 것이며, 협력을 하기 위한 행동을 구현하게 된다. 그리고 이러한 행동을 하기 위해서는 그 행동에 필요한 상태에 대해 알아야 한다. 결국 협력이 객체의 행동과 상태를 결정하는 것이다.
책임
책임은 협력에 참여하기 위해 객체가 수행하는 행동이다. 책임에는 행동과 더불어 객체가 알고 있어야 하는 정보도 포함한다. 객체를 설계하기 위한 문맥이 형성되면, 이 협력을 가장 잘 수행할 수 있는 객체, 책임을 가진 객체를 찾을 수 있다.
협력을 통해 책임을 할당할 수 있지만, 협력보다 중요한 것이 책임이다. 객체지향에서 가장 중요한 것은 객체에게 얼마나 적절한 책임을 부여하느냐는 것이다.
책임을 할당하기 위한 방법으로 '정보 전문가 패턴'이 있다. 여기에서 정보는 '지식'과 '방법' 모두를 뜻한다. 지닌 바 지식을 가지고 어떻게 책임을 수행하는지 아는 객체에게 책임을 할당할 수 있다.
전체적인 시스템의 관점에서 책임 할당의 과정을 생각해 볼 수 있다. 우선 사용자가 시스템에 원하는 기능을 생각해보았을 때, 첫 번째 책임을 '영화 예매하기'라고 생각하자. 예매의 책임을 가장 잘 수행할 수 있는 객체를 찾아서 책임을 할당한다. 예매의 책임을 수행하기 위해서 더 작은 책임들이 필요할 수 있다. 예를 들어 상영 클래스는 가격 계산에 대해 충분히 알지 못할 때, '가격을 계산하라'는 메시지를 보내 도움을 요청해야 할 수 있다. 이때 이 메시지에 맞는 정보 전문가를 찾아 책임을 할당하면 된다.
'책임 주도 설계'는 위와 같이 반복적인 과정으로 협력에 필요하는 메시지를 찾고, 가장 잘 수행하는 객체를 찾아 책임을 할당한다. 여기서 결정된 메시지가 퍼블릭 인터페이스를 결정한다. 그리고 구현은 각 객체 스스로에게 맡긴다.
기존의 생각과 달라 인상적인 부분은, 객체가 호출할 메서드를 결정하는 것이 아니라, 메시지가 객체를 선택한다는 것이다. 이는 설계 상으로 2가지 장점이 있다. 첫째는 메시지는 그 자체로 수행해야 할 책임의 일부이기 때문에, 객체의 퍼블릭 인터페이스를 최소한으로 유지할 수 있다. 꼭 필요한 기능이 무엇인지 알 수 있기에 캡슐화가 간편히 수행된다. 둘째로 추상적인 인터페이스를 확보하여, 구현으로부터 격리할 수 있다. 메시지가 먼저 추상적으로 결정되기 때문에, 구현은 객체 내부에 숨길 수 있다. 애초에 메시지가 먼저 결정된 후에 구현을 시작했기에, 이후에 구현을 변경하는 것도 용이할 것이다.
데이터 주도 설계와 다른 점은, 행동이 상태를 결정한다는 것이다. 데이터의 집합이 행동을 부여하는 것이 아니다. 따라서 상태에 억지로 행동을 끼워맞출 필요가 없게 되는 것이다. 이는 객체가 적절한 책임을 갖게 한다.
역할
역할은 객체가 수행할 책임의 집합이다. 앞서 책임을 충분히 살펴 봤으니 역할에 대해서는 그 모음으로 생각해도 크게 문제 없다. 주의깊게 보면 좋을 부분은 '추상화'이다. 객체지향에서 추상화란 다형성, 즉 특정 메시지에 대해 서로 다른 메서드가 실행될 수 있는 가능성을 만들어준다. 이는 특정 메시지에 여러 구현이 교체되어 실행될 수 있다는 것이고, 구현이 수행하는 목표를 조금 더 고수준에서 바라볼 수 있게 한다. 추상화를 사용한다면 객체의 '역할'을 명시적으로 표현할 수 있다.
4장 : 설계 품질과 트레이드오프
객체지향 설계란 올바른 객체에게 올바른 책임을 할당하며 낮은 결합도와 높은 응집도를 가진 구조를 창조하는 활동이다(Marcs Evers). 그리고 훌륭한 설계란 합리적인 비용 안에서 변경을 수용할 수 있는 구조를 만드는 것이다. 구조를 만드는 비용이 변경이 감소하는 비용보다 크다면 구조에 드는 비용을 줄일 수 있을 것이다. 그러나 대부분의 소프트웨어는 지속적으로 요구사항이 변하기 마련이다.
이 장에서는 데이터 주도 설계와 책임 주도 설계를 비교하고, 캡슐화 - 결합도 - 응집도의 관점에서 설명한다.
객체의 상태는 구현에 속한다. 구현은 인터페이스에 변하기 쉽다. 그러나 객체의 책임은 인터페이스이고 추상적이다. 상태보다는 비교적 안정적이다. 이론적으로 책임에 초점을 맞추면 비교적 변경에 안정적인 설계를 얻을 수 있다.
데이터 주도 설계
데이터 주도 설계는 '기능을 구현하기 위해 객체의 내부에 저장해야 하는 데이터가 무엇인가?'라는 질문으로 시작한다. 관련도 높은 데이터를 한 객체에 모으는 방식으로 시작한다. 익히 알기로 캡슐화를 지키기 위해 get과 set 메서드를 통해서만 객체의 상태에 접근하도록 한다. 이것이 캡슐화를 잘 지키는 방식인지는 이후에 알아보자.
캡슐화
캡슐화는 한 곳에서 일어난 변경이 다른 곳에 영향을 미치지 않게 경계를 긋는 역할이다. 관련도 높은 상태와 행동을 한 객체에 모으고, 변경사항이 발견되면 해당 객체만 수정한다. 여기서 변경될 가능성이 높은 부분이 구현부이다. 불안정한 구현부를 캡슐화를 통해 숨길 필요가 있다. 안정적인 부분만 참조한다면 다른 객체들은 해당 변경에서 비교적 안전할 수 있다.
이처럼 캡슐화의 핵심은 변경 가능성이 높은 부분을 객체 내부로 숨기는 추상화 기법이다. 이는 유지보수에 대한 두려움을 줄이고 soft한 제품이 탄생하도록 만들어준다.
응집도와 결합도
응집도는 모듈에 포함된 요소들이 연관돼 있는 정도를 말한다. 결합도는 모듈 간에 존재하는 의존성을 정도를 말하며, 서로에 대해 얼마나 알고 있는지에 대한 것이다.
응집도와 결합도는 변경과 관련이 깊다. 응집도를 측정하기 위해서 변경이 발생할 시 하나의 모듈에서 얼마나 많은 변경이 일어나는지 확인할 수 있고, 결합도를 측정하기 위해서 변경이 발생할 시 다른 모듈이 얼마나 변경되어야 하는지 확인할 수 있다.
높은 응집도와 낮은 결합도는 변경에 유연성을 제공한다. 그리고 이것의 기본은 캡슐화이다.
데이터 주도 설계의 문제점
데이터 주도 설계는 데이터를 먼저 생각한다. 따라서 어떤 맥락에서 해당 객체가 사용할 수 있을지 알 수 없다. 책임 주도 설계에서는 협력이 이러한 맥락을 제공했기에, 공개할 인터페이스를 명시할 수 있었다. 데이터 주도 설계에서는 이에 대한 막연한 추측으로 공개 인터페이스를 선정해야 하기에, 많은 부분을 공개할 수밖에 없다. 자연스럽게 캡슐화의 실패로 이어진다.
또한 상태에 대해 get 또는 set을 통해 접근할 수 있다. 이는 사실 객체가 가징 구현에 대해 모두 드러내는 것과 다를 바가 없다. 함수의 이름에서 어떤 프로퍼티가 존재하는지 추측이 가능하다. 또한 접근 시에 프로퍼티의 상세 구현에도 접근이 가능하다. 마찬가지로 캡슐화가 이루어지고 있지 않다.
객체의 상태에 무분별하게 접근할 수 있기 때문에, 관련성이 먼 객체에서 특정 객체에 접근할 수 있고, 그렇게 되면 중앙 집중화된 '책임'이 특정 객체에 생길 수 있다. 앞서 강조했던 객체의 자율성이 떨어지게 되고, 각 객체의 결합도는 높아지고 응집도는 떨어진다. 결국 변경에 유연하지 못한 설계로 이어진다.
이러한 문제들이 발생한 근본적인 이유는, 데이터 주도 설계가 너무 이른 시기에 객체의 구현을 결정하도록 한다는 것이다. 구현이 이른 시기에 결정되었다는 사실은 그 구현을 기반으로 한 또다른 설계들이 등장할 가능성이 높아진다는 것이다. 구현을 미룰수록 서로 의존하는 구현이 줄어들고, 변경하기 쉬운 구조가 만들어진다. 또한 상태를 먼저 결정했기에, 필요한 인터페이스를 상태에 끼워맞추게 된다. 적절하지 못한 책임이 부여될 가능성이 높아진다.