🌀
f1v3-log
  • Welcome
  • 개발
    • SecurityContext를 새로 만들어야 할까?
    • OAuth2AuthorizationRequestResolver 커스터마이징
    • 동시성 문제를 해결해보자
    • MySQL은 어떻게 ID 값을 순차적으로 넣어주는 것일까? (Feat. Auto Increment Lock)
    • 외부 API 호출에 대한 고찰
      • HTTP Clients in Spring Boot
      • I/O와 트랜잭션 분리하기
      • 처리율 제한 장치 (Rate Limiter) 도입
      • 외부 API 의존성을 줄여보자
      • 캐시 레이어를 구성해보자 (Local Cache)
    • JPA Deep Dive
      • 결제 및 정산 시스템 기능 요구사항 분석
      • 글로벌 서비스를 고려할 때, 타임존 이슈를 어떻게 처리해야 할까?
      • Spring Data JPA - ID 생성 전략과 채번은 어떻게 되는걸까?
  • 회고
    • NHN Academy 인증과정 회고
    • DND 11기 회고
  • 독서
    • Effective Java 3/E
      • Item 1. 생성자 대신 정적 팩터리 메서드를 고려하라
      • Item 2. 생성자에 매개변수가 많다면 빌더를 고려하라
      • Item 3. private 생성자나 열거 타입으로 싱글턴임을 보증하라
    • 객체지향의 사실과 오해
      • 1장. 협력하는 객체들의 공동체
      • 2장. 이상한 나라의 객체
      • 3장. 타입과 추상화
      • 4장. 역할, 책임, 협력
      • 5장. 책임과 메시지
      • 6장. 객체 지도
      • 7장. 함께 모으기
  • Real MySQL 8.0
    • 04. 아키텍처
    • 05. 트랜잭션과 잠금
    • 08. 인덱스
    • 09. 옵티마이저와 힌트
  • 생각정리
    • 기술에 매몰되지 말자.
  • 공부
    • 객체지향 5원칙(SOLID)
      • SRP (Single Responsibility Principle)
      • OCP (Open Closed Principle)
Powered by GitBook
On this page
  • 커피 전문점 도메인
  • 커피 주문
  • 커피 전문점이라는 세상
  • 설계하고 구현하기
  • 커피를 주문하기 위한 협력 찾기
  • 인터페이스 정리하기
  • 구현하기
  • 코드와 세 가지 관점
  • 코드는 세 가지 관점을 모두 제공해야 한다.
  • 도메인 개념을 참조하는 이유
  • 인터페이스와 구현을 분리하라

Was this helpful?

  1. 독서
  2. 객체지향의 사실과 오해

7장. 함께 모으기

마틴 파울러는 객체지향 설계 안에 존재하는 세 가지 상호 연관된 관점에 관해 설명한다.

  1. 개념 관점

  2. 명세 관점

  3. 구현 관점

개념 관점(Conceptual Perspective)

개념 관점에서 설계는 도메인 안에 존재하는 개념과 개념들 사이의 관계를 표현한다.

도메인은 사용자들이 관심을 가지고 있는 특정 분야, 주제를 말하며 소프트웨어는 도메인에 존재하는 문제를 해결하기 위해 개발된다.

개념 관점은 사용자가 도메인을 바라보는 관점을 반영하며 실제 도메인의 규칙과 제약을 최대한 유사하게 반영하는게 핵심

명세 관점(Specification Perspective)

명세 관점은 사용자의 영역인 도메인을 벗어나 개발자의 영역인 소프트웨어, 객체들의 책임에 초점이 옮겨진다. 즉, 객체의 인터페이스를 바라보게 되며 개발자는 객체가 협력을 위해 '무엇'을 할 수 있는가에 초점을 맞춘다.

인터페이스와 구현을 분리하는 것은 훌륭한 객체지향 설계를 낳는 가장 기본적인 원칙이라는 점을 기억하자.

구현 관점(Implementation Perspective)

구현 관점은 프로그래머인 우리에게 가장 익숙한 관점으로, 객체들이 책임을 수행하는 데 필요한 동작을 코드로 작성하는 것이다. 개발자는 객체의 책임을 '어떻게' 수행할 것인가에 초점을 맞추며 인터페이스를 구현하는 데 필요한 속성과 메서드를 클래스에 추가한다.

위의 세 가지 관점의 순서대로 소프트웨어를 개발한다는 의미가 아닌, 동일한 클래스가 세 가지 다른 방향을 바라보게 해야한다는 것이다!


커피 전문점 도메인

