3. 데코레이터 패턴 (Decorator Pattern)

Kim Dong Kyun·2023년 6월 24일
1

Design Pattern

목록 보기
3/5
post-thumbnail

썸네일 이미지 출처

  • 야돈은 야돈으로써의 기능과, 야도란으로 진화하면서 얻는 기능을 모두 수행 가능하다.

  • 즉, 야돈은 야돈이되 "확장된" 야돈으로 기능 가능하다! (?)

데코레이터 패턴의 정의

데코레이터 패턴(Decorator Pattern)은

객체에 추가 요소를 동적으로 더할 수 있다.

데코레이터를 사용하면, 서브 클래스를 만들 때보다 훨씬 유연하게 기능을 확장 할 수 있다.

  • OCP 만족한다! (확장에는 열려있고, 변경에는 닫혀있는!)

"헤드퍼스트 디자인 패턴" 을 참고했으며, 깃허브에 모든 코드를 올려놓았습니다.


사건의 발단 - 휘핑크림 두번 추가해주세요~

스타버즈 코리아는 떠오르는 혜성처럼 등장한 커피 가게이다. 해당 회사는 고객이 원하는 토핑(휘핑, 모카 등)을 금액을 지불하면 맘껏 추가할 수 있도록 하는 컨셉으로 큰 인기를 얻었으며, 규모가 커진 이제는 "주문 시스템"을 만들려고 한다.

스타버즈는 이에 따라 슈퍼 개발자인 당신에게 "주문 시스템"의 개선을 거액에 의뢰하게 되는데...

  • 사업을 시작 할 때의 클래스 다이어그램은 다음과 같았다.

  • "음료"의 추상 클래스인 Beverage 를 활용하는 형식이다.

  • 추상 매서드인 cost() 를 활용해서 각각의 가격을 리턴하고,

  • getDescription 매서드를 오버라이드 해서 각자의 설명을 해주고 있는 식이었다.

  • 위와 같은 형식.

그런데, 사업을 전개하면서 문제가 발생했다. 당신도 이미 스타버즈 커피를 많이 마셔봤겠지만, 음료에 다양한 커스텀이 가능하게 된 것이다!

  • 그렇다면 이제 Beverage 클래스의 구현체로 "에스프레스 샷 두번추가" 클래스, "에스프레소 샷 세번 추가하고 모카 두번추가하고 휘핑 두번 추가할까말까" 클래스
    ...

  • 엄청난 클래스들이 생겨야 한다!! 이걸 어떻게 해결해야 할까?


추상을 분리하자! 그런데 이게 맞나?

슈퍼 개발자인 당신은 일단 책임의 분리를 명확히 하고자 했다.

한 눈에 봐도, Espresso 등 "커피"의 시작과, 휘핑 등 "추가하는 것"은 다르다.

  • 그래서, 당신은 제일 먼저 책임을 분리했다. 그리고 그 책임은 "추상"으로 분리하기로 결심했다.

1. Beverage ( 음료 )

public abstract class Beverage {
    String description = "음료류!";

    public String getDescription(){
        return this.description; // getter 매서드는 구현체들도 공유하므로, 선언한다.
    }

    public abstract double cost(); // Beverage의 구현체들은 가격 매서드를 구현해야 한다.
}

2. Decorator ( 첨가물 )

public abstract class Decorator {
    public abstract String getDescription(); 
    // 모든 첨가물에도 Description 을 새로 구현하도록 만들것이다.
}
  • 오! 이러면 이제 Decorator 를 상속하는 구현체들에게 가격을 +하는 매서드를 통해서 관리가 가능하지 않을까?

예를 들어 Decorator 구현체의 생성자에서

public Mocha(Beverage beverage){
        double cost = beverage.cost();
        cost += 0.22;
        String description = beverage.getDescription();
        description += ", 모카";
    }
  • 이런 식으로 가공하면 되지 않을까?

잠시 쉬운 방법의 유혹에 흔들렸지만, 당신은 이내 정신을 차리고 이 방법의 단점을 알아냈다.
(글을 쓰는 나는 왜?를 생각해내기까지 30분이나 걸렸다...)


위 방법의 문제

일단, Beverage 가 더이상 Beverage가 아니게 된다. 그리고 이것은 객체를 운영 할 때 매우 문제가 된다.

  • 강한 결합! 위 코드는 Beverage 객체가 완전히 Decorator 에 의존하게 된다.
  • 컴포넌트는 없어지고, 데코레이터만 남는 것. 따라서 컴포넌트에 어떤 추가적인 확장이 매우 힘들어진다
  • 간단하게, Beverage 라는 컴포넌트에 추상 매서드로 음료의 사이즈를 가지도록 강제한다고 생각해보자.
  • 컴포넌트를 꾸미는 모든 데코레이터는 생성자 매서드에 beverage.size()를 가져오는 코드를 추가해야 할 것이다.
  • 즉, 유연성이 아예 없어진다.

