본문 바로가기
IT Books Review/객체지향의 사실과 오해

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

by happy coding! 2023. 4. 4.
반응형

마틴 파울러는 객체지향 설계 안에 존재하는 세 가지 상호 연관된 관점에 관해 설명한다.
파울러는 세 가지 관점을 각각 개념 관점, 명세 관점, 구현 관점이라고 부른다.

  • 개념 관점(Conceptual Perspective)에서 설계는 도메인 안에 존재하는 개념과 개념들 사이의 관계를 표현한다.
    • 도메인: 사용자들이 관심을 가지고 있는 특정 분야나 주제를 말함
    • 소프트웨어는 도메인에 존재하는 문제를 해결하기 위해 개발된다.
  • 명세 관점(Specification Perspective)은 도메인의 개념이 아니라 실제로 소프트웨어 안에서 살아 숨쉬는 객체들의 책임에 초점을 맞추게 된다. 즉 객체의 인터페이스를 바라보게 된다.
    • 명세 관점에서 프로그래머는 객체가 협력을 위해 '무엇'을 할 수 있는가에 초점을 맞춤
    • 인터페이스와 구현을 분리하는 것은 훌륭한 객체지향 설계를 낳는 가장 기본적인 원칙
  • 구현 관점(Implememtation Perspective)은 실제 작업을 수행하는 코드와 연관돼 있다.
    • 구현 관점의 초점은 객체들이 책임을 수행하는데 필요한 동작하는 코드를 작성하는 것
    • 프로그래머는 객체의 책임을 어떻게 수행할 것인가에 초점을 맞춤
    • 인터페이스를 구현하는데 필요한 속성메서드를 클래스에 추가

 

개념 관점, 명세 관점, 구현 관점동일한 클래스를 세가지 다른 방향에서 바라보는 것을 의미한다.

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

  • 클래스가 은유하는 개념은 도메인 관점을 반영한다.
  • 클래스의 공용 인터페이스는 명세 관점을 반영한다.
  • 클래스의 속성과 메서드는 구현 관점을 반영한다.

이것은 클래스를 어떻게 설계해야 하느냐에 대한 중요한 힌트를 암시한다.
클래스는 세 가지 관점을 모두 수용할 수 있도록 개념, 인터페이스. 구현을 함께 드러내야 한다.


커피 전문점 도메인

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

커피 전문점이라는 세상

개발에 들어가기 전에 먼저 커피 전문점을 구성하는 요소들에 관해 잠시나마 고민해보자.
객체지향 패러다임의 가장 중요한 도구는 '객체'이므로 커피 전문점을 객체들로 구성된 작은 세상으로 바라보자.

  1. 커피 전문점 안에는 메뉴판이 존재한다. 객체지향의 관점에서 메뉴판은 하나의 객체다.
  2. 메뉴판은 네 개의 메뉴 항목으로 구성돼 있는데 메뉴 항목들 역시 객체로 볼 수 있다. 따라서 메뉴판은 네 개의 메뉴 항목 객체들을 포함하는 객체라고 볼 수 있다.
  3. 손님은 메뉴판을 보고 바리스타에게 원하는 커피를 주문한다. 객체의 관점에서 보면 손님 역시 하나의 객체다.
  4. 바리스타는 주문을 받은 메뉴에 따라 적절한 커피를 제조한다.
  5. 바리스타가 제조하는 커피 역시 메뉴판, 메뉴 항목, 바리스타와 구별되는 자신만의 경계를 가지므로 객체로 볼 수 있다.
  • 객체지향의 관점에서 '커피 전문점'이라는 도메인은 손님 객체, 메뉴 항목 객체, 메뉴판 객체, 바리스타 객체, 커피 객체로 구성된 작은 세상이다.

 

동적인 객체를 정적인 타입으로 추상화해서 복잡성을 낮추자.

동일한 상태와 동일한 행동을 가질 수 있는 객체는 같은 타입의 인스턴스로 분류할 수 있다.

 

  • 손님 객체는 '손님 타입'의 인스턴스로 볼 수 있다.
  • 바리스타 객체는 '바리스타 타입'의 인스턴스로 볼 수 있다.
  • 아메리카노 커피, 카페라떼 커피, 카라멜 마끼아또 커피 모두 '커피 타입'의 인스턴스로 볼 수 있다.

커피 전문점을 구성하는 범주로서 '손님 타입', '메뉴판 타입', '메뉴 항목 타입', '바리스타 타입', '커피 타입'이 갖춰졌다.

