////
Search
Duplicate
🧿

7. 함께 모으기

마틴 파울러는 자신의 저서 UML Distilled 2판에서 객체지향 설계 안에 존재하는 세 가지 상호 연관된 관점에 대해 설명한다. 각각 개념 관점, 명세 관점, 구현 관점이라 부른다.
개념 관점
해당 관점에서 설계는 도메인 안에 존재하는 개념과 개념들 사이의 관계를 표현한다.
이 관점은 사용자가 도메인을 바라보는 관점을 반영한다. 이때문에 도메인 관점이라고도 한다.
명세 관점
이에 이르면 사용자의 영역이 도메인을 벗어나 개발자의 영역인 소프트웨어로 초점이 옮겨진다.
명세 관점은 도메인의 개념이 아닌 실제 소프트웨어 안의 객체들의 책임에 초점을 맞추게 된다.
즉, 객체의 인터페이스를 바라보게 된다. 명세 관점에서 프로그래머는 객체가 협력을 위해 무엇을 제공할 수 있는가에 초점을 맞춘다.
구현 관점
우리 프로그래머에게 가장 익숙한 관점으로 실제 작업을 수행하는 코드와 연관되어 있다.
구현 관점의 초점은 객체들이 책임을 수행하는 데 필요한 동작 코드들을 작성하는 것이다. 즉, 객체의 책임을 어떻게 수행할 것인가에 초점을 맞추며 인터페이스를 구혀하는 데 필요한 속성과 메소드를 클래스에 추가한다.
이 관점들은 어떠한 절차나 순차에 따라 개발이 이뤄지는 것이 아닌, 클래스를 바라보는 세 가지 관점이 있다는 것을 의미한다.
클래스는 세 가지 관점을 통해 설계와 관련된 다양한 측면들을 확인할 수 있는데, 클래스가 은유하는 개념은 도메인 관점을, 클래스의 공용 인터페이스는 명세 관점을, 클래스의 속성과 메소드는 구현 관점을 반영한다.
이를 통해 우리는 클래스 설계에 관한 가장 큰 힌트를 얻을 수 있는데, 바로 이 세 가지 관점이 명확하게 분리될 수 있도록, 서로 영향을 받지 않도록 식별이 가능하게 해야한다.
다음의 간단한 예제를 통해서, 두 가지 목표를 달성하고자 한다.
하나는 도메인 모델에서 시작해 최종 코드의 구현 과정을 간략하게나마 설명하는 것이고 나머지 하나는 클래스를 관점들에서 바라보는 것이 의미하는게 뭔지 설명하는 것이다.

커피 전문점 도메인

커피 주문

예제의 목적은 커피 전문점에서 커피를 주문하는 과정을 먼저 객체들의 협력 관계로 구현하는 것이다.
예제
커피 전문점에서는 아메리카노, 카푸치노, 카라멜 마끼아또, 에스프레소의 네 메뉴를 판매하고 있다.
손님이 테이블에 앉아 메뉴판을 잠시 훑어본 후, 바리스타에게 커피를 주문한다. 바리스타는 주문받은 커피를 제조한다.

커피 전문점이라는 세상

