7장 : 함께 모으기

커피 전문점 도메인

마틴 파울러는 객체지향 설계 안에 존재하는 세가지 상호 연관된 세 가지 관점에 대해 설명한다.

✔️ 개념 관점(Conceptual Perspective)

  • 설계는 도메인 안에 존재하는 개념과 개념들 사이의 관계를 표현한다.

  • 도메인이란 사용자들이 관심을 가지고 있는 특정 분야나 주제를 말하며 소프트웨어는 도메인에 존재하는 문제를 해결하기 위해 개발된다.

  • 이 관점은 사용자가 도메인을 바라보는 관점을 반영한다.

  • 따라서 실제 도메인의 규칙과 제약을 최대한 유사하게 반영하는 것이 핵심이다.

✔️ 명세 관점(Specification Perspective)

  • 사용자의 영역인 도메인을 벗어나 개발자의 영역인 소프트웨어로 초점이 옮겨진다.

  • 명세 관점은 도메인의 개념이 아니라 실제 소프트웨어 안에서 살아 숨쉬는 객체들의 책임에 초점을 맞추게 된다.

  • 즉, 객체의 인터페이스를 바라보게 된다.

  • 명세 관점에서 프로그래머는 객체가 협력을 위해 ‘무엇’을 할 수 있는가에 초점을 맞툰다.

  • 인터페이스와 구현을 분리하는 것은 훌륭한 객체지향 설계를 낳는 가장 원칙이다.

✔️ 구현 관점(Implementation Perspective)

  • 프로그래머에게 가장 익숙한 관점으로, 실제 작업을 수행하는 코드와 연관돼 있다.

  • 구현 관점의 초점은 객체들이 책임을 수행하는 데 필요한 동작하는 코드를 작성하는 것이다.

  • 따라서 프로그래머는 객체의 책임은 ‘어떻게’ 수행할 것인가에 초점을 맞추며 인터페이스를 구현하는 데 필요한 속성 메서드를 클래스에 추가한다.

클래스는 세 가지 관점을 통해 설계와 관련된 다양한 측면을 드러낼 수 있다.

  • 클래스가 은유하는 개념은 도메인 관점을 반영한다.

  • 클래스의 공용 인터페이스는 명세 관점을 반영한다.

  • 클래스의 속성과 메서드는 구현 관점을 반영한다.

클래스는 세 가지 관점을 모두 수용할 수 있도록 개념, 인터페이스, 구현을 함께 드러내야 한다.

to do

지금까지 역할, 책임, 협력을 이용해 객체의 인터페이스를 식별했다. 협력에 참여하기 위해 객체가 수신해야 하는 메시지를 결정하고 메시지들이 모여 객체의 인터페이스를 구성한다는 점을 기억하라. 따라서 협력 안에서 메시지를 선택하고 메시지를 수신할 객체를 선택하는 것은 객체의 인터페이스, 즉 명세 관점에서 객체를 바라보는 것이다.

이번 장에서는 명세 관점에 더해 개념 관점과 구현 관점을 함께 다룰 것이다.

이를 통해 달성하고자 하는 두 가지 목표는 아래와 같다.

  1. 도메인 모델에서 시작해서 최종 코드까지의 구현 과정을 설명한다.

  2. 구현 클래스를 개념 관점, 명세 관점, 구현 관점에서 바라본다는 것이 무엇을 의미하는지를 설명한다.

커피주문

커피 전문점에서 커피를 주문하는 과정을 객체들의 협력 관계로 구현하는 것이다.

커피 제조하기
  • 도메인은 커피 전문점이다.

  • 커피 전문점에서는 아메리카노, 카푸치노, 카라멜 마키야또, 에스프레소의 네 가지 커피를 판매하고 있다.

  • 손님은 메뉴판을 본 후 커피를 주문하며, 바리스타는 주문받은 커피를 제조한다.

  • 최종 목표 : 손님이 커피를 주문하는 사건을 객체를 이용해 컴퓨터 안에 재구성하는 것이다.

커피 전문점이라는 세상

