•
마틴 파울러는 자신의 저서 UML Distilled 2판에서 객체지향 설계 안에 존재하는 세 가지 상호 연관된 관점에 대해 설명한다. 각각 개념 관점, 명세 관점, 구현 관점이라 부른다.
◦
개념 관점
▪
해당 관점에서 설계는 도메인 안에 존재하는 개념과 개념들 사이의 관계를 표현한다.
▪
이 관점은 사용자가 도메인을 바라보는 관점을 반영한다. 이때문에 도메인 관점이라고도 한다.
◦
명세 관점
▪
이에 이르면 사용자의 영역이 도메인을 벗어나 개발자의 영역인 소프트웨어로 초점이 옮겨진다.
▪
명세 관점은 도메인의 개념이 아닌 실제 소프트웨어 안의 객체들의 책임에 초점을 맞추게 된다.
▪
즉, 객체의 인터페이스를 바라보게 된다. 명세 관점에서 프로그래머는 객체가 협력을 위해 무엇을 제공할 수 있는가에 초점을 맞춘다.
◦
구현 관점
▪
우리 프로그래머에게 가장 익숙한 관점으로 실제 작업을 수행하는 코드와 연관되어 있다.
▪
구현 관점의 초점은 객체들이 책임을 수행하는 데 필요한 동작 코드들을 작성하는 것이다. 즉, 객체의 책임을 어떻게 수행할 것인가에 초점을 맞추며 인터페이스를 구혀하는 데 필요한 속성과 메소드를 클래스에 추가한다.
•
이 관점들은 어떠한 절차나 순차에 따라 개발이 이뤄지는 것이 아닌, 클래스를 바라보는 세 가지 관점이 있다는 것을 의미한다.
•
클래스는 세 가지 관점을 통해 설계와 관련된 다양한 측면들을 확인할 수 있는데, 클래스가 은유하는 개념은 도메인 관점을, 클래스의 공용 인터페이스는 명세 관점을, 클래스의 속성과 메소드는 구현 관점을 반영한다.
•
이를 통해 우리는 클래스 설계에 관한 가장 큰 힌트를 얻을 수 있는데, 바로 이 세 가지 관점이 명확하게 분리될 수 있도록, 서로 영향을 받지 않도록 식별이 가능하게 해야한다.
•
다음의 간단한 예제를 통해서, 두 가지 목표를 달성하고자 한다.
◦
하나는 도메인 모델에서 시작해 최종 코드의 구현 과정을 간략하게나마 설명하는 것이고 나머지 하나는 클래스를 관점들에서 바라보는 것이 의미하는게 뭔지 설명하는 것이다.
커피 전문점 도메인
커피 주문
•
예제의 목적은 커피 전문점에서 커피를 주문하는 과정을 먼저 객체들의 협력 관계로 구현하는 것이다.
•
예제
◦
커피 전문점에서는 아메리카노, 카푸치노, 카라멜 마끼아또, 에스프레소의 네 메뉴를 판매하고 있다.
◦
손님이 테이블에 앉아 메뉴판을 잠시 훑어본 후, 바리스타에게 커피를 주문한다. 바리스타는 주문받은 커피를 제조한다.
커피 전문점이라는 세상
•
객체지향 패러다임의 가장 중요한 도구는 객체이므로 이 설명을 통해 객체를 판별하여 커피 전문점이라는 도메인을 객체들로 구성된 작은 세상으로 확인해보는 것이 좋다.
◦
먼저 커피 전문점 안에는 메뉴판이 존재한다. 메뉴판에는 아메리카노, 카푸치노, 카라멜 마끼아또, 에스프레소의 네 가지 커피 메뉴가 적혀있다.
▪
객체지향의 관점에서 메뉴판은 하나의 객체고 네 개의 메뉴 항목 역시, 객체로 볼 수 있다. 따라서 메뉴판은 네 개의 메뉴 항목 객체들을 포함하는 객체라고 볼 수 있다.
◦
손님은 메뉴판을 보고 바리스타에게 원하는 커피를 주문한다.
▪
객체지향의 관점에서 손님 객체는 메뉴판 객체 안에 적힌 메뉴 항목 중, 하나를 선태갛여 바리스타 객체에게 이를 전달한다.
◦
바리스타는 주문을 받은 메뉴에 따라 적절한 커피를 제조한다. 바리스타가 제조할 수 있는 커피의 종류는 메뉴판에 적혀있는 4가지다.
▪
객체지향의 관점에서 바리스타 객체는 커피를 제조하는 방식을 자율적인 객체로 바리스타가 제조하는 커피 역시, 메뉴판, 메뉴 항목 등의 객체들과 구별되는 자신만의 경계를 가지므로 객체라 볼 수 있다.
•
이를 종합해보면 객체지향의 관점에서 커피 전문점이라는 도메인은 손님 객체, 메뉴 항목 객체, 메뉴판 객체, 바리스타 객체, 커피 객체로 이뤄진 작은 세상이다.
•
어떤 객체들이 있는지 알아봤으니 이제 객체들 간의 관계를 구성해야 한다.
◦
손님은 메뉴판에서 주문할 커피를 선택할 수 있어야 한다. 따라서 손님은 어떤 식으로든 메뉴판을 알고 있어야 한다.
▪
이는 두 객체 사이에 관계가 존재한다는 것을 암시한다.
◦
손님은 바리스타에게 주문을 해야 하므로 손님과 바리스타 사이에도 관계가 존재한다.
◦
바리스타는 커피를 제조하므로 당연하게도 자신이 만든 커피와 관계를 맺는다.
•
객체는 타입의 인스턴스로 손님 객체는 손님 타입의 인스턴스로, 바리스타 객체는 바리스타 타입의 인스턴스이다. 이를 통해 타입을 구성할 수 있다.
•
커피 전문점을 구성하는 범주로 타입들이 갖춰지고, 타입 간에 어떤 관계가 존재하는 지 파악해야 한다.
◦
메뉴판 객체는 메뉴 항목 객체로 구성되어 있다. 메뉴들은 따로 떨어져 존재하지 않고 하나의 단위로 인식되어야 하므로 메뉴 항목 객체가 메뉴판 객체에 포함되어 있다고 할 수 있다.
▪
이는 메뉴 항목에서 메뉴판으로 향하는 선에 그려진 속이 찬 마름모로 표현되는데 이는 포함 또는 합성 관계를 나타낸다.
◦
손님 타입은 메뉴판 타입을 알고 있어야 하므로 단순한 선으로 연결되는데, 이처럼 한 타입의 인스턴스가 다른 타입의 인스턴스를 포함하지는 않으나 서로 알고 있어야 할 경우로 이는 연관 관계라고 표현한다.
◦
바리스타 타입과 커피 역시 연관 관계이며 손님과 바리스타 역시 연관 관계다.
•
이렇게 커피 전문점이라는 도메인을 단순화하여 이해했으므로 이제는 이 도메인 관점을 소프트웨어로 옮길 시간이다.
•
참고
◦
실제로 도메인 모델을 작서앟는 단계에서 포함이고 연관이고 관계가 중요하지는 않다.
◦
초점은 어떤 타입이 도메인을 구성하고 있느냐와 타입들 사이에 어떤 관계가 존재하는지 파악함으로써 도메인을 이해하는 것이다.
◦
실제로는 관계가 존재한다고 인식하는 것만으로도 충분하다.
설계하고 구현하기
커피를 주문하기 위한 협력 찾기
•
객체지향의 첫 번째 목표는 훌륭한 협력을 설계하는 것이라는 점을 잊지 말자. 훌륭한 객체는 훌륭한 협력을 설계할 때 비로소 얻을 수 있다.
•
협력을 설계하는 과정에서 우리는 객체가 메시지를 선택하지 않고 메시지가 객체를 선택하게끔 해야 한다.
◦
이 말은 메시지를 먼저 선택하고 그 수신자를 이후에 결정해야 한다는 것을 의미한다.
◦
수신자는 메시지 처리에 대한 책임을 갖게 되고 객체가 수신하는 메시지는 수신 객체가 외부에 제공하는 공용 인터페이스에 명시하게 된다.
•
현재 설계하고 있는 협력은 커피를 주문하는 것이다. 따라서 우리는 첫 번째 메시지가 커피를 주문한다임을 짐작해볼 수 있다.
◦
메시지를 찾았으니 이제 수신자를 찾아야 한다. 어떤 객체에 은유해야 할까?
◦
앞서 설계한 도메인 모델을 바탕으로 우리는 수신자가 손님임을 알 수 있다. 즉 손님이 커피를 주문하는 책임을 가져야 한다는 것이다.
◦
그렇다면 자연스럽게 다음으로 넘어가 손님이 커피를 주문하는 과정에서 스스로 처리할 수 없는 일은 뭘까? 손님은 메뉴 항목을 알지 못한다. 따라서 자신이 메뉴를 고르기 위해 메뉴 항목을 제공해줄 것을 요청해야 한다.
•
여기서 우리는 새로운 메시지인 메뉴 항목을 찾아라를 얻을 수 있다.
◦
이 경우, 메시지에 메뉴 이름이라는 인자를 포함해 함께 전송한다. 이 메시지를 수신한 객체는 메뉴 이름에 대응되는 메뉴 항목을 반환해야 한다.
◦
메뉴 항목을 찾는 책임은 어떤 객체가 가져야할까? 짐작하고 있듯이 메뉴판이 담당한다. 메뉴판 객체는 메뉴 항목을 포함하고 있으므로 이 책임을 가장 잘 처리할 수 있는 수신자다.
◦
이때, 능동적인 객체임을 생각하자. 송신자가 고르지 않는다. 단지 요청에 따른 반환을 해줄 뿐이다. 현실의 메뉴판은 수동적이지만 객체지향 세계에서의 메뉴판은 스스로 메뉴를 결정해 반환해줄 수 있다.
•
손님 객체는 이제 무슨 커피를 주문할 지도 알게 되었고 커피 주문을 위한 다음 메시지를 만든다. 이는 커피를 제조하라다.
◦
손님은 이제 커피를 제조하는 메시지의 인자로 메뉴 항목을 전달하고 반환 값으로 커피를 받아야 한다.
◦
누가 이 메시지의 수신자일까? 당연히 바리스타다.
⇒ 협력 자체가 어렵지 않은 것도 있으나 도메인 모델을 잘 정리해두었기 때문도 있는 것 같다.
◦
바리스타는 커피를 제조하는 데 필요한 모든 정보를 알고 있다. 이로써 커피 주문을 위한 협력은 바리스타가 커피를 만들어 손님에게 반환함으로써 종료된다.
•
협력에 필요한 객체의 종류와 책임, 주고 받아야 하는 메시지에 대한 대략적인 윤곽이 정리되었다. 남은 일은 메시지를 정제함으로써 각 객체의 공용 인터페이스를 구현 가능할 정도로 상세하게 결정하는 것이다.
인터페이스 정리하기
•
우리가 설계한 협력은 객체들의 인터페이스다. 객체가 수신한 메시지가 객체의 인터페이스를 결정한다는 사실을 기억하자. 메시지가 객체를 선택했고 선택된 객체는 메시지를 자신의 인터페이스로 받아들인다.
•
여기서 각 객체가 수신 가능한 메시지만 추려내면 객체의 인터페이스가 결정된다. 객체가 어떤 메시지를 수신할 수 있다는 것은 그 객체의 인터페이스 안에 메시지에 해당하는 실행 절차가 존재한다는 것을 의미한다.
◦
손님 객체의 인터페이스에는 커피를 주문하라라는 연산이 제공된다.
◦
메뉴판 객체의 인터페이스에는 메뉴 항목을 찾아라라는 연산이 제공된다.
◦
바리스타 객체의 인터페이스는 커피를 제조하라라는 연산이 제공된다.
◦
커피 객체의 인터페이스는 생성하라라는 연산이 제공된다.
•
객체들의 협력은 런타임에 컴퓨터에서 일어나는 상황을 동적으로 묘사한 모델이다. 실제로 소프트웨어의 구현은 동적인 객체가 아닌 정적인 타입을 이용해 명시된다.
◦
즉, 객체들을 포괄하는 타입을 정의한 후 식별된 오퍼레이션을 타입의 인터페이스에 추가해야 한다.
•
객체의 타입을 구현하는 일반적인 방법은 클래스를 사용하는 것이다.
구현하기
•
구현 도중에 객체의 인터페이스가 변경될 수 있음을 유연하게 받아들이자.
코드와 세 가지 관점
코드는 세 가지 관점을 모두 제공해야 한다
•
개념 관점에서 바라본 코드
◦
Customer, Menu, MenuItem, Barista, Coffee 클래스가 해당한다.
◦
이 클래스들을 자세히 살펴보면 커피 전문점 도메인을 구성하는 중요한 개념과 관계를 반영한다는 사실을 알 수 있다.
◦
이처럼 소프트웨어 클래스가 도메인 개념의 특성을 최대한 수용하면 변경을 관리하기 쉽고 유지보수성을 향상시킬 수 있다.
◦
즉, 소프트웨어 클래스와 도메인 클래스 사이의 간격이 좁으면 좁을수록 변경사항이 발생했을 때, 수정 범위가 줄어든다.
•
명세 관점에서 바라본 코드
◦
명세 관점은 클래스의 인터페이스를 바라본다. 즉 클래스가 공개하고 있는 public 속성들을 의미한다.
◦
이는 다른 클래스가 협력할 수 있는 공용 인터페이스로, 외부의 객체가 해당 객체로 하여금 뭔갈 시키거나 접근할 수 있는 유일한 부분이다.
◦
인터페이스를 수정하면 해당 객체와 협력을 하고 있는 모든 객체에게 영향을 미칠 수 밖에 없으므로 객체의 인터페이스는 그 수정의 영향이 멀리까지 퍼진다는 것을 명심하자.
◦
최대한 변화에 안정적인 인터페이스를 만들기 위해서는 구현과 ㅗ간련된 세부 사항이 인터페이스에 드러나지 않게 해야 한다.
•
구현 관점에서 바라본 코드
◦
클래스의 내부 구현을 의미하는 관점으로 클래스의 메소드와 속성은 구현에 속하며 공용 인터페이스와 별개다.
◦
따라서 메소드의 구현과 속성의 변경은 원칙적으로 외부의 객체에 영향을 주어서는 안 된다. 물론 100% 그럴 수는 없다.
◦
이는 유도리있게 얘기해서 메소드와 속성이 클래스 내부로 철저하게 캡슐화되어야 한다는 것을 의미한다.
•
개념 관점, 명세 관점, 구현 관점은 동일한 코드를 바라보는 서로 다른 관점들이다.
•
훌륭한 객체지향 프로그래머는 하나의 클래스 안에 세 가지 관점을 모두 포함하면서도 각 관점에 해당하는 요소들을 명확하게 깔끔하게 분리시킬 수 있다.
•
다른 사람들이 내 코드를 읽으면서 세 가지 관점을 쉽게 포착하지 못한다면 세 가지 관점이 명확하게 드러날 수 있게 코드를 개선하라. 그것이 객체지향적인 코드를 작성하는 가장 빠른 길이다.
도메인 개념을 참조하는 이유
•
어떤 메시지가 있을 때 그 메시지를 수신할 객체를 어떻게 선택할까?
•
첫 번째 전략은 도메인 개념 중에 가장 적절한 것을 선택하는 것이다.
•
도메인 개념 안에서 적절한 객체를 선택하는 것은 도메인에 대한 지식을 기반으로 코드의 구조와 의미를 쉽게 유추할 수 있게 하므로 시스템의 유지보수성에 커다란 영향을 미친다.
•
소프트웨어의 한 가지 불변하는 사실은 항상 변한다는 점이다. 설계는 변경을 위해 존재한다. 이때 소프트웨어 클래스가 도메인 개념을 잘 은유하고 있다면 변화에 쉽게 대응할 수 있다.
인터페이스와 구현을 분리하라
•
저자는 여기서 다시 한 번 강조한다. 인터페이스와 구현을 분리하라고
•
명세 관점과 구현 관점을 명확하게 분리하라고 한다.
◦
명세 관점은 클래스의 안정적인 측면을 드러내야 한다. 잘 변경되지 않는 부분이다.
◦
구현 관점은 클래스의 불안정한 측면을 드러내야 한다.
•
이런 책임이 적절히 수행되지 않는 경우, 아주 작은 변동에도 전체 협력이 요동칠 가능성이 있는 취약한 설계를 하게될 가능성이 크다.
•
마틴 파울러는 개념 관점과 명세 관점 사이는 그렇게 중요하지 않은 경우가 많지만 명세 관점과 구현 관점은 명확하게 분리하는 것이 매우 중요하다고 주장했다.
•
프로그래머의 입장에서 가장 많이 접하게 되는 것은 코드이므로 구현 관점을 가장 빈번히 사용하게 되겠지만 실제로 훌륭한 설계를 결정하는 것은 명세 관점에서 바라본 코드인 객체의 인터페이스임을 명심하자.
•
하나 더 중요한 것은 우리가 클래스를 봤을 때, 클래스를 명세 관점과 구현 관점으로 나눠볼 수 있어야 한다는 것이다.
◦
이들을 명확하게 분리되지 않고 흐릿하게 섞어놓아서도 안 된다.
•
결국 세 가지 관점 모두에서 클래스를 바라볼 수 있으려면 훌륭한 설계가 뒷받침되어야 한다는 뜻이다.