▼ What ?
이 책의 마지막 챕터는 지금까지 다뤘던 이야기들을 종합하여 간단하게 풀어낸 내용을 담고 있다. 이전까지의 챕터들에서 다룬 내용들은 워낙 추상적이다보니 지금까지 매 스터디마다 그러한 내용들을 코드에 대입해서 생각해보는 시간을 가졌다. 나름 다같이 적절하다고 생각하는 방향으로 생각을 정리하긴 했지만, 그럼에도 모호했던 부분들이 꽤 있었어서 스터디가 끝나고도 혼자 생각해보기도 하고 심적으로도 찝찝한 부분이 있었다. 저자가 마지막 챕터에서 그러한 모호한 내용들을 마지막 챕터에서 자바(java) 코드를 통해 직접 짚어주니 당시에 이해했던 내용을 되돌아볼 수 있어서 이 북 스터디를 홀가분한 마음으로 마무리해볼 수 있을 것 같다.
(추가적으로 이번 챕터 뒤에 "추상화"에 대해 다룬 부록이 있는데 이 부분은 따로 스터디를 하지 않고 각자 읽어보기로 했다!)
▼ Summory & Comment
이 챕터를 읽고 나서 기억에 남는 말 !
인터페이스와 구현을 분리하라.
- 이 책에서 길게 설명한 모든 내용들이 결국 '인터페이스'와 '구현'을 분리하라는 말이 어떤 것을 의미하는지를 이해하고, 분리해내기 위해 선행되어야 하는 과정이 어떤 것인지를 알려주기 위한 발판들(도메인 모델, 협력, 메시지 등)이었다는 사실을 깨닫고 나니 이 말이 기억에 남을 수 밖에 없었다.
클래스를 다르게 바라보는 세 가지 관점
- 개념 관점(Conceptual Perspective)
: 사용자가 도메인을 바라보는 관점을 반영하는 것에 초점이 잡혀있다.
➙ 도메인의 관점이라고도 할 수 있으며, 실제 도메인과 간격을 최대한 좁히는 것이 중요하다. - 명세 관점(Specification Perspective)
: 객체가 협력을 위해 '무엇'을 할 수 있는지에 초점이 잡혀있다.
➙ 클래스의 공용 인터페이스를 설계할 때 반영된다. - 구현 관점(Implementation Perspective)
: 주어진 객체의 책임을 '어떻게' 수행할 것인지에 초점이 잡혀있다.
➙ 인터페이스를 구현하기 위해 필요한 속성과 메서드를 추가할 때 반영된다 !
현실 세계의 "커피 주문"을 소프트웨어 세상 속으로 !
Step 1
: 현실 세계의 도메인을 단순화 !
A. "커피 전문점"이라는 도메인을 "손님", "메뉴 항목", "메뉴판", "바리스타", "커피" 객체로 구성된 작은 세상으로 바라보자.
B. 어떤 객체가 있는지 생각한 다음 객체들 간의 '관계'를 살펴봐야 하는데,
그전에 먼저 동적인 객체를 정적인 타입으로 추상화하여 복잡성을 낮추자 !
➙ 예를 들어, "손님" 객체는 '손님 타입'의 인스턴스로, 여러 커피 메뉴들은 '메뉴 항목 타입'의 인스턴스로 모델링해준다.
C. 이제 객체들 간의 관계들을 살펴보는 과정에서 '메뉴판 타입'과 '메뉴 항목 타입' 간의 관계를 예로 들어보자.
➙ "메뉴판"과 "메뉴 항목" 객체는 따로 독립되어 존재할 수 없기 때문에 둘은 '포함(containment) 관계' 혹은 '합성(composition) 관계"에 있다고 볼 수 있다.
➙ "손님"과 "메뉴판" 객체 사이의 관계의 경우엔 "손님"은 "메뉴판"을 봐야만 주문할 "커피"를 선택할 수 있다는 점을 생각해봤을 때, '메뉴판 타입'은 '손님 타입'의 일부가 아니기 때문에 '합성 관계'라 할 수 없고 '연관(association) 관계'라고 할 수 있다.
('바리스타 타입'과 '커피 타입'도 마찬가지로 '연관 관계'이다.)
사실 '도메인 모델'을 구상할 땐 이처럼 '포함 관계'와 '연관 관계'를 구분하여 객체들 간의 관계를 생각할 필요까진 없고, 객체들 간에 관계가 존재한다는 사실 정도만 이해해도 충분하다고 한다 !
D. 아래처럼 "커피 전문점"이라는 도메인을 구성하는 타입들의 종류와 관계를 이용해 관련 객체들을 추상화한 일종의 모델을 '도메인 모델'이라고 하는 것이다.
Step 2
: 도메인 모델을 기반으로 협력을 설계 !
A. 도메인을 도메인 모델로 단순화하였으니 협력을 설계하자 !
훌륭한 객체는 훌륭한 협력을 설계함으로써 탄생하는 것 !
B. 협력에 필요한 '메시지'를 생각하고 해당 메시지를 '수신할 적절한 객체'를 선택하자.
➙ "메뉴 이름"이라는 '인자'와 함께 전달할 "커피를 주문하라"라는 '메시지'가 필요하고, 이 메시지를 처리하기에 적합한 객체는 '손님 타입'의 인스턴스이다 !
C. 선택한 적절한 객체가 할당된 책임에 대해 혼자선 할 수 없는 일이 있다면 다른 객체에게 요청하기 위해 외부에 전송할 메시지를 구상한다 !
➙ 예를 들어, "손님"은 "메뉴 항목"에 대해서 모르고 있기 때문에 "메뉴 이름"이라는 '인자'를 포함해 "메뉴 항목을 찾아라"라는 '메시지'를 전송해야 하고, 이 메시지를 수신하기에 적합한 객체는 도메인 모델에서 알 수 있듯이 "메뉴 항목" 객체를 포함하고 있는 "메뉴판" 객체이다.
소프트웨어 세상 속 "메뉴판"은 현실 세계 속 "메뉴판"보다 훨씬 많은 일을 하는 능동적이고 자율적인 존재이다.
D. 이러한 과정을 반복해서 "커피 주문"이라는 '협력'은 "바리스타"가 요청받은 "커피"를 만드는 것으로 끝난다.
➙ 이때 "바리스타" 객체는 "커피"를 제조하는 데 필요한 모든 상태(지식)와 행동(기술)를 갖고 있기 때문에 '자율적인 객체'임을 알 수 있다.
Step 3
: 인터페이스를 정의하고 구현하자 !
A. 객체가 수신한 '메시지'가 곧 객체의 '인터페이스'를 결정한다는 것을 기억하자 !
➙ 예를 들어, "손님" 객체의 인터페이스엔 "커피를 주문하라"라는 오퍼레이션이 포함되어야만 한다.
B. 앞서 말했던 것처럼 동적인 객체를 정적인 타입으로 정의하고, 해당 타입의 인터페이스에 이전에 식별해놓은 오퍼레이션을 추가해준다.
➙ 객체의 '타입'을 구현하는 일반적인 방법이 아래처럼 클래스를 이용하는 것이다.
class Customer {
public void order(String menuName) {}
}
class MenuItem {
}
class Menu {
public MenuItem choose(String name) {}
}
class Barista {
public Coffee makeCoffee(MenuItem menuItem) {}
}
class Coffee {
public Coffee(MenuItem menuItem) {}
}
C. 클래스의 인터페이스를 정의했으니 오퍼레이션을 수행하는 방법을 메서드로 구현해주면 된다.
➙ "손님" 객체가 "커피를 주문하라"라는 메시지를 처리하기 위해선 "메뉴판" 객체와 "바리스타" 객체와 협력, 즉 해당 객체들에 접근하기 위해 '참조'를 얻어야 한다 !
class Customer {
public void order(String menuName, Menu menu, Barista barista) {
// 참조 문제를 해결한 후엔, 아래처럼 order() 메서드의 구현까지 채워주면 된다.
MenuItem menuItem = menu.choose(menuName);
Coffee coffee = barista.makeCoffee(menuItem);
...
}
}
🔻 메서드의 '인자'로 접근하려는 객체들을 전달받는 방법으로 참조 문제를 해결했다.
설계는 간단히 끝내고 구현 작업을 즉시 시작해야 한다.
구상해놓은 설계는 코드를 구현하는 과정에서 대부분 변경되니 협력을 구상하는 단계에 너무 오랜 시간을 투자하지 말자 !
D. 이외의 코드 구현
- "메뉴(Menu)"에겐 인자로 받은 "메뉴 이름(menuName)"에 해당하는 "메뉴 항목(MenuItem)"을 찾아야 하는 책임.
class Menu {
private List<menuItem> items;
public Menu(List<menuItem> items {
this.items = items;
}
public MenuItem choose(String name) {
for (MenuItem each : items) {
if (each.getName().equals(name) {
return each;
}
}
return null;
}
}
- "바리스타(Barista)"는 '인자'로 주어진 "메뉴 항목(MenuItem)"에 해당하는 "커피(Coffee)"를 만들어야 하는 책임.
class Barista {
public Coffee makeCoffee(MenuItem menuItem) {
Coffee coffee = new Coffee(menuItem);
return coffee;
}
}
- "커피(Coffee)"는 "바리스타"가 전송한 "생성하라"라는 메시지를 수신하고 처리해야 하는 책임.
class Coffee {
private String name;
private int price;
public Coffee(MenuItem menuItem) {
this.name = menuItem.getName();
this.price = menuItem.cost();
}
}
- "메뉴 항목(MenuItem)"은 수신받은 메시지를 처리하기 위한 메서드를 구현해야 한다.
public class MenuItem {
private String name;
private int price;
public MenuItem(String name, int price) {
this.name = name;
this.price = price;
}
public int cost() {
return price;
}
public String getName() {
return name;
}
}
코드와 세 가지 관점
- 개념 관점에서 앞선 코드들을 살펴보자.
➙ 소프트웨어 클래스와 도메인 클래스 사이의 간격이 좁으면 좁을수록 유지보수를 하는데 살펴봐야 할 코드의 양도 점점 줄어든다 !
(ex) 커피를 제조하는 과정을 변경해야 한다면?
현실 세계에서 커피를 제조하는 사람은 "바리스타"이고, 따라서 소프트웨어 안에서도 "Barista"라는 클래스가 커피를 제조할 것이라고 쉽게 유추 가능하다는 것을 알 수 있다.
- 명세 관점은 클래스의 인터페이스를 바라본다.
➙ 앞선 코드에서 public 메서드는 외부 객체와 협력하고 영향을 미치는 '공용 인터페이스'를 드러내기 때문에 수정하기가 매우 어렵다. 따라서, 안정적인 인터페이스를 만들기 위해선 구현과 관련된 세부 사항처럼 불안정한 부분이 인터페이스를 통해 드러나지 않게 해야 한다 !
- 구현 관점은 클래스의 내부 구현을 바라본다.
➙ 원칙적으로는 메서드의 구현과 속성의 변경이 객체의 외부에 영향을 줘선 안된다.
(불가능한 경우도 있다.)
➙ 즉, 클래스 내부의 비밀인 '메서드'와 '속성'은 클래스의 내부로 철저하게 '캡슐화'돼야 한다는 의미 !
개념 관점 - 명세 관점 - 구현 관점 순서로 객체지향을 설계하는 것이 아니라,
이 세 가지 관점(개념, 명세, 구현)이 명확하게 드러날 수 있는 코드를 작성해야 한다는 것 !
도메인 개념을 참조하는 이유
- 앞서 말했듯이 '도메인'이라는 개념 안에서 적절한 객체를 선택하는 것은 시스템의 유지보수성에 아주 큰 이점을 제공해준다.
인터페이스와 구현을 분리하라
- 명세 관점과 구현 관점이 뒤섞이지 않도록 하라는 말과 같은 말이다.
➙ 명세 관점은 클래스의 '안정적인' 측면을, 구현 관점은 클래스의 '불안정적인' 측면을 드러내도록 해야 한다.
(개념 관점과 구현 관점을 분리하는 것은 그다지 중요하지 않다고 한다.)
클래스를 봤을 때 클래스를 '명세 관점'과 '구현 관점'으로 나누어 볼 수 있는 능력이 중요하다 !
▼ Study
클래스에서의 '안정한' 부분과 '불안정한' 부분
- 요청하는 메시지 부분인 메서드 이름과 인자가 '안정한' 부분이라고 할 수 있고, 메서드의 구현부가 '불안정한' 부분인 것 같습니다 !
Issue_1
"커피를 주문하라"라는 책임이 손님에게 할당된 것을 유스케이스 관점에서 생각해보면,
유스케이스 명 : 커피를 주문하라
일차 엑터 : 외부 누군가??
이런식으로 정의할 수 있을까요?
- 지난 스터디 때 이야기했던 내용을 토대로 생각해보면 적절한 것 같습니다.
Issue_2
변화에 안정적인 인터페이스를 만들기 위해선 인터페이스를 통해 구현과 관련된 세부사항이 드러내지 않게 해야 한다는 것이 코드 상에서 어떻게 한다는 것인지 잘 모르겠습니다. 추가적으로, public 메서드가 공용 인터페이스를 드러내기 때문에 수정하기 어렵다고 한 것도 public 메서드의 어떤 부분을 수정하기 어렵다고 하는 것인지 특정하기가 힘든 것 같아요.
- 책에서는 자바 인터페이스와 관련된 개념을 생략하고, 클래스 내부에서의 메서드 이름과 인자가 인터페이스로 표현한 것 같습니다. 따라서, 추상 메서드 같이 메서드 이름과 인자가 public 메서드가 드러내는 공용 인터페이스 부분에 해당하고, 추상 메서드를 구현(implementation)한 부분이 외부에 영향이 미치지 않는 선에서 수정할 수 있고 이 책에서 말하는 불안정한 부분에 해당한다고 정리할 수 있을 것 같습니다 !
- 추가적으로 설계관점에서 최대한 명세 관점과 구현 관점을 잘 구분해서 변화가 발생했을 때 인터페이스 변경이 발생하지 않게 하여 설계하고 코드 상으로 구현하는 것을 의미하는 것 같습니다.
Issue_3
책에서 제시한 인터페이스 구현은 우리가 일반적으로 사용하는 인터페이스 선언 방법과 다른거 같아 헷갈립니다.
- Issue_2에 대해 이야기한 내용을 참고하면 해결될 것 같습니다.
▼ 회고
책의 모든 내용을 기억하는 것은 일반적으로는 불가능하다고 생각한다. 그래서 항상 저자가 어떤 의도로 책을 작성했고 어떤 부분이 키포인트인지 파악하는 것이 독서에 있어서 가장 중요한 것 같다. 또한, 책이 담고 있는 내용을 어떻게 수용할지는 독자에 따라 차이가 있을 수 밖에 없다. 그렇기 때문에 읽은 책에 대해 사람들과 소통하고 서로의 생각을 나눠봐야 비로소 제대로 된 독서의 힘이 발휘된다고 생각한다.
이번 스터디도 보면 매시간 각자가 생각하는 키포인트가 조금씩 달랐고 같은 구절을 읽고 다르게 이해하는 경우도 굉장히 많았다. 예를 들어 지난주에 진행한 스터디에선, "유스케이스(use case)"라는 개념을 적용시키려는 소프트웨어 시스템의 범위가 개개인마다 달랐다. 그리고 지지난주에 저자가 말하는 "인터페이스"가 일반적으로 자바(java)에서 다루는 인터페이스(interface)를 의미하는 것이라 생각하는 사람들도 있었고, 메서드(method)와 함수(function) 같은 개념도 포함하는 포괄적인 개념이라고 이해하는 사람들도 있었다. 그래서 그러한 내용들을 블로그에 추가로 정리하여 복기하는 것을 가장 중요하게 생각하고 스터디에 참여했던 것 같다.
누군가에게 이 책을 한마디로 소개해줘야 한다면,
"객체지향 5원칙(SOLID)"을 쉽게 풀어쓴 책이라고 말해줄 것 같다.
이처럼 서로 이해한 내용들에 대해 이야기 해보면서 혼자서 책을 읽었을 때보다 좀 더 '객관적인' 시각으로 바라보게 돼 시야를 넓힐 수 있다는 점이 북 스터디의 가장 큰 이점인 것 같다. 사실 이 책을 갖고 이렇게 오랫동안 스터디를 하게 될 줄은 몰랐다. 그래서 굳이 북 스터디의 단점을 꼽으라고 한다면 한 책을 완독하는 데 상당히 많은 시간과 기간이 요구된다는 점인데, 그만큼 얻어가는 것도 많다고 생각한다. 그래서 이후에도 나에게 도움될 만하고 흥미로운 책을 읽게 된다면 아마 북 스터디를 직접 모집하지 않을까 싶다 !