You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
코드와 모델을 밀접하게 연관시키는 것은 코드에 의미를 부여하고 모델을 적절하게 한다.
:에릭 에반스
마틴 파울러는 객체지향 설계 안에 존재하는 세 가지 상호 연관된 관점에 관해 설명한다.
각각 개념 관점, 명세 관점, 구현 관점이다.
개념 관점: 설계는 도메인 안에 존재하는 개념과 개념들 사이의 관계를 표현한다.
도메인이란 사용자들이 관심을 가지고 있는 특정 분야나 주제를 말하며 소프트웨어는 도메인에 존재하는 문제를 해결하기 위해 개발된다. 이 관점은 사용자가 도메인을 바라보는 관점을 반영한다.
따라서 실제 도메인의 규칙과 제약을 최대한 유사하게 반영하는 것이 핵심이다.
명세 관점: 사용자의 영역인 도메인을 벗어나 개발자의 영역인 소프트웨어 초점이 옮겨진다.
명세 관점은 도메인의 개념이 아니라 실제로 소프트웨어 안에서 살아 숨쉬는 객체들의 책임에 초점을 맞추게 된다 즉, 객체의 인터페이스를 바라보게 된다.
명세 관점에서 프로그래머는 객체가 협력을 위해 '무엇'을 할 수 있는가에 초점을 맞춘다.
인터페이스와 구현을 분리하는 것은 훌륭한 객체지향 설계를 낳는 가장 기본적인 원칙이라는 점을 기억하자.
구현 관점: 프로그래머인 우리에게 가장 익숙한 관점으로, 실제 작업을 수행하는 코드와 연관돼 있다.
구현 관점의 초점은 객체들이 책임을 수행하는 데 필요한 동작하는 코드를 작성하는 것이다.
따라서 프로그래머는 객체의 책임을 어떻게 수행할 것인가에 초점을 맞추며 인터페이스를 구현하는 데 필요한 속성과 메서드를 클래스에 추가한다.
앞의 설명이 마치 개념 관점, 명세 관점, 구현 관점의 순서대로 소프트웨어를 개발한다는 의미로 들릴 수도 있지만 이것은 사실이 아니다.
개념, 명세, 구현 관점은 동일한 클래스를 세 가지 다른 방향에서 바라보는 것을 의미한다.
클래스는 세 가지 관점이라는 안경을 통해 설계와 관련된 다양한 측면을 드러낼 수 있다.
클래스가 은유하는 개념은 도메인 관점을 반영한다.
클래스의 공용 인터페이스는 명세 관점을 반영한다.
클래스의 속성과 메서드는 구현 관점을 반영한다.
이것은 클래스를 어떻게 설계해야 하느냐에 대한 중요한 힌트를 암시한다.
클래스는 세 가지 관점을 모두 수용할 수 있도록 개념, 인터페이스, 구현을 함께 드러내야 한다.
동시에 코드 안에서 세 가지 관점을 식별할 수 있도록 깔끔하게 분리해야 한다.
지금까지 역할, 책임, 협력을 이용해 객체의 인터페이스를 식별했다.
협력에 참여하기 위해 객체가 수신해야 하는 메시지를 결정하고 메시지들이 모여 객체의 인터페이스를 구성한다는 점을 기억하라.
따라서 협력 안에서 메시지를 선택하고 메시지를 수신할 객체를 선택하는 것은 객체의 인터페이스, 즉 명세 관점에서 객체를 바라보는 것이다.
커피 전문점 도메인
커피 주문
커피 전문점에서 커피를 주문하는 과정 예제 설명.
커피 전문점이라는 세상
객체지향 패러다임의 가장 중요한 도구는 객체이므로 커피 전문점을 객체들로 구성된 작은 세상으로 바라보자.
커피 전문점 안에는 메뉴판이 존재하며 아메리카노, 카푸치노, 카라멜 마끼아또, 에스프레소의 네 가지 커피 메뉴가 적혀 있다.
객체지향의 관점에서 메뉴판이 하나의 객체이다.
또한 네 개의 메뉴 항목으로 구성돼 있기 때문에 메뉴 항목들 역시 객체로 볼 수 있다.
따라서 메뉴판은 네 개의 항목 객체들을 포함하는 객체라고 볼 수 있다.
손님은 메뉴판을 보고 바리스타에게 원하는 커피를 주문한다.
객체의 관점에서 보면 손님 역시 하나의 객체다.
손님 객체는 메뉴판 객체 안에 적힌 메뉴 항목 객체들 중에서 자신이 원하는 메뉴 항목 객체 하나를 선택해 바리스타 객체에게 전달할 것이다.
바리스타는 주문을 받은 메뉴에 따라 적절한 커피를 제조한다. (바리스타가 제조할 수 있는 커피의 종류는 4가지이다.)
바리스타는 자율적으로 커피를 제조하는 객체로 볼 수 있으며, 바리스타가 제조하는 커피 역시 메뉴판, 메뉴 항목, 바리스타와 구별되는 자신만의 경계를 가지므로 객체로 볼 수 있다.
종합해 보면 객체지향의 관점에서 커피 전문점이라는 도메인은 손님 객체, 메뉴 항목, 메뉴판 객체, 바리스타 객체, 커피 객체로 구성된 작은 세상이다.
객체들의 관계를 살펴보자.
손님은 메뉴판에서 주문할 커피를 선택할 수 있어야 한다.
손님은 어떤 식으로 메뉴판을 알아야 하며, 이것은 두 객체 사이에 관계가 존재한다는 것을 암시한다.
손님은 바리스타에게 주문을 해야 하므로 손님과 바리스타 사이에도 관계가 존재한다.
바리스타는 커피를 제조하는 사람이므로 당연히 자신이 만든 커피와 관계를 맺는다.
인간의 두뇌는 세상을 이해하기 위해 객체를 직접적으로 다룰 수 있을 만큼 효율적이지 못하기에 동적인 객체를 정적인 타입으로 추상화해서 복잡성을 낮추는 것이다.
타입은 분류를 위해 사용된다는 것을 기억하라.
상태와 무관하게 동일하게 행동할 수 있는 객체들은 동일한 타입의 인스턴스로 분류할 수 있다.
손님 객체는 '손님 타입'의 인스턴스로 볼 수 있다.
바리스타 객체는 '바리스타 타입'의 인스턴스로 볼 수 있다.
커피 객체는 '커피 타입'의 인스턴스로 볼 수 있다.
메뉴판 객체는 '메뉴판 타입'의 인스턴스로 볼 수 있다.
메뉴 항목 객체를 포함할 할 수 있다. 네 개의 메뉴 항목 객체 역시 모두 동일한 '메뉴 항목 타입'의 인스턴스로 모델링할 수 있다.
하나의 메뉴판 객체는 다수의 메뉴 항목 객체로 구성돼 있다.
메뉴판과 메뉴 항목 객체는 따로 떨어져 존재하지 않으며 하나의 단위로 움직인다.
이런 관점에서 메뉴 항목 객체가 메뉴판 객체에 포함돼 있다고 할 수 있는데 이를 메뉴판 타입과 메뉴 항목 타입 간의 합성 관계로 단순화하면 좀 더 보기 수월할 것이다.
메뉴판 타입에서 메뉴 항목 타입 쪽으로 향하는 선에 그려진 속이 찬 마름모는 포함 관계 또는 합성 관계를 나타내는 것으로, 메뉴 항목이 메뉴판에 포함된다는 사실을 표현한다.
손님 타입은 메뉴판 타입을 알고 있어야 원하는 커피를 선택할 수 있다.
메뉴판 타입은 손님의 일부가 아니므로 이 관계는 합성 관계로 볼 수 없다.
따라서 단순한 선으로 연결한다.
이처럼 한 타입의 인스턴스가 다른 타입의 인스턴스를 포함하지는 않지만 서로 알고 있어야 할 경우 이를 연관 관계라고 한다.
바리스타 타입은 커피를 제조해야 하므로 커피 타입을 알고 있어야 한다.
메뉴판 타입과 커피 타입 중 어떤 것도 바리스타의 일부가 아니므로 이 관계 역시 포함관계는 아니다.
이처럼 소프트웨어가 대상으로 하는 영역인 도메인을 단순화해서 표현한 모델을 도메인 모델이라고 한다.
커피 전문점이라는 도메인을 단순화하여 이해했다면 이제 초점을 소프트웨어로 옮길 때다.
객체지향의 세계는 협력하는 자율적인 객체들의 공동체라는 점을 기억하라.
다음 단계는 지금까지 배운 지식을 총동원해서 협력을 설계하는 것이다.
즉, 적절한 객체에게 적절한 책임을 할당하는 것이다.
실제로 도메인 모델을 작성하는 단계에서 어떤 관계가 포함 관계이고 어떤 관계가 연관 관계인지는 중요하지 않다.
초점은 어떤 타입이 도메인을 구성하느냐와 타입들 사이에 어떤 관계가 존재하는지를 파악함으로써 도메인을 이해하는 것이다.
여기서는 설명을 위해 포함 관계와 연관 관계를 구분하고 있지만 실제로는 메뉴판과 메뉴 항목 사이, 손님과 메뉴판 사이에 관계가 존재한다는 사실만 이해하는 것만으로도 충분하다.
설계에서 UML이 필수가 아닌 이유.
설계하고 구현하기
커피를 주문하기 위한 협력 찾기
객체지향 설계의 첫 번째 목표는 훌륭한 객체를 설계하는 것이 아니라 훌륭한 협력을 설계하는 것이라는 점을 잊지 말자.
훌륭한 객체는 오직 훌륭한 협력을 설계할 때만 얻을 수 있다. (협력이 객체를 결정한다.)
협력을 설계할 때는 객체가 메시지를 선택하는 것이 아니라 메시지가 객체를 선택하게 해야 한다.
메시지를 먼저 선택하고 그 후에 메시지를 수신하기에 적절한 객체를 선택해야 한다는 것을 의미한다.
이제 메시지를 수신할 객체는 메시지를 처리할 책임을 맡게 되고 객체가 수신하는 메시지는 객체가 외부에 제공하는 공용 인터페이스에 포함된다.
현재 설계하고 있는 협력은 커피를 주문하는 것이다. (첫 번째 메시지 "커피를 주문하라")
메시지 위에 붙은 화살표는 메시지에 담아 전달될 부가적인 정보인 인자를 의미한다.
이 경우 '아메리카노를 주문하라' 메시지는 나중에 '커피를 주문하라(아메리카노)'와 같이 인자를 포함하는 형식으로 구현될 것이다.
메시지를 찾았으니 이제 메시지를 처리하기에 적합한 객체를 선택해야 한다.
소프트웨어 객체는 현실 객체의 은유라는 것을 기억하자.
이미 앞서 커피 전문점을 추상화한 도메인 모델이란느 재료를 가지고 있기 때문에 (안정적인 재료:구조) 메세지를 처리할 객체를 찾고 있다면 먼저 도메인 모델 안에 책임을 수행하기에 적절한 타입이 존재하는지 살펴보는 것이 좋다.
적절한 타입을 발견했다면 책임을 수행할 객체를 그 타입의 인스턴스로 만들어라.
현실 속의 객체와 소프트웨어 객체가 완전히 동일할 수는 없겠지만 적어도 소프트웨어 객체에게 현실 객체와 유사한 이름을 붙여 놓으면 유사성을 통해 소프트웨어 객체가 수행해야 하는 책임과 상태를 좀 더 쉽게 유추할 수 있다.
'커피를 주문하라'라는 메시지를 수신할 객체는 무엇인가? (어떤 객체가 커피를 주문할 책임을 져야 하는가?)
당연히 손님이 커피를 주문할 책임을 져야 한다.
따라서 메시지를 처리할 객체는 손님 타입의 인스턴스다. (손님 객체는 커피를 주문할 책임을 할당받았다.)
손님이 커피를 주문하는 도중에 스스로 할 수 없는 일이 무엇일까?
손님이 할당된 책임을 수행하는 도중에 스스로 할 수 없는 일이 있다면 다른 객체에게 이를 요청해야 한다.
이 요청이 바로 손님 객체에서 외부로 전송되는 메시지를 정의한다.
손님은 메뉴 항목에 대해서는 알지 못한다. (메뉴 항목은 고객의 일부가 아니라 메뉴판의 일부라는 사실을 기억하라.)
따라서 고객은 자신이 선택한 메뉴 항목을 누군가가 제공해 줄 것을 요청한다.
'메뉴 항목을 찾아라'라는 새로운 메시지의 등장이다.
이 경우 메뉴 이름이라는 인자를 포함하여 전송하고, 응답으론 메뉴 항목을 반환한다.
메뉴 항목을 찾을 책임을 누구에게 할당하는 것이 좋을까?
메뉴 항목을 가장 잘 알고 있는 객체에게 할당하는 것이 적절할 것이다.
메뉴판 객체는 메뉴 항목 객체를 포함하기 때문에 이 책임을 처리할 수 있는 가장 적절한 후보다.
현실 속의 메뉴판은 스스로 메뉴 항목을 찾지 않을 것이다. 현실 속에서 메뉴판은 손님에 의해 펼처지거나 닫혀지는 수동적인 존재다.
그러나 객체지향의 세계로 들어오면 수동적인 메뉴판이라는 개념은 더 이상 유효하지 않다.
객체지향 세계에서는 모든 객체가 능동적이고 자율적인 존재다. (마치 생명을 가진 존재처럼)
소프트웨어 세상 속의 메뉴판은 현실 속의 메뉴판은 현실 속의 메뉴판으로부터 모티브를 따왔지만 현실 속의 메뉴판보다 더 많은 일을 할 수 있다.
소프트웨어 안의 메뉴판은 현실 속의 메뉴판이 제공하는 개념을 기반으로 하기 때문에 어떤 일을 수행하는지를 유추하기 쉽다.
소프트웨어 객체는 현실 속의 객체를 모방하거나 추상화한 것이 아닌 은유할 뿐이다. (쉽게 유추할 수 있도록)
손님은 자신이 주문한 커피에 대한 메뉴 항목을 얻었으니 이제 항목에 맞는 커피를 제조해달라고 요청할 수 있다.
손님은 커피를 제조하는 메시지의 인자로 메뉴 항목을 전달하고 반환값으로 제조된 커피를 받아야 한다.
커피는 바리스타가 제조한다.
UML은 의사소통을 위한 표기법이지 꼭 지켜야 하는 법칙이 아니다.
바리스타는 커피를 제조하는 데 필요한 모든 정보를 알고 있다.
아메리카노를 만들어야 한다면 머릿속에는 이미 아메리카노를 만드는 데 필요한 모든 방법이 들어 있을 것이다.
바리스타는 아메리카노를 만드는 데 필요한 정보와 기술을 함께 구비하고 있는 전문가다.
아메리카노를 만들기 위한 지식은 바리스타의 상태로, 기술은 바리스타의 행동으로 간주할 수 있다.
이런 관점에서 바리스타는 스스로의 판단과 지식에 따라 행동하는 자율적인 존재다.
커피 주문을 위한 협력은 이제 바리스타가 새로운 커피를 만드는 것으로 끝난다.
협력에 필요한 객체의 종류와 책임, 주고받아야 하는 메시지에 대한 대략적인 윤곽이 잡혔다.
남은 일은 메시지를 정제함으로써 각 객체의 인터페이스를 구현 가능할 정도로 상세하게 정제하는 것이다.
인터페이스 정리하기
이런 과정을 통해 얻어낸 것이 객체의 인터페이스다.
객체가 수신한 메시지가 객체의 인터페이스를 결정한다는 사실을 기억하라.
메시지가 객체를 선택했고, 선택된 객체는 메시지를 자신의 인터페이스로 받아들인다.
각 객체를 협력이라는 문맥에서 떼어내어 수신 가능한 메시지만 추려내면 객체의 인터페이스가 된다.
객체가 어떤 메시지를 수신할 수 있다는 것은 그 객체의 인터페이스 안에 메시지에 해당하는 오퍼레이션이 존재한다는 것을 의미한다.
손님 객체의 인터페이스 안에는 '커피를 주문하라'라는 오퍼레이션이 포함돼야 한다.
메뉴판 객체의 인터페이스는 '메뉴 항목을 찾아라'라는 오퍼레이션을 제공하며, 바리스타 객체의 인터페이스는 '커피를 제조하라'라는 오퍼레이션을, 커피 객체는 '생성하라'라는 오퍼레이션을 제공한다.
객체들의 협력은 실행 시간에 컴퓨터 안에서 일어나는 상황을 동적으로 묘사한 모델이다. (클래스는 정적인 모델)
실제로 소프트웨어의 구현은 동적인 객체가 아닌 정적인 타입을 이용해 이뤄진다.
따라서 객체들을 포괄하는 타입을 정의한 후 식별된 오퍼레이션을 타입의 인터페이스에 추가해야 한다.
객체의 타입을 구현하는 일반적인 방법은 클래스를 이용하는 것이다.
협력을 통해 식별된 타입의 오퍼레이션은 외부에서 접근 가능한 공용 인터페이스의 일부라는 사실을 기억하라.
따라서 인터페이스에 포함된 오퍼레이션 역시 외부에서 접근 가능하도록 공용으로 선언돼 있어야 한다.
구현하지 않고 머릿속으로만 구상한 설계는 코드로 구현하는 단게에서 대부분 변경된다. 설계 작업은 구현을 위한 스케치를 작성하는 단계까지 구현 그 자체일 수는 없다. 중요한 것은 설계가 아니라 코드다. 따라서 협력을 구상하는 단계에 너무 오랜 시간을 쏟지 말고 최대한 빨리 코드를 구현해서 설계에 이상이 없는지, 설계가 구현 가능한지를 판단해야 한다. 코드를 통한 피드백 없이는 깔끔한 설계를 얻을 수 없다.
Menu는 menuItem에 해당하는 MenuItem을 찾아야 하는 책임이 있다.
이 책임을 수행하기 위해서는 Menu가 내부적으로 MenuItem을 관리하고 있어야 한다.
간단하게 Menu가 MenuItem의 목록을 포함하게 하자.
Menu이 choose()메서드는 MenuItem의 목록을 하나씩 검사해가면서 이름이 동일한 MenuItem을 찾아 반환한다.
7장 함께 모으기
마틴 파울러는 객체지향 설계 안에 존재하는 세 가지 상호 연관된 관점에 관해 설명한다.
각각 개념 관점, 명세 관점, 구현 관점이다.
앞의 설명이 마치 개념 관점, 명세 관점, 구현 관점의 순서대로 소프트웨어를 개발한다는 의미로 들릴 수도 있지만 이것은 사실이 아니다.
개념, 명세, 구현 관점은 동일한 클래스를 세 가지 다른 방향에서 바라보는 것을 의미한다.
클래스는 세 가지 관점이라는 안경을 통해 설계와 관련된 다양한 측면을 드러낼 수 있다.
이것은 클래스를 어떻게 설계해야 하느냐에 대한 중요한 힌트를 암시한다.
클래스는 세 가지 관점을 모두 수용할 수 있도록 개념, 인터페이스, 구현을 함께 드러내야 한다.
동시에 코드 안에서 세 가지 관점을 식별할 수 있도록 깔끔하게 분리해야 한다.
지금까지 역할, 책임, 협력을 이용해 객체의 인터페이스를 식별했다.
협력에 참여하기 위해 객체가 수신해야 하는 메시지를 결정하고 메시지들이 모여 객체의 인터페이스를 구성한다는 점을 기억하라.
따라서 협력 안에서 메시지를 선택하고 메시지를 수신할 객체를 선택하는 것은 객체의 인터페이스, 즉 명세 관점에서 객체를 바라보는 것이다.
커피 전문점 도메인
커피 주문
커피 전문점에서 커피를 주문하는 과정 예제 설명.
커피 전문점이라는 세상
객체지향 패러다임의 가장 중요한 도구는 객체이므로 커피 전문점을 객체들로 구성된 작은 세상으로 바라보자.
커피 전문점 안에는 메뉴판이 존재하며 아메리카노, 카푸치노, 카라멜 마끼아또, 에스프레소의 네 가지 커피 메뉴가 적혀 있다.
객체지향의 관점에서 메뉴판이 하나의 객체이다.
또한 네 개의 메뉴 항목으로 구성돼 있기 때문에 메뉴 항목들 역시 객체로 볼 수 있다.
따라서 메뉴판은 네 개의 항목 객체들을 포함하는 객체라고 볼 수 있다.
손님은 메뉴판을 보고 바리스타에게 원하는 커피를 주문한다.
객체의 관점에서 보면 손님 역시 하나의 객체다.
손님 객체는 메뉴판 객체 안에 적힌 메뉴 항목 객체들 중에서 자신이 원하는 메뉴 항목 객체 하나를 선택해 바리스타 객체에게 전달할 것이다.
바리스타는 주문을 받은 메뉴에 따라 적절한 커피를 제조한다. (바리스타가 제조할 수 있는 커피의 종류는 4가지이다.)
바리스타는 자율적으로 커피를 제조하는 객체로 볼 수 있으며, 바리스타가 제조하는 커피 역시 메뉴판, 메뉴 항목, 바리스타와 구별되는 자신만의 경계를 가지므로 객체로 볼 수 있다.
종합해 보면 객체지향의 관점에서 커피 전문점이라는 도메인은 손님 객체, 메뉴 항목, 메뉴판 객체, 바리스타 객체, 커피 객체로 구성된 작은 세상이다.
객체들의 관계를 살펴보자.
인간의 두뇌는 세상을 이해하기 위해 객체를 직접적으로 다룰 수 있을 만큼 효율적이지 못하기에 동적인 객체를 정적인 타입으로 추상화해서 복잡성을 낮추는 것이다.
타입은 분류를 위해 사용된다는 것을 기억하라.
상태와 무관하게 동일하게 행동할 수 있는 객체들은 동일한 타입의 인스턴스로 분류할 수 있다.
하나의 메뉴판 객체는 다수의 메뉴 항목 객체로 구성돼 있다.
메뉴판과 메뉴 항목 객체는 따로 떨어져 존재하지 않으며 하나의 단위로 움직인다.
이런 관점에서 메뉴 항목 객체가 메뉴판 객체에 포함돼 있다고 할 수 있는데 이를 메뉴판 타입과 메뉴 항목 타입 간의 합성 관계로 단순화하면 좀 더 보기 수월할 것이다.
메뉴판 타입에서 메뉴 항목 타입 쪽으로 향하는 선에 그려진 속이 찬 마름모는 포함 관계 또는 합성 관계를 나타내는 것으로, 메뉴 항목이 메뉴판에 포함된다는 사실을 표현한다.
손님 타입은 메뉴판 타입을 알고 있어야 원하는 커피를 선택할 수 있다.
메뉴판 타입은 손님의 일부가 아니므로 이 관계는 합성 관계로 볼 수 없다.
따라서 단순한 선으로 연결한다.
이처럼 한 타입의 인스턴스가 다른 타입의 인스턴스를 포함하지는 않지만 서로 알고 있어야 할 경우 이를 연관 관계라고 한다.
바리스타 타입은 커피를 제조해야 하므로 커피 타입을 알고 있어야 한다.
메뉴판 타입과 커피 타입 중 어떤 것도 바리스타의 일부가 아니므로 이 관계 역시 포함관계는 아니다.
이처럼 소프트웨어가 대상으로 하는 영역인 도메인을 단순화해서 표현한 모델을 도메인 모델이라고 한다.
커피 전문점이라는 도메인을 단순화하여 이해했다면 이제 초점을 소프트웨어로 옮길 때다.
객체지향의 세계는 협력하는 자율적인 객체들의 공동체라는 점을 기억하라.
다음 단계는 지금까지 배운 지식을 총동원해서 협력을 설계하는 것이다.
즉, 적절한 객체에게 적절한 책임을 할당하는 것이다.
설계에서 UML이 필수가 아닌 이유.
설계하고 구현하기
커피를 주문하기 위한 협력 찾기
객체지향 설계의 첫 번째 목표는 훌륭한 객체를 설계하는 것이 아니라 훌륭한 협력을 설계하는 것이라는 점을 잊지 말자.
훌륭한 객체는 오직 훌륭한 협력을 설계할 때만 얻을 수 있다. (협력이 객체를 결정한다.)
협력을 설계할 때는 객체가 메시지를 선택하는 것이 아니라 메시지가 객체를 선택하게 해야 한다.
메시지를 먼저 선택하고 그 후에 메시지를 수신하기에 적절한 객체를 선택해야 한다는 것을 의미한다.
이제 메시지를 수신할 객체는 메시지를 처리할 책임을 맡게 되고 객체가 수신하는 메시지는 객체가 외부에 제공하는 공용 인터페이스에 포함된다.
현재 설계하고 있는 협력은 커피를 주문하는 것이다. (첫 번째 메시지 "커피를 주문하라")
메시지 위에 붙은 화살표는 메시지에 담아 전달될 부가적인 정보인 인자를 의미한다.
이 경우 '아메리카노를 주문하라' 메시지는 나중에 '커피를 주문하라(아메리카노)'와 같이 인자를 포함하는 형식으로 구현될 것이다.
메시지를 찾았으니 이제 메시지를 처리하기에 적합한 객체를 선택해야 한다.
소프트웨어 객체는 현실 객체의 은유라는 것을 기억하자.
이미 앞서 커피 전문점을 추상화한 도메인 모델이란느 재료를 가지고 있기 때문에 (안정적인 재료:구조) 메세지를 처리할 객체를 찾고 있다면 먼저 도메인 모델 안에 책임을 수행하기에 적절한 타입이 존재하는지 살펴보는 것이 좋다.
적절한 타입을 발견했다면 책임을 수행할 객체를 그 타입의 인스턴스로 만들어라.
현실 속의 객체와 소프트웨어 객체가 완전히 동일할 수는 없겠지만 적어도 소프트웨어 객체에게 현실 객체와 유사한 이름을 붙여 놓으면 유사성을 통해 소프트웨어 객체가 수행해야 하는 책임과 상태를 좀 더 쉽게 유추할 수 있다.
'커피를 주문하라'라는 메시지를 수신할 객체는 무엇인가? (어떤 객체가 커피를 주문할 책임을 져야 하는가?)
당연히 손님이 커피를 주문할 책임을 져야 한다.
따라서 메시지를 처리할 객체는 손님 타입의 인스턴스다. (손님 객체는 커피를 주문할 책임을 할당받았다.)
손님이 커피를 주문하는 도중에 스스로 할 수 없는 일이 무엇일까?
손님이 할당된 책임을 수행하는 도중에 스스로 할 수 없는 일이 있다면 다른 객체에게 이를 요청해야 한다.
이 요청이 바로 손님 객체에서 외부로 전송되는 메시지를 정의한다.
손님은 메뉴 항목에 대해서는 알지 못한다. (메뉴 항목은 고객의 일부가 아니라 메뉴판의 일부라는 사실을 기억하라.)
따라서 고객은 자신이 선택한 메뉴 항목을 누군가가 제공해 줄 것을 요청한다.
'메뉴 항목을 찾아라'라는 새로운 메시지의 등장이다.
이 경우 메뉴 이름이라는 인자를 포함하여 전송하고, 응답으론 메뉴 항목을 반환한다.
메뉴 항목을 찾을 책임을 누구에게 할당하는 것이 좋을까?
메뉴 항목을 가장 잘 알고 있는 객체에게 할당하는 것이 적절할 것이다.
메뉴판 객체는 메뉴 항목 객체를 포함하기 때문에 이 책임을 처리할 수 있는 가장 적절한 후보다.
손님은 자신이 주문한 커피에 대한 메뉴 항목을 얻었으니 이제 항목에 맞는 커피를 제조해달라고 요청할 수 있다.
손님은 커피를 제조하는 메시지의 인자로 메뉴 항목을 전달하고 반환값으로 제조된 커피를 받아야 한다.
커피는 바리스타가 제조한다.
바리스타는 커피를 제조하는 데 필요한 모든 정보를 알고 있다.
아메리카노를 만들어야 한다면 머릿속에는 이미 아메리카노를 만드는 데 필요한 모든 방법이 들어 있을 것이다.
바리스타는 아메리카노를 만드는 데 필요한 정보와 기술을 함께 구비하고 있는 전문가다.
아메리카노를 만들기 위한 지식은 바리스타의 상태로, 기술은 바리스타의 행동으로 간주할 수 있다.
이런 관점에서 바리스타는 스스로의 판단과 지식에 따라 행동하는 자율적인 존재다.
커피 주문을 위한 협력은 이제 바리스타가 새로운 커피를 만드는 것으로 끝난다.
협력에 필요한 객체의 종류와 책임, 주고받아야 하는 메시지에 대한 대략적인 윤곽이 잡혔다.
남은 일은 메시지를 정제함으로써 각 객체의 인터페이스를 구현 가능할 정도로 상세하게 정제하는 것이다.
인터페이스 정리하기
이런 과정을 통해 얻어낸 것이 객체의 인터페이스다.
객체가 수신한 메시지가 객체의 인터페이스를 결정한다는 사실을 기억하라.
메시지가 객체를 선택했고, 선택된 객체는 메시지를 자신의 인터페이스로 받아들인다.
각 객체를 협력이라는 문맥에서 떼어내어 수신 가능한 메시지만 추려내면 객체의 인터페이스가 된다.
객체가 어떤 메시지를 수신할 수 있다는 것은 그 객체의 인터페이스 안에 메시지에 해당하는 오퍼레이션이 존재한다는 것을 의미한다.
손님 객체의 인터페이스 안에는 '커피를 주문하라'라는 오퍼레이션이 포함돼야 한다.
메뉴판 객체의 인터페이스는 '메뉴 항목을 찾아라'라는 오퍼레이션을 제공하며, 바리스타 객체의 인터페이스는 '커피를 제조하라'라는 오퍼레이션을, 커피 객체는 '생성하라'라는 오퍼레이션을 제공한다.
객체들의 협력은 실행 시간에 컴퓨터 안에서 일어나는 상황을 동적으로 묘사한 모델이다. (클래스는 정적인 모델)
실제로 소프트웨어의 구현은 동적인 객체가 아닌 정적인 타입을 이용해 이뤄진다.
따라서 객체들을 포괄하는 타입을 정의한 후 식별된 오퍼레이션을 타입의 인터페이스에 추가해야 한다.
객체의 타입을 구현하는 일반적인 방법은 클래스를 이용하는 것이다.
협력을 통해 식별된 타입의 오퍼레이션은 외부에서 접근 가능한 공용 인터페이스의 일부라는 사실을 기억하라.
따라서 인터페이스에 포함된 오퍼레이션 역시 외부에서 접근 가능하도록 공용으로 선언돼 있어야 한다.
구현하기
클래스의 인터페이스를 식별했으므로 이제 오퍼레이션을 수행하는 방법을 메서드로 구현하면 된다.
먼저 Customer의 협력을 살표보자. Customer는 Menu에게
menuName
에 해당하는 MenuItem을 찾아달라고 요청해야 한다.문제는 Customer가 어떻게 Menu객체와 Barista 객체에 접근할 것이냐다.
객체가 다른 객체에게 메시지를 전송하기 위해서는 먼저 객체에 대한 참조를 얻어야 한다.
따라서 Customer 객체는 어떤 방법으로든 자신과 협력하는 Menu 객체와 Barista 객체에 대한 참조를 알고 있어야 한다.
객체 참조를 얻는 다양한 방법이 있지만 여기서는 Customer의
order()
메서드의 인자로 Menu와 Barista 객체를 전달받는 방법으로 참조 문제를 해결하기로 한다.이 결정은 결과적으로 Customer의 인터페이스를 변경한다.
남은 것은 order()메서드를 구현하는 것이다.
여기서 구현 도중에 객체의 인터페이스가 변경될 수 있다는 점을 눈여겨 보자.
Menu는 menuItem에 해당하는 MenuItem을 찾아야 하는 책임이 있다.
이 책임을 수행하기 위해서는 Menu가 내부적으로 MenuItem을 관리하고 있어야 한다.
간단하게 Menu가 MenuItem의 목록을 포함하게 하자.
Menu이 choose()메서드는 MenuItem의 목록을 하나씩 검사해가면서 이름이 동일한 MenuItem을 찾아 반환한다.
MenuItem의 목록을 Menu의 속성으로 포함시킨 결정 역시 클래스를 구현하는 도중에 내려졌다는 사실에 주목하라.
객체의 속성은 객체의 내부 구현에 속하기 때문에 캡슐화돼야 한다.
객체의 속성이 캡슐화된다는 이야기는 인터페이스에는 객체의 내부 속성에 대한 어떤 힌트도 제공돼서는 안 된다는 것을 의미한다.
이를 위한 가장 훌륭한 방법은 인터페이스를 정하는 단계에서는 객체가 어떤 속성을 가지는지, 또 그 속성이 어떤 자료 구조로 구현됐는지를 고려하지 않는 것이다.
객체에게 책임을 할당하고 인터페이스를 결정할 때는 가급적 객체 내부의 구현에 대한 어떤 가정도 하지 말아야 한다.
객체가 어떤 책임을 수행해야 하는지를 결정한 후에야 책임을 수행하는 데 필요한 객체의 속성을 결정하라.
이것이 객체의 구현 세부 사항을 객체의 공용 인터페이스에 노출시키지 않고 인터페이스와 구현을 깔끔하게 분리할 수 있는 기본적인 방법이다.
Barista는 MenuItem을 이용해서 커피를 제조한다.
Coffee는 자기 자신을 생성하기 위한 생성자를 제공한다. Coffee는 커피 이름과 가격을 속성으로 가지고 생성자 안에서 MenuItem에 요청을 보내 커피 이름과 가격을 얻은 후 Coffee의 속성에 저장한다.
MenuItem은 getName()과 cost()라는 메시지에 응답할 수 있도록 메서드를 구현해야 한다.
MenuItem의 인터페이스를 구성하는 오퍼레이션들을 MenuItem을 구현하는 단계에 와서야 식별했다는 점을 눈여겨 봐야한다.
이것은 부끄러운 일이 아닌 인터페이스는 객체가 다른 객체와 직접적으로 상호작용하는 통로다.
인터페이스를 통해 실제로 상호작용을 해보지 않은 채 인터페이스의 모습을 정확하게 예측하는 것은 불가능에 가깝다.
설계를 간단히 끝내고 최대한 빨리 구현에 돌입하라.
구조가 번뜩인다면 그대로 코드를 구현하기 시작하라.
설계가 제대로 그려지지 않는다면 고민하지 말고 실제로 코드를 작성해가면서 협력의 전체적인 밑그림을 그려보자.
이 작업이 바로 테스트-주도 설계로 코드를 구현하는 사람들이 하는 작업이다.
코드와 세 가지 관점
코드는 세 가지 관점을 모두 제공해야 한다
앞 코드를 개념 관점, 명세 관점, 구현 관점에서 각기 다른 사항들을 설명해보자.
먼저 개념 관점에서 코드를 바라보면 Customer, Menu, MenuItem, Barista, Coffee클래스가 보인다.
이 클래스들을 자세히 살펴보면 커피 전문점 도메인을 구성하는 중요한 개념과 관계를 반영한다는 사실을 쉽게 알 수 있다.
소프트웨어 클래스가 도메인 개념의 특성을 최대한 수용하면 변경을 관리하기 쉽고 유지보수성을 향상시킬 수 있다.
예를 들어, 커피를 제조하는 과정을 변경해야 한다면 어디를 수정해야 할까?
현실세계에서 커피를 제조하는 사람은 바리스타다.
따라서 현실 세계와 동일하게 소프트웨어 안에서도 Barista라는 클래스가 커피를 제조할 것이라고 쉽게 유추할 수 있다.
소프트웨어 클래스와 도메인 클래스 사이의 간격이 좁으면 좁을수록 기능을 변경하기 위해 뒤적거려야 하는 코드의 양도 점점 줄어든다.
명세 관점은 클래스의 인터페이스를 바라본다.
클래스의 public 메서드는 다른 클래스가 협력할 수 있는 공용 인터페이스를 드러낸다.
공용 인터페이스는 외부의 객체가 해당 객체에 접근할 수 있는 유일한 부분이다.
인터페이스를 수정하면 해당 객체의 협력하는 모든 객체에게 영향을 미칠 수밖에 없다.
객체의 인터페이스는 수정하기 어렵다는 사실을 명심하라.
최대한 변화에 안정적인 인터페이스를 만들기 위해서는 인터페이스를 통해 구현과 관련된 세부 사항이 드러나지 않게 해야 한다.
변화에 탄력적인 인터페이스를 만들 수 있는 능력은 객체지향 설계자의 수준을 가늠하는 중요한 척도다.
구현 관점은 클래스의 내부 구현을 바라본다.
클래스의 메서드와 속성은 구현에 속하며 공용 인터페이스의 일부가 아니다.
따라서 메서드의 구현과 속성의 변경은 원칙적으로 외부의 객체에게 영향을 미쳐서는 안 된다.
원칙적이라는 말 속에는 현실적으로 100% 파급효과가 미치는 것을 막는 것이 불가능한 경우도 있다는 사실을 암시한다.
이것은 메서드와 속성이 철저하게 클래스 내부로 캡슐화돼야 한다는 것을 의미한다. (메서드와 속성은 클래스 내부의 비밀이다)
외부의 클래스는 자신이 협력하는 다른 클래스의 비밀 때문에 우왕좌앙해서는 안 된다.
개념 관점, 명세 관점, 구현 관점은 동일한 코드를 바라보는 서로 다른 관점이다.
훌륭한 객체지향 프로그래머는 하나의 클래스 안에 세 가지 관점을 모두 포함하면서도 각 관점에 대응되는 요소를 명확하게 하고 깔끔하게 드러낼 수 있다.
다른 사람이 코드를 읽으면서 세 가지 관점을 쉽게 포착하지 못한다면 세 가지 관점을 명확하게 드러낼 수 있는 코드를 작성하지 못한 것이다. (개선하라)
도메인 개념을 참조하는 이유
어떤 메시지가 있을 때 그 메시지를 수신할 객체를 어떻게 선택하는가?
첫 번째 전략은 도메인 개념 중에서 가장 적절한 것을 선택하는 것이다.
도메인 개념 안에서 적절한 객체를 선택하는 것은 도메인에 대한 지식을 기반으로 코드의 구조와 의미를 쉽게 유추할 수 있게 한다.
이것은 시스템의 유지보수성에 커다란 영향을 미친다.
소프트웨어는 항상 변화한다. 설계는 이런 변경을 위해 존재한다.
여러 개의 클래스로 기능을 분할하고 클래스 안에서 인터페이스와 구현을 분리하는 이유는 변경이 발생했을 때 코드를 좀 더 수월하게 수정하길 원하기 때문이다.
소프트웨어 클래스가 도메인 개념을 따르면 변화에 쉽게 대응할 수 있다.
인터페이스와 구현을 분리하라
명세 관점과 구현 관점이 뒤섞여 읽는 사람에게 혼란을 주지 마라.
명세 관점은 클래스의 안정적인 측면을 드러내야 한다.
구현 관점은 클래스의 불안정한 측면을 드러내야 한다.
인터페이스가 구현 세부 사항을 노출하기 시작하면 아주 작은 변동에도 전체 협력이 요동치는 취약한 설계를 얻을 수밖에 없다.
프로그래머 입장에서 가장 많이 접하게 되는 것은 코드이므로 구현 관점을 가장 빈번하게 사용하겠지만 실제로 훌륭한 설계를 결정하는 측면은 명세 관점인 객체의 인터페이스다.
명세 관점이 설계를 주도하게 하면 설계의 품질을 향상될 수 있다는 사실을 기억하자.
중요한 것은 클래스를 봤을 때 클래스를 명세 관점과 구현 관점으로 나눠볼 수 있어야 한다는 것이고, 캡슐화를 위반해서 구현을 인터페이스 밖으로 노출해서도 안 되고, 인터페이스와 구현을 명확하게 분리하지 않고 흐릿하게 섞어놓아서도 안 된다.
결국 세 가지 관점 모두에서 클래스를 바라볼 수 있으려면 훌륭한 설계가 뒷받침돼야 하는 것이다.
코드 리뷰에서도 구현 관점보다 명세 관점의 맥락을 이야기하는 것이 좋다.
느낀점
6, 7장이 가장 이해하면서 얻어간게 많은 챕터라는 생각이 들며 앞 챕터들이 전부 6과 7을 위한 사전 지식이라는 생각이 든다.
평소 작업하면서 가져야할 마음가짐에서 애매했던 부분들이 전부 해결된 것 같고, 계속 가져가야 할 부분이라는 생각이 든다.
논의사항
저는 개념 관점에서 도메인 모델로 바라보는 부분이 제일 필요했던 것 같습니다.
물론 게임이라 허구의 모델을 만들어야 하지만, 그래도 연관관계나 비슷한 모델을 생각하여 설계 구조를 잡아야 한다는 부분이 가장 인상 깊었습니다.
The text was updated successfully, but these errors were encountered: