[자라기] - 객체지향 설계과정 알아보기

김주형·2023년 1월 31일
0

자라기

목록 보기
22/22
post-thumbnail

Reference 🙇🏻‍♂️


커피 전문점 도메인

Goal

  • 커피 전문점에서 커피를 주문하는 과정을 '객체들의 협력 관계'로 구현하기
  • '손님이 커피를 주문'하는 사건을 객체를 이용하여 컴퓨터 안에 재구성하기

구성 요소

커피 전문점을 객체들로 구성된 작은 세상으로 바라보았을 때 구성 요소는 다음과 같습니다.

  • 메뉴판: 메뉴 항목 { 아메리카노, 카푸치노, 카라멜 마키야또, 에스프레소 }
    • 4개의 메뉴 항목(객체)들로 구성
    • 메뉴 항목 객체를 보유한 객체
  • 손님: 메뉴판을 보고 바리스타에게 원하는 커피를 주문
    - 자신이 원하는 메뉴 항목 객체 (in 메뉴판 객체)를 선택
    • 선택 결과를 바리스타 객체에게 전달
  • 바리스타: 주문 받은 메뉴에 따라 적절한 커피 제조
    - 자율적으로 커피를 제조하는 객체
  • 커피: 아메리카노, 카푸치노, 카라멜 마키야또, 에스프레소

커피 전문점은 도메인이며 객체지향 관점에선
객체 (손님, 메뉴판, 메뉴 항목, 바리스타, 커피)로 구성된 작은 세상입니다.


객체 관계

  • 손님은 메뉴판에서 주문할 커피를 선택할 수 있어야 합니다.
  • 손님은 바리스타에게 주문을 해야 합니다.
    • 손님은 메뉴판과 바리스타와의 관계가 존재합니다.
  • 바리스타는 커피를 제조합니다.
    • 자신이 만든 커피와 관계를 맺습니다.

복잡성 낮추기

  • 동적인 객체를 정적인 타입으로 추상화 하면 복잡성을 낮출 수 있다.
  • 타입은 분류를 위해 사용된다.
    • 동적인 객체들을 정적인 타입으로 추상화 하여 분류할 수 있다.
    • 타입을 클래스라고 생각하면 안된다. (타입 != 클래스)
    • 클래스 : 객체의 내부 상태와 그 객체의 연산에 대한 구현 방법을 정의
    • 타입 : 객체의 인터페이스, 즉 그 객체가 응답할 수 있는 요청의 집합을 정의
      • 하나의 객체가 여러 타입을 가질 수 있고 서로 다른 클래스의 객체들이 동일한 타입을 가질 수 있다.
      • 객체의 구현은 다를지라도 인터페이스는 같을 수 있다. (출처 GoF의 디자인 패턴, p.46)
  • 상태와 무관하게 동일하게 행동할 수 있는 객체들은 동일한 타입으로 분류할 수 있다.
  • 손님 객체 -> ‘손님 타입’의 인스턴스
  • 바리스타 객체 -> ‘바리스타 타입’의 인스턴스
  • 4가지 커피 모두 -> ‘커피 타입’의 인스턴스
  • 메뉴판 객체 -> ‘메뉴판 타입’의 인스턴스
  • 4가지 메뉴 항목 객체들 -> 모두 동일한 ‘메뉴 항목 타입’의 인스턴스
  • 메뉴판 객체 -> 4개의 메뉴 항목 객체를 포함

타입 관계

객체를 타입 별로 분류한 이후 타입 별 관계를 파악하면 다음을 알 수 있다고 합니다.

  • 어떤 타입이 도메인을 구성하는가
  • 타입들 사이 어떤 관계가 존재하는가 -> 도메인 이해

2

커피 전문점 도메인을 구성하는 타입들의 종류와 관계를 표현

  • 검은 마름모 : 포함(containment) 또는 합성(composition) 관계
  • 숫자 : 포함되는 항목의 갯수
  • 단순한 선 : 연관 관계(association)

여기에서 도메인과 도메인 모델의 의문을 어느정도 해소할 수 있습니다.

  • 도메인 : 소프트웨어가 대상으로 하는 영역
    • 사용자 관점에서 인간이 인지한 추상적, 논리적인 멘탈 모델을 소프트웨어가 대상으로써 삼아야함
  • 도메인 모델 : 도메인을 단순화해서 표현한 모델

객체지향 설계

  • 객체지향의 첫 번째 목표 : 훌륭한 객체 설계 x, 훌륭한 협력 설계 o
  • 객체보다는 메시지를 먼저 선택
  • 메시지를 수신하기에 적당한 객체를 선택
  • 메시지를 수신할 객체는 그것을 처리할 책임을 맡게 됨
  • 객체가 수신하는 메시지는 외부에 제공하는 공용 인터페이스에 포함 됨

'협력하는 자율적인 객체들의 공동체'를 곱씹어보면 각 객체들의 협력을 설계할 단계임을 알 수 있습니다.
협력을 설계한다는 것은 적절한 객체에게 적절한 책임을 할당하는 것을 의미합니다.

