데코레이터 패턴

이주오·2022년 3월 11일
0

디자인 패턴

목록 보기
6/12
post-custom-banner

헤드 퍼스트 디자인 패턴을 읽고 정리한 글입니다.

데코레이터 패턴을 이용하면 객체에 추가 요소를 동적으로 더할 수 있습니다. 데코레이터를 사용하면 서브 클래스를 만드는 경우에 비해 훨씬 유연하게 기능을 확장할 수 있습니다.


개요

OO커피는 단기간에 급속도로 성장한 대형 커피 전문점이다. 빠르게 성장한 만큼, 음료들을 모두 포괄하는 주문 시스템이 이제서야 개발되려고 하는 상황이다. 처음 시스템 시작할 무렵에 만들어진 클래스는 위의 사진과 같다.

Beverage : 음료를 나타내는 추상 클래스, 모든 음료는 해당 클래스의 서브클래스

  • description : 해당 인스턴스 변수는 서브클래스에서 설정되며, 음료의 설명이 저장된다.
  • cost() : 추상 메서드이며, 서브 클래스에서 해당 메서드를 구현해야 한다.

하지만 음료에는 스팀 우유, 두유, 모카, 휘핑 크림 등 옵션을 추가할 때마다 가격이 달라진다. 이러한 경우를 모두 고려한다면??

이처럼 클래스 개수가 폭발적으로 증가하게 된다. 만약 우유나 크림 가격이 인상된다면?? 한눈에 보기에도 이렇게 수많은 클래스를 관리하기는 힘들 것이다.


그러면 슈퍼 타입인 Beverage 클래스에 인스턴스 변수로 관리하면 안될까??

  • milk, soy, mocha, ... ,whip : 각 추가 요소에 해당하는 인스턴스 변수 추가
  • cost() : 각 음료 서브 클래스의 인스턴스마다 추가 사항에 해당하는 추가 가격까지 포함할 수 있도록 기본 음료 값을 가져와서 오버라이드 하기위해 추상 메소드가 아닌 구현 메소드로 수정
  • 부울 인스턴스 변수를 위한 게터, 세터
public class Beverage {

    String description;
    boolean hasMilk, hasSoy, hasMocha;
    double milkCost, soyCost, mochaCost;

    public double cost() {
        double condimentCost = 0;
        if (getHasMilk()) {
            condimentCost += milkCost;
        }
        if (getHasSoy()) {
            condimentCost += soyCost;
        }
        if (getHasMocha()) {
            condimentCost += mochaCost;
        }
        return condimentCost;
    }

    // get, set..
    public boolean getHasMilk() {
        return hasMilk;
    }

    public boolean getHasSoy() {
        return hasSoy;
    }

    public boolean getHasMocha() {
        return hasMocha;
    }

}
public class DarkRoast extends Beverage{

    public DarkRoast() {
        description = "다크 로스트";
    }

    @Override
    public double cost() {
        return super.cost() + 3500;
    }
}

아까와 같은 클래스 폭발을 막게되었다. 하지만 아직 확신이 서지 않는다. 어떤 문제점이 있을 수 있을까??

  • 음료에 추가되는 옵션(우유, 휘핑, 모카 등)의 가격이 바뀔때마다 코드를 수정해야 한다.
  • 음료에 추가되는 옵션의 종류가 많아지면 그 때마다 메소드를 추가하고, 슈퍼 클래스의 cost() 메소드를 수정해야 한다.
  • 새로운 음료가 추가되는 경우, 옵션이 없는 경우와 우유가 들어가지 않는 음료임에도 관련된 멤버들을 상속받게 된다.
  • 만약 샷이라는 옵션이 추가되고 샷을 두번 추가한 음료는 어떻게 해야할까??

상속은 객체지향 디자인의 강력한 요소 중 하나지만, 이처럼 상속을 사용한다고 해서 무조건 유연하고 관리하기 쉬운 디자인이 만들어지지 않는다. 그 이유는 서브 클래스를 만드는 방식으로 행동을 상속 받으면 해당 행동은 컴파일시에 완전히 결정되고 모든 서브클래스에서 슈퍼 클래스의 멤버들을 상속 받아야 하기 때문이다. 하지만 composite를 통해서 객체의 행동을 실행 중에 동적으로 설정하는 방법을 사용한다면, 즉 객체를 동적으로 구성하면, 기존 코드를 수정하는 대신 새로운 코드를 추가하는 방식으로 새로운 기능을 추가할 수 있다. 기존 코드는 수정되지 않으므로(변경에 대해서는 닫혀있으므로) 버그가 생기거나 사이드 이펙트를 방지하면서 새로운 기능을 추가(확장에 대해서는 열려있는)할 수 있는 것이다.


데코레이터 해보자!

이제 음료에서 추가되는 옵션이 있는 경우 해당 음료를 데코레이터 하는 방식으로 수정해보자. 만약 모카와 휘핑 크림을 추가한 다크 로스트 커피는 다음처럼 할 수 있을 것이다.

  1. DarkRoast 객체를 가져온다
  2. Mocha 객체로 장식한다.
  3. Whip 객체로 장식한다.
  4. cost() 메소드를 호출한다. 이때 추가 옵션의 가격을 계산하는 일은 해당 객체들에게 위임한다.

이 때 장식하고 위임하는 방법은 해당 객체를 래퍼 객체라고 생각하면 쉽다.

