study with book

[객체지향의 사실과 오해] 7. 함께 모으기

yjs3819 2021. 9. 27. 17:40
728x90

실제로 객체지향적으로 설계를 처음부터 끝까지 해보자. 그리고 객체지향 설계를 세가지 관점에서 바라보아 보자.

커피 전문점

어디서나 볼수 있는 동네 카페에서 손님이 커피를 주문해서 받는 과정까지를 객체지향적으로 설계해보자.

객체 생각하기

큰 시스템을 객체들로 구성되었다 생각하고 객체들로 나누어보자.

손님 - 손님 객체
메뉴판 - 메뉴판 객체
메뉴 - 메뉴 객체
바리스타 - 바리스타 객체
커피 - 커피 객체

객체간 관계 생각해보기

손님은 바리스타에게 주문해야함 : 손님 - 바리스타 관계 O
손님은 주문하기 위해선 메뉴판에서 메뉴를 골라야함 : 손님 - 메뉴판 관계 O
메뉴판에있는 메뉴들중 몇개를 먹을 커피를 골라야하므로 : 메뉴판 - 메뉴 관계 O
바리스타가 손님으로부터 주문받은 메뉴를 커피로 만들어야함 : 바리스타 - 커피 관계 O

객체를 클래스라는 도구를 통해 타입으로 추상화하기

객체는 동적, 복잡하다. 클래스라는 도구를 통해 타입으로 분류하여 추상화(간단하게)해보자.

손님 객체 - 손님 클래스의 인스턴스(손님 타입)
메뉴판 객체 - 메뉴판 클래스의 인스턴스(메뉴판 타입)
메뉴 객체 - 메뉴 클래스의 인스턴스(메뉴 타입)
바리스타 객체 - 바리스타 클래스의 인스턴스(바리스타 타입)
커피 객체 - 커피 클래스의 인스턴스(커피 타입)

참고로 타입은 상태는 무관, 행동만 같으면 같은 타입, 같은 클래스이다.

만들어진 타입과 객체간 관계들을 토대로 도메인 모델 만들기

메뉴는 메뉴판에 포함되어있으므로 포함관계, 나머지는 관계가 그냥있기에 연관관계. - 요런건 중요하지 않고 그냥 두 객체간의 관계가 있다는걸 인식하는 것이 중요하다.

객체에게 책임 할당하기

협력을 위해서 어떤 객체에게 어떤 책임을 할당해야할지 정해야한다.
객체지향설계는 좋은 협력을 만드는것!

어떻게? 바로 계속 강조했던 메시지가 객체를 선택하도록 한다 (이것이 자율적인 책임을 만든다고 했다.)

메시지는 책임을 자연스레 유발한다. 이러한 메시지를 객체가 찾게하지말고 메시지가 객체를 찾게하자. 이건 자율적인 책임을 만드는데 매우 중요하다.

가장 먼저 필요한 메시지는? '커피를 주문해라' 라는 메시지이다.

이 메시지에게 필요한 객체는? '손님 객체'

커피를 주문해라 --> 손님 객체

손님 객체는 메시지를 받아 책임을 수행해야한다. 그런데 손님 혼자 못하는 일이 무엇이 있을까?
손님은 어떤 메뉴가 뭐가있는지 알지못한다. 그렇기에 누군가에게 '요청'을 해야한다.
이 '요청'은 '메뉴를 선택해라'가 되겠다.

'메뉴를 선택해라'라는 메시지는 누가 응답해야할까? 바로 메뉴판 객체이다.

메뉴를 선택해라 --> 메뉴판 객체
메뉴판 객체는 이렇게 메시지를 통해 책임이 할당되었다.

이제 손님은 메뉴를 골랐다.

손님이 '커피를 주문해라'라는 메시지를 요청받아 책임을 수행하기 위해서 또 못하는 일이 있다. 그건 커피를 만드는 일이다.
다시 '커피를 만들어라'라는 메시지를 통해 바리스타 객체에게 요청해야한다.

커피를 만들어라 --> 바리스타 객체

