마틴 파울러는 객체지향 설계 안에 존재하는 세 가지 상호 연관된 세 가지 관점을 설명했다.
1. 개념 관점
개념 관점에서 설계는 도메인 안에 존재하는 개념과 개념들 사이의 관계를 표현한다.
도메인은 특정 분야/주제이며, 소프트웨어는 도메인 내 문제를 해결하기 위해 개발된다. (ex. 커피 전문점, 재판장 등)
개념 관점은 사용자가 도메인을 바라보는 관점을 반영하기 때문에, 실제 도메인의 규칙과 제약을 최대한 유사하게 반영하는 것이 핵심이다.
2. 명세 관점
사용자의 영역인 도메인을 벗어나 개발자의 영역인 소프트웨어로 초점이 옮겨진다.
도메인의 개념이 아닌 실제 소프트웨어 내 객체들의 책임에 초점을 맞추어 객체의 인터페이스를 바라보게 된다.
프로그래머는 객체가 협력을 위해 '무엇'을 할 수 있는가에 초점을 맞춘다. (ex. 검사라는 객체는 증인을 심문하는 책임이 있다.)
인터페이스와 구현은 분리되어야 한다.
3. 구현 관점
실제 작업을 수행하는 코드와 연관돼 있다.
객체들이 책임을 수행하는 데 필요한 동작하는 코드를 작성하는 것에 초점을 맞춘다.
프로그래머는 객체의 책임을 '어떻게' 수행할 것인가에 초점을 맞추며 인터페이스 구현에 필요한 속성과 메서드를 클래스에 추가한다.
개념/명세/구현 관점은 동일한 클래스를 서로 다른 방향에서 바라보는 것이다.
- 클래스의 타입은 도메인에서의 개념 관점을,
- 클래스의 공용 인터페이스(객체가 수신할 메시지)는 명세 관점을,
- 클래스의 속성과 메소드는 구현 관점을 반영한다.
클래스는 세 관점을 모두 수용할 수 있도록 개념, 인터페이스, 구현을 함께 드러내야 하며
동시에 세 관점을 쉽게 식별할 수 있도록 분리해야 한다.
협력을 위해 수신하는 메시지를 결정하고, 메시지들이 모여 객체의 인터페이스를 구성한다.
협력 안에서 메시지를 선택하고 수신할 객체를 선택하는 것은 명세 관점에서 객체를 바라보는 것이다.
커피 전문점 도메인
메뉴판: 아메리카노, 카푸치노, 카라멜 마키아또, 에스프레소 총 네 가지 커피 메뉴가 적혀있다.
- 메뉴 항목들은 각각 객체이며, 메뉴판 또한 하나의 객체이다.
손님: 메뉴판을 통해 커피를 선택하고, 바리스타에게 커피를 주문한다.
바리스타: 주문 받은 커피를 제조한다.
- 바리스타가 제조할 수 있는 커피는 아메리카노, 카푸치노, 카라멜 마키아또, 에스프레소다.
커피 전문점이라는 도메인은 손님, 메뉴, 메뉴판, 바리스타, 커피 객체들로 구성된 세상이다.
복잡성을 낮추기 위해, 도메인 내 동적인 객체를 정적인 타입으로 추상화하자.
상태와 무관하게 동일하게 행동하는 객체들은 동일한 타입으로 분류할 수 있다.
커피 전문점이라는 도메인은 손님 객체, 메뉴 항목 객체, 메뉴판 객체, 바리스타 객체, 커피 객체로 구성된 세상이다.
1. 먼저 복잡성을 낮추기 위해, 도메인 속의 동적인 객체를 정적인 타입으로 추상화해보자.
이 때, 타입은 분류를 위해 사용되기 때문에 상태와 무관하게 동일하게 행동하는 객체들은 동일한 타입으로 분류할 수 있다.
- 손님 객체는 “손님 타입" 의 인스턴스
- 바리스타 객체는 “바리스타 타입"의 인스턴스
- 아메리카노 커피, 에스프레소 커피, 카라멜 마키아또 커피, 카푸치노 커피는 모두 “커피 타입"의 인스턴스
- 메뉴판 객체는 “메뉴판 타입"의 인스턴스, 메뉴 항목 객체는 “메뉴 항목 타입"의 인스턴스
2. 개념 관점에서 타입(개념) 간의 관계를 나타내보자.
개념 관점에서 설계란 도메인 안에 존재하는 “개념” 과 “개념”들 사이의 관계를 표현하는 것이다.
1. 포함 관계
메뉴 항목들은 메뉴판에 포함되어 있다.
마름모는 포함 관계를 나타내며, 4는 포함되는 메뉴 항목이 4개임을 나타낸다.

