본문 바로가기

Study

객체지향의 사실과 오해 스터디 7장 발표 자료

목차

  • 07 함께 모으기
    • 커피 전문점 도메인
      • 커피 주문
      • 커피 전문점이라는 세상
    • 설계하고 구현하기
      • 커피를 주문하기 위한 협력 찾기
      • 인터페이스 정리하기
      • 구현하기
    • 코드의 세 가지 관점
      • 코드는 세 가지 관점을 모두 제공해야 한다
      • 도메인 개념을 참조하는 이유
      • 인터페이스와 구현을 분리하라
    • 추상화 기법

07 함께 모으기

서문

  • 객체지향에는 개념, 명세, 구현의 세 가지 관점이 있다.
    • 각 관점은 클래스를 바라보는 방식에 관한 것이며, 개발 순서에 따른 것이 아니다.
    • 클래스는 아래의 세 가지 관점을 모두 수용하고, 명확하게 분리할 수 있어야 한다.
    • 개념 관점은 도메인 내부의 각 개념들 간 관계에 관한 것이다.
      • 도메인은 사용자들이 관심을 가지는 특정 분야나 주제를 뜻한다.
        • 소프트웨어의 목적은 현실 도메인에 존재하는 문제를 해결(비즈니스 로직)하는 것이다.
    • 명세 관점은 소프트웨어에 관한 것이다.
      • 명세 관점은 실제 소프트웨어에 존재하는 객체의 책임(인터페이스)에 초점을 맞춘다.
      • 인터페이스와 구현을 분리하고, 인터페이스의 책임을 구성하는 것에 집중한다.
    • 구현 관점은 객체들의 책임 수행에 필요한 코드에 관한 것이다.
      • 각 객체가 책임을 어떻게 수행할 것인가에 집중한다.

커피 전문점 도메인

커피 주문

  • 손님은 메뉴판을 통해 커피를 주문한다.
  • 바리스타는 주문받은 커피를 제조한다.

커피 전문점이라는 세상

  • 커피 전문점 도메인에는 손님 객체, 메뉴 항목 객체, 메뉴판 객체, 바리스타 객체, 커피 객체가 존재한다.
    • 메뉴판 객체에는 메뉴 항목 객체인 아메리카노, 카푸치노, 카라멜 마키아또, 에시프레소가 속해 있다.
    • 손님 객체는 메뉴판 객체를 통해 바리스타 객체에게 커피 객체를 주문한다.
    • 바리스타 객체는 커피 객체를 제조한다.
  • 요구사항 분석
    • 손님은 메뉴판에서 주문할 커피를 선택할 수 있어야 한다.
      • 손님은 메뉴판을 알아야 한다. 즉, 두 객체 사이에 관계가 존재해야 한다.
    • 손님은 바리스타에게 주문을 할 수 있어야 한다.
      • 두 객체 사이에 관계가 존재해야 한다.
    • 바리스타는 커피를 제조할 수 있어야 한다.
      • 두 객체 사이에 관계가 존재해야 한다.
  • 인간이 복잡한 객체를 다루기 위해서는 동적인 객체를 정적인 타입으로 추상화해야 한다.
    • 동일한 행동을 가진 객체는 동일한 타입의 인스턴스로 분류할 수 있다.
      • 예) 메뉴판 객체는 메뉴판 타입의 인스턴스이다.
      • 예) 아메리카노와 에스프레소 객체는 커피 타입의 인스턴스이다.
  • Use Case 모델링
    1. 손님이 카페에 들어와 메뉴판을 확인한다.
    2. 손님은 선택한 커피를 바리스타에게 주문한다.
    3. 바리스타는 주문을 받고 커피를 제조한다.
    4. 바리스타는 제조된 커피를 손님에게 제공한다.
  • 도메인 모델링
    • 도메인 모델링은 소프트웨어가 대상으로 하는 도메인 영역을 단순화하는 작업이다.
    • 도메인 모델링을 통해 객체 간 합성 관계를 표현할 수 있다.
      • 메뉴판 객체는 다수의 메뉴 항목 객체를 포함한다.
        • 이는 객체 간 포함 관계로 표현할 수 있다.
      • 손님 타입은 메뉴판 타입을 알고 있으며, 서로 포함하지 않는다.
        • 이는 객체 간 연관 관계로 표현할 수 있다.