이제 타입 간에 어떤 관계가 존재하는지 살펴보자.

  1. 하나의 메뉴판 객체는 다수의 메뉴 항목으로 구성돼 있다. 메뉴 항목 객체가 메뉴판 객체에 포함돼 있다고 할 수 있는데 이를 메뉴판 타입과 메뉴 항목 타입 간의 합성 관계로 단순화하면 좀 더 수월할 것이다. 메뉴판 타입에서 메뉴 항목 타입 쪽으로 향하는 선에 그려진 속이 찬 마름포는 '포함 관계' 또는 '합성 관계'를 나타낸다. 즉, 메뉴 항목이 메뉴판에 포함된다는 사실을 표현한다. 메뉴 항목 좌측 아래의 3이라는 숫자는 메뉴판에 포함되는 메뉴 항목이 3개라는 것을 의미한다.
  2. 손님 타입은 메뉴판 타입을 알고 있어야 원하는 커피를 선택할 수 있다. 메뉴판 타입은 손님의 일부가 아니므로 이 관계는 합성 관계가 아니다. 이 경우 타입들 사이를 단순한 선으로 연결한다. 이처럼 한 타입의 인스턴스가 다른 타입의 인스턴스를 포함하지는 않지만 서로 알고 있어야할 경우 이를 연관(association) 관계라고 한다.
  3. 바리스타 타입은 커피를 제조해야 하므로 커피 타입을 알고 있어야 한다.

위 그림은 커피 전문점 도메인을 구성하는 타입들의 종류와 관계를 표현한 것이다.

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


설계하고 구현하기

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

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

  • 현재 설계하고 있는 협력은 커피를 주문하는 것이다.
  • 아마도 첫번째 메시지는 '커피를 주문하라'일 것이다.
  • 메시지 위에 붙은 화살표는 메시지에 담아 전달될 부가적인 정보인 인자를 의미한다. 이 경우 '아메리카노를 주문하라' 메시지는 나중에 '커피를 주문하라(아메리카노)'와 같이 인자를 포함하는 형식으로 구현될 것이다.
  • 화살표 아래에 붙은 작은 화살표는 이 메시지를 수신한 객체가 무엇을 응답해야 하는지를 나타낸다.

현실 속의 메뉴판은 자기 스스로 메뉴 항목을 찾지 않을 것이다. 현실 속에서 메뉴판은 손님에 의해 펼쳐지거나 닫혀지는 수동적인 존재다. 그러나 객체지향의 세계로 들어오면 수동적인 메뉴판이라는 개념은 더 이상 유효하지 않다. 객체지향 세계에서는 모든 객체가 능동적이고 자율적인 존재다. 소프트웨어 세상 속의 메뉴판은 현실 속의 메뉴판으로부터 모티브를 따왔지만 현실 속의 메뉴판보다 더 많은 일을 할 수 있다. 소프트웨어 객체는 현실 속의 객체를 모방하거나 추상화한 것이 아니다. 단지 의미를 쉽게 유추할 수 있도록 '은유'할 뿐이다.

협력에 필요한 객체의 종류와 책임, 주고받아야 하는 메시지에 대한 대략적인 윤곽이 잡혔다.

 

2. 인터페이스 정리하기

  • 우리가 힘들여 얻어낸 것은 객체들의 인터페이스다.
    객체가 수신한 메시지가 객체의 인터페이스를 결정한다.
  • 각 객체를 협력이라는 문맥에서 떼어내어 수신 가능한 메시지만 추려내면 객체의 인터페이스가 된다.
    객체가 어떤 메시지를 수신할 수 있다는 것은 그 객체의 인터페이스 안에 메시지에 해당하는 오퍼레이션이 존재한다는 것을 의미한다.
  • 객체의 타입을 구현하는 일반적인 방법은 클래스를 이용하는 것이다.
    협력을 통해 식별된 타입의 오퍼레이션은 외부에서 접근 가능한 공용 인터페이스의 일부이다.
    따라서 인터페이스에 포함된 오퍼레이션 역시 외부에서 접근 가능하도록 공용(public)으로 선언되어 있어야 한다.
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) {}
}

