Chapter 7. 함께 모으기

이은지·2024년 1월 20일
0

북스터디 마지막 Chapter 7을 읽고 정리해보았다.

0. 개요

💙 마틴 파울러가 제시한 객체지향 설계 안에 존재하는 세 가지 상호 연관된 관점

  • 개념 관점
    • 도메인 안에 존재하는 개념과 개념들 사이의 관계를 표현
    • 해당 관점은 사용자가 도메인을 바라보는 관점을 반영하여 실제 도메인 규칙과 제약을 최대한 유사하게 반영하는 것이 핵심
  • 명세 관점
    • 도메인(사용자 영역)에서 벗어나 소프트웨어(개발자 영역)으로 초점이 옮겨짐
    • 해당 관점은 도메인의 개념이 아니라 소프트웨어 안 객체들의 책임에 초점을 맞추게 됨.
    • 즉, 객체가 협력을 위해 ‘무엇’을 할 수 있는가에 초점을 맞추게 되며, 객체의 인터페이스를 바라보게 됨
  • 구현 관점
    • 객체들이 책임을 수행하는 데 필요한 동작하는 코드를 작성하는 것
    • 객체의 책임을 ‘어떻게’ 수행할 것인가에 초점을 맞추며 인터페이스를 구현하는데 필요한 속성과 메서드를 클래스에 추가

💙 세 가지 관점을 클래스 설계에 적용한다면?

  • 개념/명세/구현 관점은 동일한 클래스를 세 가지 다른 방향에서 바라보는 것을 의미
    • 클래스가 은유하는 개념은 도메인 관점을 반영, 공용 인터페이스는 명세 관점 반영, 속성과 메서드는 구현 관점을 반영
  • 클래스는 세 가지 관점을 모두 수용할 수 있도록 개념, 인터페이스, 구현을 함께 드러내야 함

💡 이번 장의 주제는 3가지 관점을 함께 다루는 것
1. 예제를 통해 도메인 모델에서 시작해서 최종 코드까지의 구현 과정 설명
2. 구현 클래스를 개념 관점, 명세 관점, 구현 관점에서 바라보는 것이 무엇을 의미하는지 설명
(예제) 커피 전문점에서 커피를 주문하는 과정을 객체들의 협력 관계로 구현해보자.

1. 커피 전문점 도메인

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

  • 메뉴판: 아메리카노, 카푸치노, 카라멜 마키아또, 에스프레소 (4가지 커피 메뉴)
  • 손님
  • 바리스타
  • 바리스타가 제조하는 커피

→ 커피 전문점이라는 도메인은 손님 객체, 메뉴 항목 객체, 메뉴판 객체, 바리스타 객체, 커피 객체로 구성된 작은 세상

💙 객체지향의 관점에서 객체들 간의 관계

  • 동적인 객체를 정적인 타입으로 추상화
    • 손님 객체 = ‘손님 타입’의 인스턴스
    • 바리스타 객체 = ‘바리스타 타입’의 인스턴스
    • 아메리카노, 카푸치노, 카라멜 마키아또, 에스프레소 커피 객체 = ‘커피 타입’의 인스턴스
    • 메뉴판 객체 = ‘메뉴판’타입의 인슽너스
    • 아메리카노, 카푸치노, 카라멜 마키아또, 에스프레소 메뉴 항목 객체 = ‘메뉴 항목 타입’의 인스턴스
  • 타입 간 관계
    • 포함 관계 : 메뉴판 타입 ← 메뉴 항목 타입
      • 메뉴 항목이 메뉴판에 포함됨
    • 연관 관계 : 손님-메뉴판, 바리스타-커피, 손님-바리스타
      • 한 타입의 인스턴스가 다른 타입의 인스턴스를 포함하지는 않지만 알고 있어야 함
    • 커피 전문점 도메인을 구성하는 타입들의 종류와 관계를 표현한 도메인 모델

💡 이렇게 소프트웨어가 대상으로 하는 영역인 도메인을 단순화해서 표현한 모델을 도메인 모델이라고 함. 도메인 모델의 초점은 어떤 타입이 도메인을 구성하느냐와 타입들 사이에 어떤 관계가 존재하는지를 파악함으로써 도메인을 이해하는 것 !

2. 설계하고 구현하기