커피를 주문하라

커피를 주문하는 협력 설계

2

  • 메시지 위 화살표 : 인자 ( 메시지에 전달 될 부가 정보)

메시지를 선택하면 수신하기에 적절한 객체를 선택할 수 있습니다.

커피를 주문할 책임을 가질 객체로 적절한 것은 손님이며,
동시에 해당 메시지를 처리할 객체는 손님 타입의 인스턴스임이 분명해짐을 알 수 있습니다.

메뉴 항목을 찾아라

손님은 메뉴 항목을 모르는 상황에서 스스로 할 수 있는 것이 없습니다.
이를 다른 객체에게 요청해야 하는 상황이 필연적입니다.
손님은 자신이 선택한 메뉴 항목을 누군가가 제공해 줄 것을 외부로 요청합니다.

2

손님은 요청한 '메뉴 이름'에 대응되는 '메뉴 항목'을 응답 받아야 합니다.
해당 메시지를 수신해서 메뉴 항목을 찾을 책임을 떠올리면 어떤 객체에게 할당하는게 적절할지가 좀 더 분명해집니다.
3

의인화

  • 메뉴판이 스스로 메뉴 항목을 반환하는 것이 익숙치 않을 수 있지만,
    객체지향 세계에서 모든 객체를 능동적이고 자율적인 존재로 인식하기 위해 개발자는 무생물도 생물처럼 '의인화'할 필요가 있습니다.

커피를 제조하라

메뉴 항목을 얻은 손님은 해당하는 커피를 제조해달라고 요청할 수 있습니다.
새로운 요청엔 새로운 메시지가 따라옴을 기억하면서 메시지를 먼저 정의합니다.

  • 손님은 '커피를 제조하라'라는 메시지를 '메뉴 항목'이라는 인자를 포함하여 전달
  • 제조된 커피를 응답 받아야 하는 상황

위 2가지 사실을 통해 바리스타에게 커피를 제조하는 책임을 할당시키는 것이 적절함을 알 수 있습니다.

4

  • 바리스타는 커피를 제조하는데 필요한 상태와 행위를 스스로 관리하는 자율적인 존재입니다.
    • 상태 : 커피를 제조하기 위한 지식
    • 행위 : 바리스타의 행동

바리스타가 커피를 제조하는 것으로 커피 주문의 협력이 끝납니다.

인터페이스

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

지금까지 얻어낸 것은 객체들의 인터페이스입니다.

  • 메시지가 객체를 선택했고, 선택된 객체는 메시지를 인터페이스로 받아들입니다.
  • 협력 설계에서 수신 가능한 메시지만 분리하여 추출하면 객체의 인터페이스가 됩니다.
  • 객체가 어떤 메시지를 수신할 수 있다 -> 그 객체의 인터페이스 안에 메시지에 해당하는 오퍼레이션이 존재한다는 것을 의미합니다.

각 객체별 설명

  • 손님 객체의 인터페이스에는 '제품을 주문하라'라는 오퍼레이션
  • 메뉴판 객체의 인터페이스 안에는 ‘메뉴 항목을 찾아라’ 라는 오퍼레이션
  • 바리스타 객체의 인터페이스 안에는 ‘커피를 제조하라’ 라는 오퍼레이션
  • 커피 객체는 ‘생성하라’ 라는 오퍼레이션

소프트웨어의 구현은 객체들을 포괄하는 타입을 정의한 후 식별된 오퍼레이션을 타입의 인터페이스에 추가해야 한다고 합니다.

p220
객체의 타입을 구현하는 일반적인 방법은 클래스를 이용하는 것이다. 협력을 통해 식별된 타입의 오퍼레이션은 외부에서 접근 가능한 공용 인터페이스의 일부라는 사실을 기억하라!

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

public interface Customer {
	public void order(String menuName);
}

public interface Menu {
	public MenuItem choose(String name);
}

public class MenuItem {} // 협력이 아닌 오퍼레이션에 포함된 부가 정보이므로 클래스를 통해 타입으로 분류

public interface Barista {
	public Coffee makeCoffee(MenuItem menuItem);
}

public class Coffee { // MenuItem과 마찬가지
	public Coffee(MenuItem menuItem);
}

구현하기

인터페이스를 식별했으니 오퍼레이션을 수행하는 방법을 메서드로 구현할 수 있습니다.

Customer

Customer의 협력

  • Customer는 Menu에게 menuName에 해당하는 MenuItem을 찾아달라고 요청
  • MenuItem을 받아 이를 Barista에게 전달함으로써 원하는 커피를 제조하도록 요청

customer

Customer는 메시지를 전송하기 위해서 Menu 객체와 Barista 객체에 대해 알고 있어야 합니다.
책에서는 Customer의 order() 메서드의 인자로 Menu와 Barista 객체를 전달받는 방법으로 참조 문제를 해결하기로 합니다.

// 의존성 연결
public interface Customer {
	public void order(String menuName, Menu menu, Barista barista);
}