✔️ 커피 전문점을 구성하는 요소

  • 커피 전문점 안에는 메뉴판이 존재하고, 메뉴판에는 아메리카노, 카푸치노, 카라멜 마끼야또 에스프레소의 네 가지 커피 메뉴가 적혀 있다.

  • 객체지향의 관점에서 메뉴판은 하나의 객체다. 메뉴 항목들 역시 객체로 볼 수 있따.

  • 손님은 메뉴판을 보고 바리스타에게 원하는 커피를 주문한다.

  • 객체의 관점에서 보면 손님과 바리스타 역시 하나의 객체다.

  • 바리스타는 주문을 받은 메뉴에 따라 적절한 커피를 제조한다.

  • 바리스타는 자율적으로 커피를 제조한다.

  • 바리스타가 제조하는 커피 역시 메뉴판, 메뉴 항목, 바리스타와 구별되는 자신만의 경계를 가지는 객체다.

종합해 보면 객체지향의 관점에서 커피 전문점이라는 도메인은 손님 객체, 메뉴 항목 객체, 메뉴판 객체, 바리스타 객체, 커피 객체로 구성된 작은 세상이다.

✔️객체들 간의 관계

  • 손님은 메뉴판에서 주문할 커피를 선택할 수 있어야 한다.

  • 손님은 어떤 식으로든 메뉴판을 알아야 한다. 즉, 객체 사이의 관계가 존재한다.

  • 손님은 바리스타에게 주문을 해야 하므로 손님과 바리스타 사이에도 관계가 존재한다.

  • 바리스타는 커피를 제조하는 사람이므로 당연히 자신이 만든 커피와 관계를 맺는다.

✔️ 객체를 타입으로 추상화

동적인 객체를 정적인 타입으로 추상화해서 복잡성을 낮춘다. 타입은 분류를 위해 사용된다. 상태와 무관하게 동일하게 행동할 수 있는 객체들은 동일한 타입의 인스턴스로 분류할 수 있다.

  • 손님 객체는 ‘손님 타입’의 인스턴스로 볼 수 있다.

  • 바리스타 객체는 ‘바리스타 타입’의 인스턴스로 볼 수 있다.

  • 아메리카노 커피, 에스프레소 커피, 카라멜 마키아또 커피, 카푸치노 커피는 모두 ‘커피 타입’의 인스턴스로 볼 수 있다.

  • 메뉴판 객체는 ‘메뉴판’ 타입의 인스터스다.

  • 메뉴판 객체는 아메리카노, 에스프레소, 카라멜 마키야또, 카푸치노라는 데 개의 메뉴 항목 개체를 포함할 수 있다.

  • 네 개의 메뉴 항목 객체 역시 모둘 동일한 ‘메뉴 항목 타입’의 인스턴스로 모델링할 수 있다.

✔️ 타입 간에 관계

  • 하나의 메뉴판 객체는 다수의 메뉴 항목 객체로 구성돼 있다. 메뉴 항목 객체가 메뉴판 객체에 포함돼 있다고 할 수 있다.

  • 메뉴판과 메뉴 항목 객체는 따로 떨어져 존재하지 않으며 하나의 단위로 움직인다.

  • 포함(containment) 관계 또는 합성(composition) 관계

  • 손님 타입은 메뉴판 타입을 알고 있어야 커피를 선택할 수 있지만 메뉴판 타입은 손님의 일부가 아니므로 이 관계는 합성 관계가 아니다.

  • 한 타입의 인스턴스가 다른 타입의 인스턴스를 포함하지는 않지만 서로 알고 있어야 할 경우 이를 연관(association) 관계라고 한다.

  • 바리스타 타입은 커피를 제조해야 하므로 커피 타입을 알고 있어야 한다.

  • 메뉴판 타입과 커피 타입 중 어떤 것도 바리스타의 일부가 아니므로 이 관계 역시 포함관계는 아니다.

소프트웨어가 대상으로 하는 영역인 도메인을 단순화해서 표현한 모델을 도메인 모델이라고 한다.