설계하고 구현하기

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

  • 협력 설계에서는 먼저 메시지를 선택하고, 메시지에 적합한 객체를 선택해야 한다(개념 관점).
    • 객체가 메시지를 처리할 책임을 맡게 되면, 해당 메시지는 객체의 공용 인터페이스에 추가된다.
  • 커뮤니케이션 다이어그램 모델링
    • 첫 번째 메시지: 커피를 주문하라(아메리카노)
      1. 책임을 수행하기에 적합한 타입을 찾는다. —> 손님 타입
      2. 책임을 수행할 객체를 해당 타입의 인스턴스로 만든다. —> 손님 객체
      3. 현실 객체와 유사한 이름을 붙인다. —> 손님
      4. 손님이 할 수 없는 일을 찾는다. —> 메뉴 항목을 모른다.
      5. 할 수 없는 일을 다른 객체에게 위임한다. —> 메뉴판 객체
    • 두 번째 메시지: 메뉴 항목을 찾아라(메뉴 이름)
      1. 책임을 수행하기에 적합한 타입을 찾는다. —> 메뉴판 타입
      2. 책임을 수행할 객체를 해당 타입의 인스턴스로 만든다. —> 메뉴판 객체
      3. 현실 객체와 유사한 이름을 붙인다. —> 메뉴판
      4. 메뉴판이 할 수 없는 일을 찾는다. —> 없음
      5. 메뉴 항목을 찾는다.
      6. 메뉴 항목을 반환한다.
    • 세 번째 메시지: 커피를 제조하라(메뉴 항목)
      1. 책임을 수행하기에 적합한 타입을 찾는다. —> 바리스타 타입
      2. 책임을 수행할 객체를 해당 타입의 인스턴스로 만든다. —> 바리스타 객체
      3. 현실 객체와 유사한 이름을 붙인다. —> 바리스타
      4. 바리스타가 할 수 없는 일을 찾는다. —> 커피 객체 생성
      5. 네 번째 메시지: 생성하라(메뉴 항목)
        1. 책임을 수행하기에 적합한 타입을 찾는다. —> 커피 타입
        2. 책임을 수행할 객체를 해당 타입의 인스턴스로 만든다. —> 커피 객체
        3. 현실 객체와 유사한 이름을 붙인다. —> 커피
        4. 커피가 할 수 없는 일을 찾는다. —> 없음
        5. 아메리카노의 이름과 가격을 초기화한다.
        6. 아메리카노를 반환한다.
      6. 아메리카노를 제조한다.
      7. 아메리카노를 반환한다.

인터페이스 정리하기

  • 각 객체가 수신 가능한 메시지를 토대로 인터페이스를 구성한다(명세 관점).
    • 손님 객체: 커피를 주문하라
    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) {}
    }
    

구현하기

  • 인터페이스를 메소드로 구현한다(구현 관점).
  • 커피를 주문하라
class Customer {
    public void order(String menuName, Menu menu, Barista barista) {
        MenuItem menuItem = menu.choose(menuName);
        Coffee coffee = barista.makeCoffee(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;
    }
}
  • 커피를 제조하라
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;
    }
}
  • 클래스 다이어그램 모델링

 

코드와 세 가지 관점

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

  • 개념 관점
    • 도메인을 구성하는 개념과 관계를 반영한다.
      • 예) Customer, Menu, MenuItem, Barista, Coffee
    • 현실의 도메인을 최대한 반영하여 변경과 유지보수를 편리하게 해 준다.
  • 명세 관점
    • 공용 인터페이스에 초점을 맞춘다.
    • 인터페이스를 통해 구현과 관련된 세부 사항이 드러나지 않게 함으로써, 변화에 대한 안정성을 제고한다.
  • 구현 관점
    • 클래스의 내부 구현에 초점을 맞춘다.
      • 메소드와 속성이 되도록 외부에 영향을 미치지 않도록 캡슐화한다.

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

  • 메시지 수신 객체를 선택하는 방법 중 하나는 도메인 개념 중 적합한 요소를 선택하는 것이다.
    • 인간에게 익숙한 도메인에 대한 지식을 기반으로 코드를 쉽게 이해할 수 있게 해 줌으로써 유지보수성을 향상시킨다.

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

  • 명세 관점과 구현 관점을 혼동하지 말아야 한다.
    • 명세 관점은 클래스의 안정적인 측면에 집중한다. 설계의 품질을 결정한다.
    • 구현 관점은 클래스의 불안정한 측면에 집중한다. 코드의 품질을 결정한다.
    • 명세 관점의 인터페이스가 구현 관점의 세부사항을 노출해서는 안 된다.