동적으로 추가 요소를 더해 확장할 수 있는 Decorator Pattern

바이너리·2022년 6월 6일
0

데코레이터 패턴은 GoF의 디자인패턴 중 하나로, 객체에 추가 요소를 동적으로(런타임에) 더할 수 있는 패턴입니다.
새로운 요소가 필요할 때 마다 객체를 새로 정의하거나 기존 코드를 변경하지 않고도 유연하게 기능을 확장할 수 있습니다.

기존의 피자 판매 서비스

abstract class Pizza {
    abstract fun price(): Int
    open fun description(): String = "pizza"
}

class Margherita : Pizza() {
    override fun price() = 15000
    override fun description(): String = "tomato based pizza with basil, cheese"
}

class PepperoniPizza : Pizza() {
    override fun price() = 17000
    override fun description(): String = "tomato based pizza with salami, cheese"
}

class Gorgonzola : Pizza() {
    override fun price() = 16000
    override fun description(): String = "cream based pizza with blue cheese"
}

피자 판매 서비스가 있다고 가정하겠습니다. 해당 서비스는 인기있는 스테디셀러 메뉴들을 시작으로 빠르게 성장했습니다.
처음에는 적은 수의 메뉴만 있어도 문제가 없었지만, 서비스의 확장과 여러가지 니즈로 인해 고객이 재료를 직접 커스텀해서 주문할 수 있도록 서비스를 변경하기로 했습니다.

치즈, 토마토, 페퍼로니, 루꼴라 등등 재료를 마음대로 추가할 수 있고 베이스 소스도 고를 수 있게 확장해야 합니다.

단점

하지만 기존의 시스템은 Pizza 클래스를 상속받은 서브클래스들이 하나의 메뉴를 담당했는데, 재료를 동적으로 추가하기는 힘들었습니다.

    val order = "Margherita"

    var pizza: Pizza =
        if (order.lowercase() == "margherita")
            Margherita()
        else if (order.lowercase() == "pepperoni")
            PepperoniPizza()
        else if (order.lowercase() == "gorgonzola")
            Gorgonzola()
        else throw Exception()

    println("The price of the pizza you ordered is ${pizza.price()}.")

심지어 기존의 시스템은 새로운 피자 종류가 추가되거나, 재료 가격이 변경되면 주문과 관련된 코드를 전부 수정해야 했습니다.

토마토 재료 가격이 두 배 올라서 토마토 소스를 사용하는 피자의 가격을 일괄 1000원씩 인상한다고 가정해보겠습니다.
기존 메뉴 중 토마토소스가 베이스인 피자는 마르게리따, 페퍼로니여서, 두 피자의 가격이 1000원씩 인상됐습니다.

이러한 이유로 인해 기존 시스템은 두 가지 객체지향 원칙을 위반하고 있었습니다.

  1. 하나의 프로퍼티가 변경되어도 두 개의 클래스가 변경되었습니다. 이는 클래스가 변경될 하나의 이유만 가져야 한다는 SRP를 위반합니다.
  2. 새로운 피자 종류가 추가되면 주문 코드까지 수정돼야 했습니다. 이는 확장에는 열려있고 변경에는 닫혀있어야 한다는 OCP를 위반합니다.

Decorator Pattern

데코레이터 패턴의 구성요소를 설명하는 클래스 다이어그램입니다.

  • 모든 클래스의 슈퍼클래스가 되는 Component를 정의합니다.
  • 새로운 행동이 동적으로 추가될 수 있는 기반 클래스인 ConcreteComponent를 정의합니다.
  • 장식할 인터페이스/추상 클래스를 구현하는 Decorator를 정의합니다. 구성 요소의 레퍼런스를 포함한 인스턴스 변수를 가지고 있습니다.
  • Component의 상태를 확장할 수 있는 ConcreteDecorator를 정의합니다.