3. 구현하기

  • 클래스의 인터페이스를 식별했으므로 이제 오퍼레이션을 수행하는 방법을 메서드로 구현하자.
  • Customer는 Menu에게 menuName에 해당하는 MenuItem을 찾아달라고 요청해야 한다. 그리고 MenuItem을 받아 Barista에게 전달해서 원하는 커피를 제조하도록 요청해야 한다.
  • 문제는 Customer가 어떻게 Menu 객체와 Barista 객체에 접근할 것이냐이다.
  • 객체가 다른 객체에게 메시지를 전송하기 위해서는 먼저 객체에 대한 참조를 얻어야 한다.
  • 객체 참조를 얻는 다양한 방법이 있지만 여기서는 Customer의 order() 메서드의 인자로 Menu와 Barista 객체를 전달한다.

Customer

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

Menu

Menu는 menuName에 해당하는 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;
    }
}

Barista

Barista는 MenuItem을 이용해서 커피를 제조한다.

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

Coffee

Coffee는 자기 자신을 생성하기 위한 생성자를 제공한다.

class Coffee {
    private String name;
    private int price;

    public Coffee(MenuItem menuItem) {
        this.name = menuItem.getName();
        this.pricae = menuItem.cost();
    }
}

MenuItem

MenuItem은 getName()과 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;
    }
}

아래 그림은 커피 전문점 코드를 클래스 다이어그램으로 나타낸 것이다.

코드와 세 가지 관점

코드는 세 자기 관점을 모두 제공해야 한다.

  1. 개념 관점
    • 개념 관점에서 코드를 바라보면 Customer, Menu, MenuItem, Barista, Coffee 클래스가 있다.
    • 이 클래스들을 살펴보면 커피 전문점 도메인을 구성하는 중요한 개념과 관계를 반영한다는 사실을 알 수 있다.
    • 소프트웨어 클래스가 도메인 개념의 특성을 최대한 수용하면 변경을 관리하기 쉽고 유지보수성을 향상시킬 수 있다.
    • 예를 들어 커피를 제조하는 과정을 변경해야 한다면 Barista 클래스가 커피를 제조할 것이라고 쉽게 유추할 수 있다.
  2. 명세 관점
    • 명세 관점은 클래스의 인터페이스를 바라본다.
    • 클래스의 public 메서드는 다른 클래스가 협력할 수 있는 공용 인터페이스를 드러낸다.
    • 공용 인터페이스는 외부의 객체가 해당 객체에 접근할 수 있는 유일한 부분이다.
    • 최대한 변화에 안정적인 인터페이스를 만들기 위해서는 인터페이스를 통해 구현과 관련된 세부 사항이 드러나지 않게 해야 한다.
  3. 구현 관점
    • 구현 관점은 클래스의 내부 구현을 바라본다.
    • 클래스의 메서드와 속성은 구현에 속하며 공용 인터페이스의 일부가 아니다.
    • 메서드의 구현과 속성의 변경은 원칙적으로 외부의 객체에게 영향을 미쳐서는 안된다.
    • 이것은 메서드와 속성이 철저하게 클래스 내부로 캡슐화돼야 한다는 것을 의미한다.

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

어떤 메시지가 있을 때 그 메시지를 수신할 객체를 어떻게 선택하는가?

  • 첫번째 전략은 도메인 개념 중에서 가장 적절한 것을 선택하는 것
  • 도메인에 대한 지식을 기반으로 코드의 구조와 의미를 쉽게 유츄할 수 있다.
  • 이것은 시스템의 유지보수성에 커다란 영향을 미친다.

 

소프트웨어는 항상 변한다.

 

  • 여러 개의 클래스로 기능을 분할하고 클래스 안에서 인터페이스와 구현을 분리하는 이유는 변경이 발생했을 때 코드를 좀 더 수월하게 수정하길 원하기 때문
  • 소프트웨어 클래스가 도메인 개념을 따르면 변화에 쉽게 대응할 수 있다.

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

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

  • 인터페이스가 구현 세부 사항을 노출하기 시작하면 아주 작은 변동에도 전체 협력이 요동치는 취약한 설계를 얻을 수 밖에 없다.
  • 실제로 훌륭한 설계를 결정하는 측면은 명세 관점은 객체의 인터페이스다.
  • 명세 관점이 설계를 주도하게 하면 설계의 품질이 향상될 수 있다.
  • 중요한 것은 클래스를 봤을 때 클래스를 명세 관점과 구현 관점으로 나눠볼 수 있어야한다는 것이다.
  • 캡슐화를 위반해서 구현을 인터페이스 밖으로 노출해서도 안되고, 인터페이스와 구현을 명확하게 분리하지 않고 흐릿하게 섞어놓아서도 안된다.

참고 자료 및 그림 출처

https://mindock.github.io/book/the-essence-of-object-orientation-7/

반응형

댓글