예제 코드로 이해해보는 디자인 패턴 - 데코레이터 패턴(Decorator Pattern)

kindof

·

2023. 11. 5. 21:11

데코레이터 패턴(Decorator Pattern)은 객체에 동적으로 추가적인 책임을 더해줄 수 있는 디자인 패턴입니다.

 

데코레이터 패턴을 구현할 때는 서브클래스의 상속(Inheritance)이 아닌 Composition(조합, 구성)을 활용하는데요.

 

서브 클래스를 활용했을 때도 부모의 역할에 더해 자식의 책임을 추가할 수 있겠지만, 모든 자식이 똑같은 부모의 역할을 상속받아야하고 기능을 확장할 때마다 상속 구조를 다시 설계해야 하는 문제가 발생하게 됩니다.

 

약간 추상적인 이야기이기 때문에 예제 상황과 코드를 통해 데코레이터 패턴을 이해해보도록 하겠습니다.

 

 

1. 코드로 이해하기

데코레이터 패턴을 구현해보기 위해 아래와 같은 상황을 생각해보겠습니다.

 

피자 가게를 운영하는데 피자에는 다양한 토핑이 존재할 수 있습니다. 그리고 각 피자는 서로 다른 가격을 가지고 있고 고객은 기호에 따라 토핑을 추가하여 피자를 주문할 수 있습니다.

 

Decorator Pattern

위 그림은 아래 코드들의 관계들을 나타내는 다이어그램입니다. 데코레이터 패턴은 이름에서 알 수 있듯이 특정 인스턴스가 다른 인스턴스를 장식(decorate)하는 방식으로 구현되는데, 이는 흔히 래퍼 객체(Wrapper Object)로 구현될 수 있습니다.

 

위 그림에서 가장 아래에 있는 CheeseToppings이 PepperoniPizza를 Wrapping하고, 다시 PepperoniPizza가 Pizza를 Wrapping할 예정입니다.

 

객체는 언제든지 감쌀 수 있으므로 실행 중에 필요한 데코레이터를 마음대로 적용할 수 있고, 한 객체를 여러 개의 데코레이터로 감쌀 수 있습니다.

 

또한, 위 그림에서 ToppingsDecorator, Pizza의 관계가 Composition이라고 표기했는데요. 만약 ToppingsDecorator 없이 각 토핑을 포함하는 서브 클래스를 만든다면 PepperoniCheesePizza, MushroomChessPizza, ... , 등 너무 많은 서브 클래스가 필요하고 만약 VegetablePizza에 적용될 수 없는 토핑(베이컨, 햄)이 있다면 이 역시 개별 서브 클래스를 관리해야 하는 번거로움이 생기게 됩니다.

 

 

이제, 실제 코드를 작성해보겠습니다.

[Pizza.java]

public abstract class Pizza {

    String description = "Unknown pizza";

    public String getDescription() {
        return description;
    }

    public abstract double cost();

}

Pizza 클래스는 추상 클래스로 정의하고 getDescription(), cost() 두 개의 메서드를 가집니다.

 

getDescription()은 이미 구현되어있지만 cost()는 서브 클래스에서 구현해야 합니다.

 

[ToppingDecorator.java]

public abstract class ToppingsDecorator extends Pizza {

    Pizza pizza;

    public abstract String getDescription();

}

데코레이터 객체가 자신이 감싸고 있는 객체와 같은 부모를 가져야 원래 있던 구성 요소가 들어갈 자리에 자신이 들어갈 수 있습니다. 따라서 Pizza 클래스를 확장합니다.

 

각 데코레이터(토핑)가 추가될 피자를 멤버 변수로 갖습니다. 또한, 모든 토핑 데코레이터에서 getDescription()을 오버라이딩하도록 추상 메서드로 정의했습니다.

 

 

[PepproniPizza.java]

public class PepperoniPizza extends Pizza {

    public PepperoniPizza() {
        description = "PepperoniPizza";
    }

    public double cost() {
        return 12000;
    }

}

실제 판매할 피자 구현체입니다. Pizza를 상속하고 있으며 부모의 description을 생성자에서 수정합니다. 아직 토핑이 없는 기본 페페로니 피자이기 때문에 기본 가격인 12,000원을 책정했습니다.

 

 

[MushroomPizza.java]

public class MushroomPizza extends Pizza {

    public MushroomPizza() {
        description = "MushroomPizza";
    }

    public double cost() {
        return 15000;
    }
}

페페로니 피자와 유사한 머시룸 피자입니다. 가격은 15,000원입니다.

 

[CheeseTopping.java]

public class CheeseTopping extends ToppingsDecorator {

    public CheeseTopping(Pizza pizza) {
        this.pizza = pizza;
    }

    public String getDescription() {
        return pizza.getDescription() + ", CheeseTopping";
    }

    public double cost() {
        return pizza.cost() + .30;
    }

}

CheeseTopping 클래스는 데이레이터 클래스이며 ToppingsDecorator를 확장합니다. ToppingsDecorator에서 Pizza를 확장했기 때문에 ChessTopping 인스턴스에서도 Pizza 레퍼런스가 존재합니다.

 

따라서 생성자에서 피자 객체를 전달하여 데코레이터 인스턴스가 피자 인스턴스를 래핑(Wrapping) 할 수 있도록 합니다.

 

이제 아래와 같은 테스트 코드를 실행해볼 수 있습니다.

 

[DecoratorTest.java]

    @Test
    void pizza_topping_with_decorator_pattern() {

        Pizza pepperoniPizza = new PepperoniPizza();
        Assertions.assertEquals("PepperoniPizza", pepperoniPizza.getDescription());

        Pizza pepperoniPizzaWithCheeseTopping = new PepperoniPizza();
        pepperoniPizzaWithCheeseTopping = new CheeseTopping(pepperoniPizzaWithCheeseTopping);
        Assertions.assertEquals("PepperoniPizza, CheeseTopping", pepperoniPizzaWithCheeseTopping.getDescription());

        Pizza mushroomPizzaWithCheeseAndCorn = new MushroomPizza();
        mushroomPizzaWithCheeseAndCorn = new CheeseTopping(mushroomPizzaWithCheeseAndCorn);
        mushroomPizzaWithCheeseAndCorn = new CornTopping(mushroomPizzaWithCheeseAndCorn);
        Assertions.assertEquals("MushroomPizza, CheeseTopping, cornTopping", mushroomPizzaWithCheeseAndCorn.getDescription());
        Assertions.assertEquals(19000, mushroomPizzaWithCheeseAndCorn.cost());

    }

위 테스트에서 mushroomPizzaWithCheeseAndCorn 인스턴스는 기본적인 MushroomPizza에서 출발하여 그 위를 CheeseTopping이 Wrapping하고, CornTopping이 Wrapping하고 있습니다.

 

그 결과 description, cost 모두 토핑이 합쳐진 결과로 출력되는 것을 확인할 수 있습니다.

 

2. 정리

위에서 살펴봤듯, 데코레이터 패턴을 사용하면 피자에 다양한 토핑을 동적으로 추가하고 조합할 수 있습니다.

 

그리고 새로운 토핑을 추가할 때 기존 피자 클래스들을 변경할 필요 없이 새로운 내용을 추가할 수 있었습니다. 이를 통해 객체 지향 설계의 개방/폐쇄 원칙(OCP)을 조금 더 준수할 수 있게 되었습니다.

 

간단한 예제로 작성해본 데코레이터 패턴 실습이었지만 객체에 대해 동적으로 역할과 책임을 추가해야 할 때 데코레이터 패턴을 떠올려보면 괜찮을 것 같습니다.