💙 객체 지향 설계의 첫 번째 목표

  • 훌륭한 객체를 설계하는 것이 아니라 훌륭한 협력을 설계하는 것
    • 협력을 설계할 때는 메시지를 먼저 선택하고 그 후에 메시지를 수신하기에 적절한 객체를 선택해야 함
    • 메시지를 수신할 객체는 메시지를 처리할 책임을 맡게 되고 객체가 수신하는 메시지는 외부에 제공하는 공용 인터페이스에 포함됨

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

  • 현재 설계하고 있는 협력: 커피를 주문하는 것

    • 첫번째 메시지: ‘커피를 주문하라’
      - 메시지 위에 붙은 화살표는 메시지에 담아 전달될 부가적인 정보인 인자
      - 아메리카노를 주문할 경우 → 커피를 주문하라(아메리카노)

      - 첫번째 메시지를 수신할 객체: 손님 객체(손님 타입의 인스턴스)
      - 이제, 손님 객체는 커피를 주문할 책임을 할당받음

      		![](https://velog.velcdn.com/images/ejdmswl/post/27d8824e-9aa8-4134-a2a5-f8c1d2763cbd/image.png)
    • 손님 객체는 할당된 책임을 수행하는 도중 스스로 할 수 없는 일은 메시지를 전송해 다른 객체에게 도움을 요청 : ‘메뉴 항목을 찾아라’

    • ‘메뉴 항목을 찾아라’ 메시지를 수신할 객체 : 메뉴판 객체

      • 메뉴판 객체는 책임을 수행하여 메뉴항목을 손님객체에게 반환함
    • 메뉴항목을 얻은 손님은 메뉴 항목에 맞는 커피를 제조해달라고 요청 : ‘커피를 제조하라’

    • ‘커피를 제조하라’ 메시지를 수신할 객체 : 바리스타 객체

    • 커피 주문을 위한 협력은 바리스타가 새로운 커피를 만드는 것으로 끝남

💙 인터페이스 정리하기

  • 협력을 설계하여 얻어낸 것은 객체들의 인터페이스

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

    • 객체가 어떤 메시지를 수신할 수 있다는 것은 그 객체의 인터페이스 안에 메시지에 해당하는 오퍼레이션이 존재한다는 것을 의미

      실제로 소프트웨어의 구현은 동적인 객체가 아닌 정적인 타입을 이용해 이루어지므로, 객체들을 포괄하는 타입을 정의한 후 식별된 오퍼레이션을 타입의 인터페이스에 추가해야함

    • 클래스를 통해 객체 타입 구현

      public class Customer {
      
          public void order(String menuName, Menu menu, Barista barista) {
              // 내용
          }
      }
      
      class MenuItem {}
      
      class Menu {}
      
      class Barista {}
      
      class Coffee {}

💙 구현하기

  • 인터페이스를 식별했으므로 오퍼레이션을 수행하는 방법을 메서드로 구현하면 된다.
  • Customer
public class Customer {

    public void order(String menuName, Menu menu, Barista barista) { 
		// order 인자값으로 Menu와 Barista 객체를 전달받아 참조 
        MenuItem menuItem = menu.choose(menuName); 
				//Menu에게 menuName에 해당되는 MenuItem을 찾아달라고 요청
        Coffee coffee = barista.makeCoffee(menuItem);
				//MenuItem을 Barista 객체에게 전달하여 커피를 제조해달라고 요청 
    }
}
  • Menu
public class Menu {

    private List<MenuItem> items; //Menu는 MenuItem의 목록을 포함 

    public Menu(List<MenuItem> items) {
        this.items = items;
    }

    public MenuItem choose(String name) { 
		//MenuItem의 목록을 하나씩 검사해가면서 이름이 동일한 MenuItem을 찾아 반환
        for (MenuItem each : items) {
            if (each.getName().equals(name)) { 
                return each;
            }
        }
        return null;
    }
}
  • Barista
public class Barista {
    
    public Coffee makeCoffee(MenuItem menuItem) { //MenuItem을 전달받아 커피를 제조 
        Coffee coffee = new Coffee(menuItem);
        return coffee;
    }
}
  • Coffee
public class Coffee {

    private String name;
    private int price;
		//커피는 자기 자신을 생성하기 위한 생성자 제공
		//MenuItem에게 요청을 보내 커피 이름과 가경을 얻은 후 Coffee 속성에 저장 
    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;
    }
		//Coffee의 요청에 응답할 수 있도록 cost() getName() 메서드 구현 
    public int cost() {
        return price;
    }

    public String getName() {
        return name;
    }
}

3. 코드와 세 가지 관점

💙 코드는 세 가지 관점을 모두 제공해야 한다.

  • 개념 관점에서 코드를 바라보면, Customer, Menu, MenuITem, Barista, Coffee 클래스가 보인다.
  • 소프트웨어 클래스가 도메인 개념의 특성을 최대한 수용하면 변경을 관리하기 쉽고, 유지보수성을 향상시킬 수 있다.
  • 명세 관점은 클래스의 인터페이스를 바라본다. 클래스의 public 메서드는 다른 클래스가 협력할 수 있는 공용 인터페이스를 드러낸다.
  • 최대한 변화에 안정적인 인터페이스를 만들기 위해서는 인터페이스를 통해 구현과 관련된 세부사항이 드러나지 않게 해야 한다.
  • 구현 관점은 클래스의 내부 구현을 바라본다.
  • 클래스의 메서드와 속성은 구현에 속하며 공용 인터페이스의 일부가 아니다.

💡 하나의 클래스 안에 세 가지 관점을 모두 포함하면서도 각 관점에 대응되는 요소를 명확하고 깔끔하게 드러내는 것이 핵심

💙 도메인 개념을 참조하는 이유

  • 도메인 개념 안에서 메시지를 수신할 적절한 객체를 선택하는 것은 도메인에 대한 지식을 기반으로 코드의 구조와 의미를 쉽게 유추할 수 있게 하여 시스템의 유지보수성에 큰 영향을 미침

💙인터페이스와 구현을 분리하라

  • 클래스를 명세 관점과 구현 관점으로 나눌 수 있어야함
  • 인터페이스가 구현 세부사항을 노출하기 시작하면 아주 작은 변동에도 전체 협력이 요동치는 취약한 설계를 얻을 수 밖에 없음
profile
소통하는 개발자가 꿈입니다!

0개의 댓글