바리스타는 커피를 만들라는 메시지를 통해 책임이 할당되었다.
바리스타는 이 요청에 응답하기 위해, 즉 책임을 수행하기 위해 또 스스로 하지 못하는건 무엇일까? 그건 커피를 생성하는 일이다.

아니 바리스타가 생성하면되는데 왜 그걸못해?????
SW세계에서 객체들은 자율적이다. 실세계에선 바리스타가 커피를 만들수 있지만 커피 스스로 자신을 만들진 못한다. SW세계에서는 그것이 가능하다.
SW세계에선 각 객체를 자율적인 객체로 보는 것이 뽀인트이다!!

그렇담, '커피를 생성하라'라는 메시지가 필요하다. 이 메시지를 요청받을 객체는 바로 '커피 객체'이다.

커피를 생성하라 --> 커피 객체

정리하면

커피를 주문하라 --> 손님 객체
메뉴를 선택하라 --> 메뉴판 객체
커피를 만들어라 --> 바리스타 객체
커피를 생성하라 --> 커피 객체

이런 메시지가 존재하고, 도메인 모델에 이 메시지를 넣고 각 메시지에 필요한 인자를 넣어서 도메인 모델을 다시금 정리하면

인터페이스 만들기

인터페이스는 객체가 수신할수 있는 메시지의 모음이다.
수신한 메시지를 토대로 객체들의 인터페이스를 만들어 외부와 소통이 가능하도록 해보자.
(메시지는 외부와 소통할수 있어야한다!@!@!@!@ - 당연)

  • Customer

    public class Customer {
      public Coffee orderCoffe(String menuName){
    
      }
    }
  • Menu

    public class Menu {
      public MenuItem chooseMenuItem(String menuItem){
    
      }
    }
  • Barista

    public class Barista {
      public Coffee makeCoffee(MenuItem menuItem){
    
      }
    }
  • Coffee

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

인터페이스는 외부에서 접근이 가능해야하므로 접근제어자를 public으로 한것에 유의하자.

구현하기

이제 인터페이스를 만들었으니, 실제 '어떻게'동작해야하는지에 대한 구현을 위해 상태와 행동인 필드와 메서드를 만들어보자.

Customer의 협력 부터 보면
Customer는 Menu에게 MenuItem을 받아 Barista에게 menuItem을 건네줘야한다.
그렇기 위해선 Customer는 Menu, Barista를 알아야한다. 즉, 두 객체의 참조를 받아야한다.
spring이였으면 spring container를 통해 DI를 할 것이다.

public class Customer {
    public Coffee orderCoffee(String menuName, Menu menu, Barista barista){

    }
}

참조를 받아오고, 이제 Menu와 Barista에게 요청을 구현해보자.

public class Customer {
    public Coffee orderCoffee(String menuName, Menu menu, Barista barista){
        MenuItem menuItem = menu.chooseMenuItem(menuName);
        Coffee coffee = barista.makeCoffee(menuItem);

        return coffee;
    }
}

Menu는 menuName에 해당되는 MenuItem을 찾아야하는 책임이 존재한다.
이 책임을 수행하기위해선 Menu가 MenuItem을 내부적으로 가지고있어야한다.(상태)

chooseMenuItem메서드는 내부 상태중 menuName과 같은 MenuItem을 반환하면 된다.

public class Menu {
    private List<MenuItem> menuItems;

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

    public MenuItem chooseMenuItem(String menuItem){
        for (MenuItem eachMenuItem : menuItems) {
            if(eachMenuItem.getName().equals(menuItem))
                return eachMenuItem;
        }
        return null;
    }
}

Barista는 menuItem을 통해서 커피를 만든다.

public class Barista {
    public Coffee makeCoffee(MenuItem menuItem){
        Coffee coffee = Coffee.createCoffee(menuItem);
        return coffee;
    }
}

Coffee는 static한 생성 메서드를 통해서 menuItem에 맞는 커피를 생산해서 리턴한다.

public class Coffee {
    private String name;
    private int price;