그렇다면 어떻게 하나요?


진짜, 데코레이터 패턴!

당신은 번뜩이는 아이디어를 가지고, Decorater 추상을 다음과 같이 바꾸게 된다.

  • 다음은 클래스 다이어그램이다

이제 실제 코드를 알아보자

1. Beverage (Component)

public abstract class Beverage {
    String description = "음료류!";

    public String getDescription(){
        return this.description; // getter 매서드는 구현체들도 공유하므로, 선언한다.
    }

    public abstract double cost(); // Beverage의 구현체들은 가격 매서드를 구현해야 한다.
}

2. Decorator

public abstract class Decorator extends Beverage {
    Beverage beverage; // 데코레이터가 감쌀 음료 객체를 정한다
    public abstract String getDescription(); 
    // 모든 첨가물에도 Description 을 새로 구현하도록 만들것이다.
}
  • Beverage 를 인스턴스 변수로 선언해서, 감쌀 음료 객체를 정한다
  • getDescription() 매서드를 통해서 Description을 업데이트 해줄 것이다.

3. Decorator 의 구현체

public class Mocha extends Decorator{ 
// Decorator 는 Beverage(Component) 를 확장한다.
    public Mocha(Beverage previousBeverage){
        this.beverage = previousBeverage;
// 생성자 매서드를 통해서 매개변수로 들어온 previousBeverage 가 필드의 인스턴스 변수가 된다
        // Decorator 클래스의 인스턴스 변수로 선언한 beverage 가 된다.
    }
    @Override
    public double cost() {
        return beverage.cost() + 0.2;
    }

    @Override
    public String getDescription() {
        return beverage.getDescription() + ", 모카";
        // getDescription 매서드를 통해 이전에 있던 음료
        // (혹은 데코레이터) 설명 호출 후 덧붙이기
    }
}
  • 위와 같이, Component -> Decorator -> 구현체 순서로 확장된다

  • Mocha 라는 데코레이터의 생성자 매서드는 매개변수로 previousBeverage 를 가지는데, 이렇게 들어온 녀석은 "모카"필드의 인스턴스 변수인

Beverage beverage;
  • 가 된다.

  • 그리고 이 녀석을 가공도 할 수 있다.

  • component인 Beverage 만이 가지던 cost() 매서드를 오버라이드 해서, 생성 할 때 들어온 객체에 가공이 가능하다!

그러면, 어떻게 가공이 되는지 한번 보자!


테스트!

  • Beverage 추상은 계속 유지된다.

  • 객체가 계속 가공되어도 Component 에서 벗어나지 않는다.

그래서 뭐!!? 정리해서 말해봐!


데코레이터 패턴의 특징

  1. 데코레이터의 슈퍼클래스는 자신이 장식하고 있는 객체의 슈퍼클래스이다.

  2. 한 객체를 여러 개의 데코레이터로 감쌀 수 있다.

  3. 데코레이터는 자신이 감싸고 있는 객체와 같은 슈퍼클래스를 가지고 있기 때문에, 원래 객체(싸여있는 객체)가 들어갈 자리에 데코레이터 객체를 넣어도 상관이 없다
    (휘핑에 휘핑에 휘핑에 휘핑)...

  4. 데코레이터는 자신이 장식하고 있는 객체에게 어떤 행동을 위임하는 일 말고도 추가 작업을 수행 할 수 있다.
    ex) Java.io -> BufferedReader / InputStream ... 추가 작업의 마뜨료시카 녀석들


데코레이터 패턴의 장점

  1. 유연한 확장성
  • 데코레이터 패턴은 기존 객체의 동작을 수정하지 않고도 새로운 기능을 추가하거나 변경할 수 있다. 다양한 데코레이터를 조합하여 동적으로 객체를 확장할 수 있으므로, 객체의 기능을 유연하게 조정하고 확장 가능하다.
  1. OCP
    : 각 데코레이터를 통해서 Component 를 유연하게 확장 가능하다.

  2. 다형성 활용
    : 데코레이터는 기존의 Component 객체를 상속받으므로, 데코레이터를 사용하는 클라이언트는 원본 객체 또는 데코레이터 객체를 모두 사용할 수 있다.

데코레이터 패턴의 단점!

  1. 잡다한 클래스가 너무 많다
  • 데코레이터가 많아질수록 잡다한 클래스가 많아진다. 그렇게 되면 클래스의 구성 요소를 파악하는 시간이 그만큼 소요된다 (BufferedReader(new InputSystem(...)) 백준이 처음에 어려웠던 이유임 개인적으로
  1. 관리 할 객체가 늘어나므로 실수 할 가능성이 높아진다.
  • 따라서, 다음 시간에 배워볼 "팩토리" 나 "빌더"같은 패턴으로 데코레이터를 만들고 사용한다.

0개의 댓글