2. 연관 관계
손님은 메뉴판을 알고 있어야 커피를 선택할 수 있다.
손님과 메뉴판은 연관있는 관계이다.

마찬가지로 손님은 바리스타와 연관 관계가,
바리스타는 커피와 연관 관계가 성립니다.
3. 도메인 모델
최종적으로 아래와 같은 도메인 모델을 표현할 수 있다.

3. 명세 관점에서 협력을 살펴보자.
객체지향 설계의 첫 번째 목표는 객체 간의 훌륭한 협력을 설계하는 것이다.
메시지를 먼저 선택하고, 이 메시지를 수신할 적절한 객체를 선택해야 한다.
커피 전문점에서의 첫 메시지는 ‘커피를 주문하라' 일 것이고, 이 메시지를 수신할 적절한 객체는 손님이다.
손님 객체는 외부에 제공하는 공용 인터페이스에 "커피를 주문하다"를 포함시켜야한다. (ex. Customer.orderCoffee() )
또한 손님은 메뉴의 가격, 사이즈 등에 대해서는 알지 못한다.
여기서 메뉴 항목을 찾아라" 라는 새로운 메시지가 등장하며, 이 메시지를 수신할 적절한 객체는 메뉴판이다.
이렇게 협력에 필요한 메시지를 찾고, 찾은 메시지를 수신할 적절한 객체를 선택한 것을 바탕으로
각 객체가 무엇을 할 수 있는지를 정하여 인터페이스를 구현할 수 있다.
객체의 타입을 구현하는 일반적인 방법은 클래스를 이용하는 것이다.
협력을 통해 식별된 공용 인터페이스는 외부에서 접근 가능해야하기 때문에 public으로 선언하자.
class Customer {
public order(menuName: string): void {}
}
class MenuItem {}
class Menu {
public choose(name: string): MenuItem {
return new MenuItem();
}
}
class Barista {
public makeCoffee(menuItem: MenuItem): Coffee {
return new Coffee();
}
}
class Coffee {
public Coffee(menuItem: MenuItem) {}
}
또는 타입스크립트의 interface를 정의할 수도 있을 것이다.
interface ICustomer {
order: (menuName: string, menu: Menu, barista: Barista) => void;
}
interface IMenuItem {
getName: () => string;
getPrice: () => number;
}
interface IMenu {
choose: (name: string) => IMenuItem;
}
interface IBarista {
makeCoffee: (menuItem: MenuItem) => ICoffee;
}
interface ICoffee {}
4. 실제로 인터페이스가 동작하게끔 구현해보자.
class Customer implements ICustomer {
public order(menuName: string, menu: Menu, barista: Barista): void {
const menuItem: MenuItem = menu.choose(menuName);
barista.makeCoffee(menuItem);
}
}
class MenuItem implements IMenuItem {
private name: string;
private price: number;
constructor(name: string, price: number) {
this.name = name;
this.price = price;
}
public getPrice(): number {
return this.price;
}
public getName(): string {
return this.name;
}
}
class Menu implements IMenu {
private items: MenuItem[];
constructor(items: MenuItem[]) {
this.items = items;
}
public choose(name: string): MenuItem {
const item = this.items.find((i) => i.getName() === name);
if (!item) throw Error("존제하지 않는 메뉴입니다");
return item;
}
}
class Barista implements IBarista {
public makeCoffee(menuItem: MenuItem): Coffee {
return new Coffee(menuItem);
}
}
class Coffee implements ICoffee {
private name: string;
constructor(menuItem: MenuItem) {
this.name = menuItem.getName();
}
}
정리해보면...
1. 개념 관점
코드에서 Customer, Menu, MenuItem, Barista, Coffee 객체를 볼 수 있다.
해당 클래스들은 커피 전문점이라는 도메인을 구성하는 개념과 관계를 반영하고 있다.
개념 관점이 명확하다면 유지보수가 용이하다. (커피를 제조하는 과정을 변경해야할 때, Barista 객체만 수정하면 된다.)
2. 명세 관점
코드에서 객체가 무엇을 하는지 인터페이스를 통해 알 수 있다.
클래스의 public 메서드는 협력을 위한 공용 인터페이스를 드러낸다.
3. 구현 관점
클래스의 메서드와 속성은 구현에 속하며 공용 인터페이스의 일부가 아니다.
따라서 외부의 객체에 영향을 미쳐서는 안 되며, 철저하게 클래스 내부로 캡슐화돼야 한다.
클래스를 명세 관점과 구현 관점으로 나눠볼 수 있어야 한다.
인터페이스와 구현을 분리하라!!