Skip to content

Latest commit

 

History

History
171 lines (111 loc) · 7.37 KB

3. 데코레이트 패턴.md

File metadata and controls

171 lines (111 loc) · 7.37 KB

데코레이트 패턴이란?

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

음.. 동적으로 첨가한다는 말이 와닿지는 않지만 어떤 것인지 좀 더 알아보겠습니다.


커피 예시

coffee

어떤 커피집에서 초기에 위와 같이 클래스를 설계했다고 가정하겠습니다. Beverage는 음료를 나타내는 추상 클래스이고, 커피집에서 파는 모든 음료는 Beverage 클래스를 상속받아야 합니다.

그리고 cost()라는 추상메소드를 가지고 있기 때문에 하위 클래스에서 모두 구현해야 합니다.


그런데 위의 클래스 설계는 문제가 있습니다.

요즘 대부분의 커피집은 휘핑크림, 샷추가, 모카 추가, 아이스크림 추가 등등 엄청나게 많은 부가적인 것들이 있습니다. 이런 것들을 추가할 때마다 돈을 더 내야하는 여러가지 상황이 존재합니다.

그래서 추가할 수 있는 것들이 나올 때마다 클래스 설계를 했더니 아래와 같이 되었습니다.

class

그냥 보아도 클래스의 개수가 엄청 많고 복잡하다는 것을 알 수 있습니다.

change

그래서 좀 더 고민을 해본 후에 위와 같이 설계를 바꾸었습니다.

Beverage 클래스에 우유, 두유, 모카, 휘핑 크림이 들어가는지 여부를 인스턴스 변수로 추가했습니다. 그리고 cost() 메소드를 추상메소드로 선언하지 않고 Beverage 클래스 내부에서 추가 사항에서 추가 가격까지 포함시킬 수 있도록 구현을 해놓았습니다.

이렇게 설계를 했더라도, 하위 클래스에서는 cost()를 오버라이딩 해서 기능을 확장해서 자신의 음료 가격을 더하는 것을 구현하고, 추가된 가격에 대해서는 Beverage 클래스의 cost() 메소드에서 값을 얻을 수 있습니다.


이래도 지속적으로 사용하기에는 문제가 있습니다.

  1. 차후에 휘핑 크림, 모카 같이 추가할 수 있는 것들이 추가된다면 Beverage 클래스를 계속 수정해야 합니다.
  2. 메뉴가 다양해 질 수록 Beverage 클래스가 불분명해집니다. 지금은 커피만을 판매하고 있지만 나중에 차(tea)를 팔게 된다면, 차에는 휘핑크림이나 초콜릿 이런것들을 추가하지 않기 때문에 필요 없는 메소드들(hasWhip())을 계속 상속 받게 됩니다.

OCP(Open-Closed Principle)

디자인 원칙

클래스는 확장에 대해서는 열려 있어야 하지만 코드 변경에 대해서는 닫여 있어야 한다.

즉, 기존 코드는 건드리지 않은 채로 확장을 통해서 새로운 행동을 간단하게 추가할 수 있도록 하는 것


이제 다시 데코레이터 패턴에 대해서 알아보겠습니다. 위에서 아까 객체에 추가적인 요건을 동적으로 첨가한다라고 했습니다.

dong

그림으로 보면 위와 같습니다.

  1. DarkRoast를 주문합니다.
  2. 그리고 Mocha를 추가했기 때문에 다시 새로 감싸는 것을 볼 수 있습니다.
  3. 그리고 휘핑크림도 추가했기 때문에 한번 더 감싸고 있는 상황입니다.

그리고 이렇게 감싼 후에 가장 바깥에 있는 Whip의 cost()를 호출하면 Whip -> Mocha -> DarkRoast 순서로 cost() 메소드를 호출하게 됩니다. 가격의 결과가 반환될 때는 DarkRoast -> Mocha -> Whip 순서로 최종 가격이 반환됩니다.


데코레이터 패턴을 적용해서 커피 클래스 설계의 문제점을 해결해보겠습니다.


데코레이터 패턴 적용하기

decorator

그러면 위와 같이 설계할 수 있습니다. 왼쪽에 보이는 HouseBlend, DarkRoast, Decaf, Espresso는 커피 종류마다 구성요소를 나타내는 구상 클래스입니다.

Milk, Whip, Soy, Mocha는 각각의 첨가물을 나타내는 데코레이터 입니다. 이는 cost() 뿐만 아니라, getDescription()도 구현해야 합니다.


예제 코드

public abstract class Beverage {
    String description = "제목 없음";

    public String getDescription() {
        return description;
    }

    public abstract double cost();
}

커피집에서 파는 모든 음료는 위의 클래스를 상속받아야 합니다.

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

첨가물 관련 클래스들은 모두 위의 클래스를 상속 받아야 합니다.

public class Espresso extends Beverage {

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

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

에스프레소는 음료의 종류이기 때문에 Beverage 클래스를 상속 받았고, 자신에게 맞는 가격을 오버라이딩 한 것을 볼 수 있습니다.

public class Mocha extends CondimentDecorator {
    Beverage beverage;

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

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

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

그리고 첨가물인 Mocha 클래스는 CondimentDecorator 클래스를 상속 받았고 이것도 cost()를 자신에게 맞게 오버라이딩을 했습니다.

여기서 보아야할 점은 필드가 Beverage 타입이고 Mocha 객체를 만들 때 생성자로 필요로 한다는 것입니다. 이것이 바로 객체를 감싸는 것입니다. 지금은 이해가 안될 수 있으니 Main 클래스를 보고 좀 더 이해해보겠습니다.

public class StarbuzzCoffee {
    public static void main(String[] args) {
        Beverage beverage2 = new HouseBlend();
        beverage2 = new Mocha(beverage2);
        beverage2 = new Mocha(beverage2);

        System.out.println(beverage2.getDescription() + " $" + beverage2.cost());
    }
}
  • Beverage beverage2 = new HouseBlend(); 이 코드를 통해서 처음에 HouseBlend 커피를 주문했습니다.
  • beverage2 = new Mocha(beverage2); 그리고 이 코드를 통해서 Mocha를 추가했습니다. (한마디로 HouseBlend 객체를 Mocha로 데코레이트 한 것입니다.(객체를 감싼))
  • 이 때 Mocha 객체를 만들면서 Mocha 클래스 내부에 Beverage 인스턴스 필드가 HouseBlend 객체를 가리키게 됩니다. 그렇게 총 두번 Mocha를 추가한 상태가 되었습니다.
  • 그런 후에 beverage2.cost()를 통해서 비용을 계산하면 위에서 보았던 객체를 감싼 그림처럼 동작을 하게 됩니다.
    • HouseBlend cost() -> Mocha cost() -> Mocha cost()를 호출하게 되고, Mocha cost() + Mocha cost() + HouseBlend cost()를 통해서 총 비용이 계산되게 됩니다.

마지막에 객체를 감싸는 것이 이해하기가 쉽지 않았던 것 같습니다,,,