커피 전문점과 관련된 간단한 예제로 도메인 모델에서 시작해 최종 코드까지의 구현 과정과 구현 클래스를 개념, 명세, 구현 관점에서 바라본다는 것이 무엇을 의미하는지 파악해보자.

커피 주문

예제의 목적은 커피 전문점에서 커피를 주문하는 과정을 객체들의 협력 관계로 구현하는 것이다.

[커피 제조하기]

        Menu
아메리카노            1,500원
카푸치노            2,000원
카라멜 마키아또        2,500원
에스프레소            2,500원

1. 손님이 테이블에 앉아 메뉴판을 훑어본 후 커피를 주문한다.
2. 주문받은 커피를 제조하는 것은 이제 바리스타의 몫

커피 전문점이라는 세상

객체지향 패러다임의 가장 중요한 도구인 '객체'를 기준으로 커피 전문점을 객체들로 구성된 작은 세상으로 바라보자.

어떤 객체가 존재할까?

  • 커피 전문점 안에는 메뉴판이 존재한다.

    • 아메리카노, 카푸치노, 카라멜 마키아또, 에스프레소

    • 메뉴판은 네 개의 메뉴 항목 객체를 포함하는 객체

  • 손님은 메뉴판을 보고 바리스타에게 원하는 커피를 주문한다.

    • 손님 또한 하나의 객체로, 메뉴판 개체 안에 적힌 메뉴 항목 객체들 중 원하는 메뉴를 선택하여 바리스타 객체에게 전달

  • 바리스타는 주문을 받은 메뉴에 따라 적절한 커피를 제조한다.

    • 바리스타는 자율적으로 커피를 제조하는 객체로 볼 수 있다.

객체들 간의 관계

  • 손님은 메뉴판에서 주문할 커피를 선택할 수 있어야 한다.

  • 손님은 어떤 식으로든 메뉴판을 알아야 하며, 두 객체 사이에 관계가 존재한다는 것을 암시한다.

  • 손님은 바리스타에게 주문을 해야 하므로 손님과 바리스타 사이에도 관계가 존재한다.

  • 바리스타는 커피를 제조하는 사람이므로 당연하게 자신이 만든 커피와 관계를 맺는다.

메뉴판 타입에서 메뉴 항목 타입 쪽으로 향하는 선에 그려진 속이 찬 마름모는 포함(containment) 관계 또는 합성(composite) 관계를 나타내는 것으로, 메뉴 항목이 메뉴판에 포함된다는 사실을 표현한다. (아메리카노, 카푸치노, 카라멜 마키아또, 에스프레소)

손님 타입은 메뉴판 타입을 알고 있어야 한다. 이처럼 한 타입의 인스턴스가 다른 타입의 인스턴스를 포함하지 않지만 서로 알고 있어야 할 경우 이를 연관(association) 관계라고 한다.

도메인 모델을 작성할 때 어떤 타입이 도메인을 구성하느냐와 타입들 사이에 어떤 관계가 존재하는지를 파악함으로써 도메인을 이해하는 것에 초점을 맞추는 것이 중요하다.

설계하고 구현하기

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

객체지향 설계의 첫 번째 목표는 훌륭한 객체를 설계하는 것이 아니라 훌륭한 협력을 설계하는 것이라는 점을 잊지 말자. 훌륭한 객체는 훌륭한 협력을 설계할 때만 얻을 수 있다.

협력을 설계할 때는 객체가 메시지를 선택하는 것이 아닌, 메시지가 객체를 선택하게 해야 한다.

  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) {}
}

구현하기

클래스의 인터페이스를 식별했으므로 이제 오퍼레이션을 수행하는 방법을 메서드로 구현하자.

1. 손님(Customer)

  1. Customer는 Menu에게 menuName 에 해당하는 MenuItem을 찾아달라고 요청해야 한다.

  2. MenuItem을 받아 이를 Barista에게 전달해서 원하는 커피를 제조하도록 요청해야 한다.

여기서 문제는 Customer가 어떻게 Menu 객체와 Barista 객체에 접근할 것이냐다. 객체가 다른 객체에게 메시지를 전송하기 위해서는 먼저 객체에 대한 참조를 얻어야 한다.

객체를 참조하는 방법은 여러가지가 있지만 여기서는 Customer의 order() 메서드의 인자로 객체를 전달하는 방법을 사용하자. 이 결정은 결과적으로 Customer의 인터페이스를 변경하게 된다.

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

이렇듯 구현 도중 객체의 인터페이스가 변경될 수 있다는 점을 생각하자. 중요한 것은 설계가 아닌 코드다. 협력을 구상하는 단계에 너무 오랜 시간을 쏟지 말고 최대한 빨리 코드를 구현해서 설계에 이상이 없는지, 설계가 구현 가능한지 판단해야 한다. 코드를 통한 피드백 없이는 깔끔한 설계를 얻을 수 없다.