객체지향 패러다임의 가장 중요한 도구는 객체이므로 이 설명을 통해 객체를 판별하여 커피 전문점이라는 도메인을 객체들로 구성된 작은 세상으로 확인해보는 것이 좋다.
먼저 커피 전문점 안에는 메뉴판이 존재한다. 메뉴판에는 아메리카노, 카푸치노, 카라멜 마끼아또, 에스프레소의 네 가지 커피 메뉴가 적혀있다.
객체지향의 관점에서 메뉴판은 하나의 객체고 네 개의 메뉴 항목 역시, 객체로 볼 수 있다. 따라서 메뉴판은 네 개의 메뉴 항목 객체들을 포함하는 객체라고 볼 수 있다.
손님은 메뉴판을 보고 바리스타에게 원하는 커피를 주문한다.
객체지향의 관점에서 손님 객체는 메뉴판 객체 안에 적힌 메뉴 항목 중, 하나를 선태갛여 바리스타 객체에게 이를 전달한다.
바리스타는 주문을 받은 메뉴에 따라 적절한 커피를 제조한다. 바리스타가 제조할 수 있는 커피의 종류는 메뉴판에 적혀있는 4가지다.
객체지향의 관점에서 바리스타 객체는 커피를 제조하는 방식을 자율적인 객체로 바리스타가 제조하는 커피 역시, 메뉴판, 메뉴 항목 등의 객체들과 구별되는 자신만의 경계를 가지므로 객체라 볼 수 있다.
이를 종합해보면 객체지향의 관점에서 커피 전문점이라는 도메인은 손님 객체, 메뉴 항목 객체, 메뉴판 객체, 바리스타 객체, 커피 객체로 이뤄진 작은 세상이다.
어떤 객체들이 있는지 알아봤으니 이제 객체들 간의 관계를 구성해야 한다.
손님은 메뉴판에서 주문할 커피를 선택할 수 있어야 한다. 따라서 손님은 어떤 식으로든 메뉴판을 알고 있어야 한다.
이는 두 객체 사이에 관계가 존재한다는 것을 암시한다.
손님은 바리스타에게 주문을 해야 하므로 손님과 바리스타 사이에도 관계가 존재한다.
바리스타는 커피를 제조하므로 당연하게도 자신이 만든 커피와 관계를 맺는다.
객체는 타입의 인스턴스로 손님 객체는 손님 타입의 인스턴스로, 바리스타 객체는 바리스타 타입의 인스턴스이다. 이를 통해 타입을 구성할 수 있다.
커피 전문점을 구성하는 범주로 타입들이 갖춰지고, 타입 간에 어떤 관계가 존재하는 지 파악해야 한다.
메뉴판 객체는 메뉴 항목 객체로 구성되어 있다. 메뉴들은 따로 떨어져 존재하지 않고 하나의 단위로 인식되어야 하므로 메뉴 항목 객체가 메뉴판 객체에 포함되어 있다고 할 수 있다.
이는 메뉴 항목에서 메뉴판으로 향하는 선에 그려진 속이 찬 마름모로 표현되는데 이는 포함 또는 합성 관계를 나타낸다.
손님 타입은 메뉴판 타입을 알고 있어야 하므로 단순한 선으로 연결되는데, 이처럼 한 타입의 인스턴스가 다른 타입의 인스턴스를 포함하지는 않으나 서로 알고 있어야 할 경우로 이는 연관 관계라고 표현한다.
바리스타 타입과 커피 역시 연관 관계이며 손님과 바리스타 역시 연관 관계다.
이렇게 커피 전문점이라는 도메인을 단순화하여 이해했으므로 이제는 이 도메인 관점을 소프트웨어로 옮길 시간이다.
참고
실제로 도메인 모델을 작서앟는 단계에서 포함이고 연관이고 관계가 중요하지는 않다.
초점은 어떤 타입이 도메인을 구성하고 있느냐와 타입들 사이에 어떤 관계가 존재하는지 파악함으로써 도메인을 이해하는 것이다.
실제로는 관계가 존재한다고 인식하는 것만으로도 충분하다.

설계하고 구현하기

커피를 주문하기 위한 협력 찾기