// 구현
public class JooHyeong implemnts Customer {

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

참고 p222

  • 구현하지 않고 머릿속으로만 설계한 코드는 대부분 구현하는 단계에서 변경된다.
  • 설계 작업은 구현을 위한 스케치를 작성하는 단계지 구현 그 자체일 수는 없다.
  • 중요한 것은 설계가 아니라 코드다. 따라서 협력을 구상하는 단계에 너무 오랜 시간을 쏟지 말고 최대한 빨리 코드를 구현해서 설계에 이상이 없는지, 설계가 구현 가능한지를 판단해야 한다.
  • 코드를 통한 피드백 없이는 깔끔한 설계를 얻을 수 없다.
    (TDD가 베스트 프랙티스인 이유 같네요)

menuName에 해당하는 menuItem을 찾는 책임이 있습니다.
도메인 모델을 설계할 때처럼 간단히 메뉴가 내부적으로 menuItem을 포함하게 하겠습니다.

public class implements MenuItem
public class MenuImpl implements Menu {
	private List<MenuItem> items;
    
    public Menu(List<MenuItem> items){
		this.items = items;
    }
    
    @Overide
    public MenuItem choose(String name) {
    	for (MenuItem each : items) {
        	if (each.getName.equals(name) {
            	return each;
            }
        }
        return null;
    }

주목할 점 (p223)

  • MenuItem의 목록을 MenuImpl에 포함하는 과정이 클래스 구현 과정에서 이루어진 것에 주목하라
  • 객체의 속성은 객체의 내부 구현에 속하기 때문에 캡슐화되어야 한다.
  • 객체의 속성을 캡슐화 -> 인터페이스에는 객체의 내부 속성에 대한 어떤 힌트도 제공되어선 안됨을 의미
  • 이를 위한 가장 훌륭한 방법
    • 인터페이스를 결정하는 단계에서 객체가 어떤 속성을 가지는지, 그 속성이 어떤 자료구조로 구현됐는지를 고려하지 않는 것
  • 객체에게 책임을 할당하고 인터페이스를 결정할 때는 가급적 객체 내부의 구현에 대한 어떤 가정도 하지 말아야 한다.
  • 객체가 어떤 책임을 수행해야 하는지를 결정한 후에야 책임을 수행하는데 필요한 객체의 속성을 결정하라.
  • 이것이 객체의 구현 세부 사항을 객체 공용 인터페이스에 노출시키지 않고 인터페이스와 구현을 깔끔하게 분리할 수 있는 기본적인 방법이다.

Barista

public class BaristaImpl implements Barista {

	@Override
    public Coffee makeCoffee(MenuItem menuItem) {
    	Coffee coffee = new Coffee(menuItem);
        
    }

Coffee

public class Coffee {
	private String name;
    private int price;
    
    public Coffee(MenuItem menuItem) {
    	this.name = menuItem.getName();.
        this.price = menuItem.cost();
public class MenuItem {

	public MenuItem(String name, int price) {
    	this.name = name;
        this.price = price;
    }
    
    public int cost() {
    	return price;
    }
    
    public String getName() {
    	return this.name;
    }
}

중요한 측면을 모두 포함한 구조는 다음과 같습니다.
2

참고 (p225)

  • MenuItem의 인터페이스를 구성하는 오퍼레이션들을 MenuItem을 구현하는 단계에 와서야 식별했다는 점을 눈여겨보기 바란다.
  • 이것을 부끄러워해야 할 일이 아니다.
  • 인터페이스는 객체가 다른 객체와 직접적으로 상호작용하는 통로다.
  • 인터페이스를 통해 실제로 상호작용을 해보지 않은 채 인터페이스의 모습을 정확하게 예측하는 것은 불가능에 가깝다.
  • 설계를 간단히 끝내고 최대한 빨리 구현에 돌입하라. 머릿 속에 객체의 협력 구조가 번뜩인다면 그대로 코드를 구현하기 시작하라.
  • 설계가 제대로 그려지지 않는다면 고민하지 말고 실제로 코드를 작성해가면서 협력의 전체적인 밑그림을 그려보라.
  • 테스트-주도 설계로 코드를 구현하는 사람들이 하는 작업이 바로 이것이다. 그들은 테스트 코드를 작성해 가면서 협력을 설계한다.

정리

  • 책임 주도 설계가 이뤄지는 과정
  1. 도메인 구성 요소를 타입으로 분류한뒤 도메인 모델로 표현한다.
  • 도메인 모델에 객체지향 설계를 적용한다.
  1. 협력을 설계하기 위해 객체보다 메시지를 먼저 선택한다.
  2. 메시지의 처리 책임을 적절한 객체에게 할당한다.
  • 객체가 수신하는 메시지는 외부에 제공하는 공용 인터페이스에 포함된다.

  • 인터페이스와 구현의 분리

    • 인터페이스는 인터페이스 대로 정의한다.
      구현은 그 뒤에
profile
왔을때보다좋은곳으로

0개의 댓글