2. 메뉴(Menu)

Menu는 menuName 에 해당하는 MenuItem을 찾아야 하는 책임을 가지고 있다. 이 책임을 수행하기 위해서 내부적으로 MenuItem을 관리하고 있어야 하기에 간단하게 Menu가 MenuItem의 목록을 포함하도록 하자.

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

객체가 어떤 책임을 수행하는지 결정한 후에 책임을 수행하는 데 필요한 객체의 속성을 결정하자. 위에서 MenuItem을 설정한 것과 같은 방식으로 진행하면 된다. 객체의 구현 세부 사항을 객체의 공용 인터페이스에 노출시키지 않고 인터페이스와 구현을 깔끔하게 분리할 수 있는 기본적인 방법이다.

3. 바리스타(Barista)

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

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

4. 커피(Coffee)

Coffee는 자기 자신을 생성하기 위한 생성자를 제공한다. Coffee는 커피 이름과 가격을 속성으로 가지고 생성자 안에 MenuItem에 요청을 보내 커피 이름과 가격을 얻은 후 Coffee를 속성에 저장한다.

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

5. 메뉴 항목(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 클래스가 보인다. 이 클래스들을 통해 커피 전문점 도메인을 구성하는 중요한 개념과 관계를 반영한다는 사실을 쉽게 알 수 있다.

소프트웨어 클래스가 도메인 개념의 특성을 최대한 수용하면 변경을 관리하기 쉽고 유지보수성을 향상시킬 수 있다.

2. 명세 관점

명세 관점은 클래스의 인터페이스를 바라본다. 클래스의 public 메서드는 다른 클래스가 협력할 수 있는 공용 인터페이스를 드러낸다. 공용 인터페이스는 외부의 객체가 해당 객체에 접근할 수 있는 유일한 부분이다. 이러한 인터페이스를 수정하면 해당 객체와 협력하는 모든 객체에게 영향을 미칠 수밖에 없다.

객체의 인터페이스는 수정하기 어렵다. 최대한 변화에 안정적인 인터페이스를 만들기 위해 구현과 관련된 세부 사항이 인터페이스에 드러나지 않게 하자.

3. 구현 관점

구현 관점은 클래스의 내부 구현을 바라본다. 클래스의 메서드와 속성은 구현에 속하며 공용 인터페이스의 일부가 아니다. 따라서 메서드의 구현과 속성의 변경은 원칙적으로 외부 객체에 영향을 미쳐서는 안된다. 즉, 메서드와 속성이 철저하게 클래스 내부로 캡슐화 되어야 한다는 것을 의미한다.

훌륭한 객체지향 프로그래머는 하나의 클래스 안에 세 가지 관점을 모두 포함하면서도 각 관점에 대응되는 요소를 명확하고 깔끔하게 드러낸다. 우리가 작성한 코드에서 그렇지 못한다면 빠르게 코드를 개선하자. 그것이 변경에 유연하게 대응할 수 있는 객체지향 코드를 작성하는 빠른 길이니까.

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

어떤 메시지가 있을 때 그 메시지를 수신하는 객체는 도메인 개념 중에서 가장 적절한 것을 선택하는 방법이 존재한다.

  • 도메인 개념 안에서 적절한 객체를 찾게 된다면, 도메인에 대한 지식을 기반으로 코드의 구조와 의미를 쉽게 유추할 수 있다.

  • 이것은 시스템의 유지보수성에 커다란 영향을 미친다.

소프트웨어(software)는 항상 변한다. 설계는 변경을 위해 존재한다.

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

명세 관점과 구현 관점이 뒤섞여 머릿속을 함부로 어지럽히지 못하게 하라.

  • 명세 관점은 클래스의 안정적인 측면을 드러내야 한다.

  • 구현 관점은 클래스의 불안정한 측면을 드러내야 한다.

  • 인터페이스가 구현 세부 사항을 노출하기 시작하면 아주 작은 변동에도 전체 협력이 요동치는 취약한 설계가 된다.

중요한 것은 클래스를 봤을 때 클래스를 명세 관점과 구현 관점으로 나눠볼 수 있어야 한다는 것이다. 캡슐화를 위반해서 구현을 인터페이스 밖으로 노출해서도 안 되고, 인터페이스와 구현을 명확하게 분리하지 않고 흐릿하게 섞어놓아서도 안 된다.

Previous6장. 객체 지도NextReal MySQL 8.0

Last updated 7 days ago

Was this helpful?

도메인 모델