객체지향의 첫 번째 목표는 훌륭한 협력을 설계하는 것이라는 점을 잊지 말자. 훌륭한 객체는 훌륭한 협력을 설계할 때 비로소 얻을 수 있다.
협력을 설계하는 과정에서 우리는 객체가 메시지를 선택하지 않고 메시지가 객체를 선택하게끔 해야 한다.
이 말은 메시지를 먼저 선택하고 그 수신자를 이후에 결정해야 한다는 것을 의미한다.
수신자는 메시지 처리에 대한 책임을 갖게 되고 객체가 수신하는 메시지는 수신 객체가 외부에 제공하는 공용 인터페이스에 명시하게 된다.
현재 설계하고 있는 협력은 커피를 주문하는 것이다. 따라서 우리는 첫 번째 메시지가 커피를 주문한다임을 짐작해볼 수 있다.
메시지를 찾았으니 이제 수신자를 찾아야 한다. 어떤 객체에 은유해야 할까?
앞서 설계한 도메인 모델을 바탕으로 우리는 수신자가 손님임을 알 수 있다. 즉 손님이 커피를 주문하는 책임을 가져야 한다는 것이다.
그렇다면 자연스럽게 다음으로 넘어가 손님이 커피를 주문하는 과정에서 스스로 처리할 수 없는 일은 뭘까? 손님은 메뉴 항목을 알지 못한다. 따라서 자신이 메뉴를 고르기 위해 메뉴 항목을 제공해줄 것을 요청해야 한다.
여기서 우리는 새로운 메시지인 메뉴 항목을 찾아라를 얻을 수 있다.
이 경우, 메시지에 메뉴 이름이라는 인자를 포함해 함께 전송한다. 이 메시지를 수신한 객체는 메뉴 이름에 대응되는 메뉴 항목을 반환해야 한다.
메뉴 항목을 찾는 책임은 어떤 객체가 가져야할까? 짐작하고 있듯이 메뉴판이 담당한다. 메뉴판 객체는 메뉴 항목을 포함하고 있으므로 이 책임을 가장 잘 처리할 수 있는 수신자다.
이때, 능동적인 객체임을 생각하자. 송신자가 고르지 않는다. 단지 요청에 따른 반환을 해줄 뿐이다. 현실의 메뉴판은 수동적이지만 객체지향 세계에서의 메뉴판은 스스로 메뉴를 결정해 반환해줄 수 있다.
손님 객체는 이제 무슨 커피를 주문할 지도 알게 되었고 커피 주문을 위한 다음 메시지를 만든다. 이는 커피를 제조하라다.
손님은 이제 커피를 제조하는 메시지의 인자로 메뉴 항목을 전달하고 반환 값으로 커피를 받아야 한다.
누가 이 메시지의 수신자일까? 당연히 바리스타다.
⇒ 협력 자체가 어렵지 않은 것도 있으나 도메인 모델을 잘 정리해두었기 때문도 있는 것 같다.
바리스타는 커피를 제조하는 데 필요한 모든 정보를 알고 있다. 이로써 커피 주문을 위한 협력은 바리스타가 커피를 만들어 손님에게 반환함으로써 종료된다.
협력에 필요한 객체의 종류와 책임, 주고 받아야 하는 메시지에 대한 대략적인 윤곽이 정리되었다. 남은 일은 메시지를 정제함으로써 각 객체의 공용 인터페이스를 구현 가능할 정도로 상세하게 결정하는 것이다.

인터페이스 정리하기

우리가 설계한 협력은 객체들의 인터페이스다. 객체가 수신한 메시지가 객체의 인터페이스를 결정한다는 사실을 기억하자. 메시지가 객체를 선택했고 선택된 객체는 메시지를 자신의 인터페이스로 받아들인다.
여기서 각 객체가 수신 가능한 메시지만 추려내면 객체의 인터페이스가 결정된다. 객체가 어떤 메시지를 수신할 수 있다는 것은 그 객체의 인터페이스 안에 메시지에 해당하는 실행 절차가 존재한다는 것을 의미한다.
손님 객체의 인터페이스에는 커피를 주문하라라는 연산이 제공된다.
메뉴판 객체의 인터페이스에는 메뉴 항목을 찾아라라는 연산이 제공된다.
바리스타 객체의 인터페이스는 커피를 제조하라라는 연산이 제공된다.
커피 객체의 인터페이스는 생성하라라는 연산이 제공된다.
객체들의 협력은 런타임에 컴퓨터에서 일어나는 상황을 동적으로 묘사한 모델이다. 실제로 소프트웨어의 구현은 동적인 객체가 아닌 정적인 타입을 이용해 명시된다.
즉, 객체들을 포괄하는 타입을 정의한 후 식별된 오퍼레이션을 타입의 인터페이스에 추가해야 한다.
객체의 타입을 구현하는 일반적인 방법은 클래스를 사용하는 것이다.

구현하기

구현 도중에 객체의 인터페이스가 변경될 수 있음을 유연하게 받아들이자.

코드와 세 가지 관점

코드는 세 가지 관점을 모두 제공해야 한다