    public static Coffee createCoffee(MenuItem menuItem){
        Coffee coffee = new Coffee();
        coffee.name = menuItem.getName();
        coffee.price = menuItem.getPrice();

        return coffee;
    }
}

MenuItem은 응답하기 위한 getName과 getPrice getter 외부 인터페이스가 필요하다.

public class MenuItem {
    private String name;
    private int price;

    public String getName() {
        return name;
    }

    public int getPrice() {
        return price;
    }
}

이제서야 MenuItem에 외부인터페이스가 뭔지 알게되었다.

즉, 실제로 코드를 구헌해나가면서 외부인터페이스를 만들었다. 베스트는 설계단계에서 모든 외부인터페이스를 아는 것인데 그건 불가능하다. 빨리 설계를 끝내고 직접 구현하며 외부인터페이스를 만들어 나가는 것이 중요하다.

실제로 객체들끼리의 상호작용을 하지 않은채 설계단계에서 모든 외부인터페이스를 생각하는 것은 불가능하다. 빨리 코드 구현에 돌입하라!

실제로 객체지향적으로 설계를 한다음 구현까지 해보았다.!@!@ 설계단계에서 모든 인터페이스를 예측하는건 불가능하기에 어느정도 설계한다음 코드 구현으로 뛰어들자! 실제로 해보니 도메인모델과 실제 코드간의 차이가 적다. 비슷하다라는 것을 느꼈다. 그리고 메시지가 객체를 선택하게하여 책임을 할당하는 것도 직접 해보았다. 객체들간의 협력을 위해서 객체에게 책임을 할당하기, 그를 위해 메시지가 객체를 선택하게하기를 직접해보니 되게 자연스러운 흐름이라는 것을 느꼈다.

코드와 세가지 관점

개념 관점

개념관점에서 설계란 도메인 내에 존재하는 개념과 개념들간의 관계를 관계를 표현한다.
사용자가 바라보는 소프트웨어를 의미하고, 위에 구현한 코드는 도메인모델과 상당히 유사함을 알수 있다.
이는 변경을 관리하기 쉽고 유지보수성을 향상시킨다.

도메인 모델과 소프트웨어 클래스간의 차이가 적을수록 변경에 유연하다.

명세 관점

명세관점은 클래스의 인터페이스를 본다.
인터페이스는 외부와 소통하는 유일한 길이기에 수정되면 외부와의 상호작용도 모두 수정되야한다.
인터페이스를 수정하기 어렵다

그렇기에 인터페이스의 수정을 최대한 줄이기 위해선 인터페이스와 - 코드, 명세와 - 구현을 확 분리해서 변경이 필요한 부분은 구현에서 하도록 해야한다.

구현 관점

클래스의 내부 구현을 바라본다.
클래스의 메서드와 필드(속성)은 구현에 해당되며 공용 인터페이스가 아니다. 그러므로 외부 객체에 영향을 미쳐선 안된다.
완전히 캡슐화되어서 외부에서 알지 못하도록해야한다.
private으로 캡슐화 한것을 알수 있다.

이는 명세와 구현을 완벽하게 분리해서 변경에 유리하게 만드는 방법이다.

세가지 관점이 드러나게 코드를 개선하자. 이것이 변경에 유연한 코드를 만드는데 도와준다.

도메인 참조이유

  1. 메시지가 선택할 객체를 찾는 방법을 알려준다. 도메인 모델을 참고해서 메시지가 선택할 객체를 찾을수 있다.
  2. 도메인 모델과, 실제 구현된 코드간의 차이가 적으면 변경에 유리하다. 바로바로 직관적으로 개발자가 이해할수 있기에 변경도 쉽다.

명세 / 구현 분리하라.

구현 세부사항을 절대절대로 공용 인터페이스(명세)에 드러내게 하지마라.
공용 인터페이스는 외부 객체와 소통하는 녀석이기에 변경이 일어나면 외부 객체도 모두 영향을 받는다.
그렇기에 세부사항은 뒤로 숨기고 안정적인 부분만 공용인터페이스로 하자.
즉, 불안정한 부분은 구현으로 안정적인 부분은 공용인터페이스로 만들자.

728x90