데코레이터 패턴을 적용해서 새롭게 설계해보겠습니다.

  • 모든 피자 객체의 베이스가 될 Pizza를 Component로 지정합니다.
  • 토마토 피자, 일반 피자를 고를 수 있도록 ConcreteComponent로 지정합니다.
  • 나머지 토핑들은 자유롭게 선택할 수 있도록 Pizza를 프로퍼티로 가지는 Decorator를 생성합니다.
  • 치즈, 바질, 페퍼로니와 같이 자유롭게 선택할 수 있는 토핑들은 ConcreteDecorator로 지정합니다.

완성된 설계의 다이어그램은 위와 같습니다.

적용 코드

interface Pizza {	// Component
    fun price(): Int
    fun description(): String
}

class SimplePizza : Pizza {		// ConcreteComponent
    override fun price(): Int = 8000
    override fun description(): String = "simple pizza"
}

class TomatoPizza : Pizza {		// ConcreteComponent
    override fun price(): Int = 10000
    override fun description(): String = "tomato pizza"
}

abstract class PizzaDecorator(val pizza: Pizza) : Pizza		// Decorator

class Cheese(pizza: Pizza) : PizzaDecorator(pizza) {		// ConcreteDecorator
    override fun price(): Int = pizza.price() + 2000
    override fun description(): String = pizza.description() + ", with cheese"
}

class Basil(pizza: Pizza) : PizzaDecorator(pizza) {			// ConcreteDecorator
    override fun price(): Int = pizza.price() + 3000
    override fun description(): String = pizza.description() + ", with basil"
}

class Pepperoni(pizza: Pizza) : PizzaDecorator(pizza) {		// ConcreteDecorator
    override fun price(): Int = pizza.price() + 3500
    override fun description(): String = pizza.description() + ", with pepperoni"
}
  • 모든 클래스의 슈퍼클래스는 Pizza로 같습니다.
  • 한 객체를 여러 개의 데코레이터로 감쌀 수 있습니다. 중복된 데코레이터를 적용할 수도 있습니다.
  • 데코레이터는 자신이 감싼 객체와 같은 슈퍼클래스를 가지고 있어 원래 객체가 들어갈 자리에 데코레이터 클래스가 들어가도 됩니다.
  • 자신이 장식하는 객체에 행동을 위임하는 일 말고도 추가 작업을 수행할 수 있습니다.
fun main() {
    var pizza: Pizza = TomatoPizza()
    pizza = Cheese(pizza)
    pizza = Basil(pizza)

    println("The price of the pizza you ordered is ${pizza.price()}.")
    println("The ingredients for the pizza are ${pizza.description()}.")
}

이처럼 여러 기능을 선택적으로 적용하고 싶을 때 데코레이터로 원래 객체를 감싸면 됩니다.

특징

  • 데코레이터의 형식이 데코레이터로 감싸는 객체의 형식과 같습니다.
    • 원래 구성 요소가 들어갈 자리에 데코레이터 객체가 들어가야 하기 때문입니다.
  • 객체 구성을 이용하고 있어 유연성을 해치지 않습니다.
  • 코드 복잡도가 증가하기에 소규모 시스템에는 적합하지 않습니다.
  • 구상 구성 요소로 돌아가는 코드에 적용하기는 적합하지 않습니다.

정리

개발 중에는 기존 코드의 수정 없이 행동을 확장해야 하는 경우가 생깁니다. 이 때 디자인 유연성 관점에서 상속보다는 구성을 활용하는 편이 좋습니다.
상속을 이용하는 대신 데코레이터 패턴을 적용하면 행동을 확장할 수 있습니다.
데코레이터 패턴은 구상 구성 요소를 감싸주는 데코레이터를 활용하고, 데코레이터 클래스의 형식은 그 클래스가 감싸는 클래스 형식을 반영합니다.
또한 자기가 감싸고 있는 구성 요소의 메서드를 호출한 결과에 새로운 기능을 더함으로써 행동을 확장할 수 있습니다.
하지만 데코레이터 패턴을 남용하면 코드가 필요 이상으로 복잡해질 수 있다는 것에 유의해야 합니다.

profile
01101001011010100110100101101110

0개의 댓글