개념 관점에서 바라본 코드
Customer, Menu, MenuItem, Barista, Coffee 클래스가 해당한다.
이 클래스들을 자세히 살펴보면 커피 전문점 도메인을 구성하는 중요한 개념과 관계를 반영한다는 사실을 알 수 있다.
이처럼 소프트웨어 클래스가 도메인 개념의 특성을 최대한 수용하면 변경을 관리하기 쉽고 유지보수성을 향상시킬 수 있다.
즉, 소프트웨어 클래스와 도메인 클래스 사이의 간격이 좁으면 좁을수록 변경사항이 발생했을 때, 수정 범위가 줄어든다.
명세 관점에서 바라본 코드
명세 관점은 클래스의 인터페이스를 바라본다. 즉 클래스가 공개하고 있는 public 속성들을 의미한다.
이는 다른 클래스가 협력할 수 있는 공용 인터페이스로, 외부의 객체가 해당 객체로 하여금 뭔갈 시키거나 접근할 수 있는 유일한 부분이다.
인터페이스를 수정하면 해당 객체와 협력을 하고 있는 모든 객체에게 영향을 미칠 수 밖에 없으므로 객체의 인터페이스는 그 수정의 영향이 멀리까지 퍼진다는 것을 명심하자.
최대한 변화에 안정적인 인터페이스를 만들기 위해서는 구현과 ㅗ간련된 세부 사항이 인터페이스에 드러나지 않게 해야 한다.
구현 관점에서 바라본 코드
클래스의 내부 구현을 의미하는 관점으로 클래스의 메소드와 속성은 구현에 속하며 공용 인터페이스와 별개다.
따라서 메소드의 구현과 속성의 변경은 원칙적으로 외부의 객체에 영향을 주어서는 안 된다. 물론 100% 그럴 수는 없다.
이는 유도리있게 얘기해서 메소드와 속성이 클래스 내부로 철저하게 캡슐화되어야 한다는 것을 의미한다.
개념 관점, 명세 관점, 구현 관점은 동일한 코드를 바라보는 서로 다른 관점들이다.
훌륭한 객체지향 프로그래머는 하나의 클래스 안에 세 가지 관점을 모두 포함하면서도 각 관점에 해당하는 요소들을 명확하게 깔끔하게 분리시킬 수 있다.
다른 사람들이 내 코드를 읽으면서 세 가지 관점을 쉽게 포착하지 못한다면 세 가지 관점이 명확하게 드러날 수 있게 코드를 개선하라. 그것이 객체지향적인 코드를 작성하는 가장 빠른 길이다.

도메인 개념을 참조하는 이유

어떤 메시지가 있을 때 그 메시지를 수신할 객체를 어떻게 선택할까?
첫 번째 전략은 도메인 개념 중에 가장 적절한 것을 선택하는 것이다.
도메인 개념 안에서 적절한 객체를 선택하는 것은 도메인에 대한 지식을 기반으로 코드의 구조와 의미를 쉽게 유추할 수 있게 하므로 시스템의 유지보수성에 커다란 영향을 미친다.
소프트웨어의 한 가지 불변하는 사실은 항상 변한다는 점이다. 설계는 변경을 위해 존재한다. 이때 소프트웨어 클래스가 도메인 개념을 잘 은유하고 있다면 변화에 쉽게 대응할 수 있다.

인터페이스와 구현을 분리하라

저자는 여기서 다시 한 번 강조한다. 인터페이스와 구현을 분리하라고
명세 관점과 구현 관점을 명확하게 분리하라고 한다.
명세 관점은 클래스의 안정적인 측면을 드러내야 한다. 잘 변경되지 않는 부분이다.
구현 관점은 클래스의 불안정한 측면을 드러내야 한다.
이런 책임이 적절히 수행되지 않는 경우, 아주 작은 변동에도 전체 협력이 요동칠 가능성이 있는 취약한 설계를 하게될 가능성이 크다.
마틴 파울러는 개념 관점과 명세 관점 사이는 그렇게 중요하지 않은 경우가 많지만 명세 관점과 구현 관점은 명확하게 분리하는 것이 매우 중요하다고 주장했다.
프로그래머의 입장에서 가장 많이 접하게 되는 것은 코드이므로 구현 관점을 가장 빈번히 사용하게 되겠지만 실제로 훌륭한 설계를 결정하는 것은 명세 관점에서 바라본 코드인 객체의 인터페이스임을 명심하자.
하나 더 중요한 것은 우리가 클래스를 봤을 때, 클래스를 명세 관점과 구현 관점으로 나눠볼 수 있어야 한다는 것이다.
이들을 명확하게 분리되지 않고 흐릿하게 섞어놓아서도 안 된다.
결국 세 가지 관점 모두에서 클래스를 바라볼 수 있으려면 훌륭한 설계가 뒷받침되어야 한다는 뜻이다.