이렇게 가장 바깥쪽에 있는 데코레이터 객체에서 cost()를 호출하고, 해당 객체가 장식하고 있는 객체에게 가격을 위임한다. 위임한 객체에게 가격의 값을 얻으면, 자신의 가격을 더한 다음 리턴하는 것이다.

여기서 중요한 점은 데코레이터는 자신이 장식하고 있는 객체에게 어떤 행동을 위임하는 것 외에 원하는 추가적인 작업을 수행할 수도 있다는 저이다.


데코레이터 패턴

데코레이터 패턴에서는 객체에 추가적인 요건을 동적으로 첨가한다. 데코레이터는 서브클래스를 만드는 것을 통해서 기느을 유연하게 확장할 수 있는 방법을 제공한다.

Compent

  • 각 구성요소는 직접 사용할 수도 있고 데코레이터로 감싸져서 쓰일 수도 있다.
  • ex) Beverage 클래스

ConcreteComponent

  • 해당 클래스에 새로운 행동을 동적으로 추가하게 된다.

Decorator

  • 데코레이터는 자신이 장식할 Component와 같은 인터페이스 또는 추상 클래스를 구현한다.
  • 각 데코레이터 안에는 Component 객체가 들어있다. 즉, 데코레이터에는 구성요소에 대한 레퍼런스가 들어있는 인스턴스 변수를 가진다.

ConcreteDecorator

  • ConcreteDecorator 에는 데코레이터가 감싸고 있는 Component 객체를 위한 인스턴스 변수가 있다.
  • Decorator는 Component의 상태를 확장할 수 있다.
  • Decorator에서 새로운 메소드를 추가할 수도 있다. 하지만 일반적으로 새로운 메소드를 추가하는 대신 Component의 메소드를 호출하기 전, 후에 별도의 작업을 처리하는 방식으로 새로운 기능을 추가한다.

Beverage 클래스 다이어그램

public abstract class Beverage {

    String description = "";

    public abstract double cost();

    public String getDescription() {
        return description;
    }
}

public class Espresso extends Beverage {

    public Espresso() {
        description = "에스프레소";
    }

    @Override
    public double cost() {
        return 3500;
    }
}

public abstract class CondimentDecorator extends Beverage {
    public abstract String getDescription();
}

public class Mocha extends CondimentDecorator {

    Beverage beverage;

    public Mocha(Beverage beverage) {
        this.beverage = beverage;
    }

    @Override
    public double cost() {
        return 500 + beverage.cost();
    }

    @Override
    public String getDescription() {
        return beverage.getDescription() + ", 모카";
    }
}
public class StarbuzzCoffee {

    public static void main(String[] args) {
        Beverage beverage = new Espresso();
        System.out.println(beverage.getDescription() + ": " + beverage.cost() + " won");

        Beverage beverage2 = new Espresso();
        beverage2 = new Mocha(beverage2);
        beverage2 = new Mocha(beverage2);
        System.out.println(beverage2.getDescription() + ": " + beverage2.cost() + " won");
    }

}

데코레이터 패턴을 적용한 코드는 아까의 문제점이 사라진 오류의 코드지만 저런형태로 관리하게 될경우 마지막 Soy를 빼먹는다던가 실수로 두번넣는 경우가 생기게됩니다. 팩토리 패턴과 빌더 패턴을 이용해서 더 쉽게 객체를 만드는 방법이 존재한다.


주의해야 할 점

  • 특정 ConcreteComponent 타입을 바탕으로 작업을 처리하는 코드에 데코레이터 패턴을 적용하면 제대로 작동하지 않는다. (ConcreteDecorator로 감싸져 있기 때문)
  • 만약 여러 단계의 데코레이터를 파고 들어가서 어떤 작업을 해야 한다면, 데코레이터 패턴의 의의와 어긋나는 것이다.
  • 데코레이터 패턴에서는 특정한 추상 구성요소를 지정할 필요가 없다. 인터페이스를 사용해도 무방하다.

마무리

  • 상속을 통한 확장은 디자인의 유연성 면에서 좋지 않을 수 있다.
  • 기존 코드를 수정하지 않고도 행동을 확장하는 방법이 필요하다 (OCP)
  • 합성과 위임을 통해서 실행중에 새로운 행동을 추가할 수 있다.
  • 상속 대신 데코레이터 패턴을 통해 행동을 확장할 수 있다.
  • 데코레이터 패턴에서는 구상 구성요소를 감싸주는 데코레이터를 사용한다.
  • 데코레이터 클래스의 형식은 그 클래스가 감싸고 있는 클래스의 형식을 반영한다.
  • 데코레이터에서는 자기가 감싸고 있는 구성요소의 메소드를 호출한 결과에 새로운 기능을 더함으로써 확장한다.
  • 구성요소를 감싸는 데코레이터의 개수에는 제한이 없다.
  • 구성요소의 클라이언트 입장에서는 데코레이터의 존재를 알 수 없다.
    • 따라서 클라이언트에서 구성 요소의 구체적인 타입에 의존하게 되는 경우는 다시 생각해봐야한다.
  • 데코레이터 패턴을 사용하면 객체들이 많이 추가될 수 있고, 코드가 복잡해질 수 있다.
profile
동료들이 같이 일하고 싶어하는 백엔드 개발자가 되고자 합니다!
post-custom-banner

0개의 댓글