설계하고 구현하기

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

  • 훌륭한 객체는 훌륭한 협력을 설계할 때만 얻을 수 있다.

  • 협력을 설계할 때는 객체가 메시지를 선택하는 것이 아니라 메시지가 객체를 선택하게 해야 한다.

인터페이스 정리하기

  • 객체가 수신한 메시지가 객체의 인터페이스를 결정한다.

  • 메시지가 객체를 선택했고, 선택된 객체는 메시지를 자신의 인터페이스로 받아들인다.

  • 객체들의 협력은 실행 시간에 컴퓨터 안에서 일어나는 상황을 동적으로 묘사한다.

  • 소프트웨어의 구현은 동적인 객체가 아닌 정적인 타입을 이용해 이뤄진다.

  • 따라서 객체들을 포괄하는 타입을 정의한 후 식별된 오퍼레이션을 타입의 인터페이스에 추가해야 한다.

  • 객체의 타입을 구현하는 일반적인 방법은 클래스를 이용하는 것이다.

  • 협력을 통해 식별된 타입의 오퍼레이션은 외부에서 접근 가능한 공용 인터페이스의 일부이다.

  • 따라서 인터페이스에 포함된 오퍼레이션 역시 외부에서 접근 가능하도록 공용(Public)으로 선언돼 있어야 한다.

class Customer {
	public void order(String menuName) {}
}

class MenuItem {
}

class Menu {
	public MenuItem choose(String name) {}
}

class Barista {
	public Coffee makeCoffee(MenuItem mynuItem) {}
}

class Coffee {
	public Coffee(MenuItem menuItem) {]
}

구현하기

  • 클래스의 인터페이스를 식별했으므로 이제 오퍼레이션을 수행하는 방법을 메서드로 구현하자.

  • Customer가 Menu에게 menuName에 해당하는 MenuItem을 요청하고, 이 MenuItem을 받아 Barista에게 전달해서 원하는 커피를 제조하도록 요펑하기 위해서 Menu와 Barista 객체에 접근해야 한다.

  • 객체가 다른 객체에게 메시지를 전송하기 위해서는 먼저 객체에 대한 참조를 얻어야 한다.

  • 객체 참조를 얻는 다양한 방법이 있지만 여기서는 Customer의 order() 메서드의 인자로 객체를 넘겨준다.

class Customer {
	public void order(String menuName, Menu menu, Barista barista) {}
}

class Customer {
	 public void order(String menuName, Menu menu, Barista barista) {
		 MenuItem menuItem = menu.choose(menuName);
		 Coffee coffee = barista.makeCoffee(menuItem);
		 ..
	 }
}
reference

구현하지 않고 머릿속으로만 구상한 설계는 코드로 구현하는 단계에서 대부분 변경된다. 설계작업은 구현을 위한 스케치를 작성하는 단계지 구현 그 자체일 수는 없다. 중요한 것은 설계가 아니라 코드다.

협력을 구상하는 단계에 너무 오랜 시간을 쏟지 말고 최대한 빨리 코드를 구현해서 설계에 이산이 없는지, 설계가 구현 가능한지를 판단해야 한다. 코드를 통한 피드백 없이는 깔끔한 설계를 얻을 수 없다.

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;
	}
}
reference

MenuItem의 목록을 Menu의 속성으로 포함시킨 결정 역시 클래스를 구현하는 도중에 내려졌다. 객체의 속성은 객체의 내부 구현에 속하기 때문에 캡슐화돼야 한다.

이를 위해 인터페이스를 정하는 단계에서 객체가 어떤 속성을 가지는지, 또 그 속성이 어떤 자료 구조로 구현됐는지를 고려하지 않는다.

class Barista {
	public Coffee makeCoffee(MenuItem menuItem) {
		Coffee coffee = new Coffee(menuItem);
		return coffee;
	}
}
class Coffee {
	private String name;
	private int price;

	public Coffee(MenuItem menuItem) {
		this.name = menuItem.getName();
		this.price = menuItem.cost();
	}
}
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;
	}
}

코드와 세 가지 관점

추상화 기법

Last updated