정상에서 IT를 외치다

[디자인 패턴] 데코레이터 패턴 본문

디자인패턴

[디자인 패턴] 데코레이터 패턴

Black-Jin 2019. 5. 12. 14:12
반응형

안녕하세요. 블랙진입니다.

책 HeadFirstDesignPatter 을 보며 코드를 Kotlin 으로 바꿔가며 공부한 내용입니다.


데코레이터 패턴(Decorator pattern)이란 주어진 상황 및 용도에 따라 어떤 객체에 책임을 덧붙이는 패턴으로, 기능 확장이 필요할 때 서브클래싱 대신 쓸 수 있는 유연한 대안이 될 수 있다.

출처 위키


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


- 객체 작성이라는 형식으로 실행중에 클래스를 꾸미는 방법입니다.

- 객체에 추가적인 요건을 동적으로 첨가할 수 있습니다.



예제


음료수를 상속한 에스프레소가 있고 여기에는 스팀밀크가 들어갈 수도 있고 모카가 들어갈 수도 있습니다.

//음료를 나타내는 추상 클래스
abstract class Beverage(
val description: String) {

abstract fun cost(): Double
}

class EspressoWithSteamedmMilk:
Beverage("에스프레소, 스팀밀크") {

override fun cost() = 1.00
}

class EspressoWithSteamedMilkAndMocha:
Beverage("에스프레소, 스팀밀크, 모카") {

override fun cost() = 1.55
}



문제 상황


첨가물이 추가 될때마다 새로운 클래스를 계속 생성해야합니다. 


enum class COST(val cost: Double){
MILK(0.1), SOY(0.15), MOCHA(0.2), WHIP(0.1)
}

밀크, 소이, 모카, 휘핑의 가격을 나타내는 클래스 입니다.



문제 해결을 위해 클래스 상속을 사용해 추가사항을 관리해 봅시다.

//음료를 나타내는 추상 클래스
abstract class Beverage(
val description: String,
var milk: Boolean = false,
var soy: Boolean = false,
var mocha: Boolean = false,
var whip: Boolean = false) {

//각 음료 인스턴스마다 추가 사항에 해당하는 추가 가격까지 포함시킬 수 있도록
//추상클래스가 아닌 구현을 하도록 하겠습니다.
open fun cost(): Double {
var sumCost = 0.0

if(hasMilk()) sumCost += MILK.cost

if(hasSoy()) sumCost += SOY.cost

if(hasMocha()) sumCost += MOCHA.cost

if(hasWhip()) sumCost += WHIP.cost

return sumCost
}

// 첨가물에 대한 부울값
fun hasMilk() = milk
fun hasSoy() = soy
fun hasMocha() = mocha
fun hasWhip() = whip

}

추상클래스에 음료, 소이, 모카, 휘핑이 들어있는지 여부를 가져와 cost 함수에서 가격을 더해 반환해줍니다. 이렇게 함으로서 모카가 들어있는 클래스 휘핑이 들어있는 클래스 등 첨가물 별로 클래스를 생성해 주지 않아도 됩니다. 그럼 실재 구현되는 코드를 살펴 볼까요?


class DarkRoast(
milk: Boolean = false,
soy: Boolean = false,
mocha: Boolean = false,
whip: Boolean = false
) :
Beverage("최고의 다크 로스트", milk, soy, mocha, whip) {

override fun cost(): Double {
return 1.99 + super.cost()
}


fun main() {

//우유 추가
val darkRoast1 = DarkRoast(milk = true)

//우유, 모카 추가
val darkRoast2 = DarkRoast(milk = true, mocha = true)

println(darkRoast1.description + " $" + darkRoast1.cost())
println(darkRoast2.description + " $" + darkRoast2.cost())
}

위와 같이 클래스를 생성할 때 첨가물 여부를 추가해주면 됩니다.


문제 상황


1.  첨가물 종류가 많아지면 새로운 메소드를 추가해야 되고 cost 메소드를 고쳐야 합니다

2. 더블 모카와 같은 경우 해당 경우에 맞는 메소드를 따로 구현해야 합니다.

3. 확장에 열려있고 변경에 닫혀 있어야 하는 OCP에 위배됩니다.


객체를 동적으로 구성하면, 기존 코드를 고치는 대신 새로운 코드를 만들어서 새로운 기능을 추가할 수 있습니다. 그럼 데코레이터 패턴을 적용하여 이 문제들을 해결해 보겠습니다. 우리는 기존 코드를 건드리지 않고 확장을 통해 새로운 기능을 추가하는 것이 목표입니다.

abstract class Beverage(
open val description: String = "제목 없음") {

abstract fun cost(): Double
}

먼저 음료에 관한 추상 구상요소를 만들어 줍니다. 여기서 description 은 구현되어 있고 cost 는 서브클래스에서 구현해 줍니다.


class Espresso: Beverage("에스프레소") {

override fun cost() = 1.99
}

class HouseBlend: Beverage("하우스 블랜드 커피") {

override fun cost() = 0.89
}

class DarkRoast: Beverage("다크로스트") {

override fun cost() = 0.99

}

class Decaf: Beverage("디카페인") {

override fun cost() = 1.05
}

자 그럼 Beverage 을 상속하는 4개의 음료  (에스프레소, 하우스 블랜드 커피, 다크로스트, 디카페인) 을 준비 했습니다.


이제 재료를 준비해 보겠습니다.

class SteamMilk(val beverage: Beverage): Beverage() {

override fun cost() = 0.1 + beverage.cost()

override val description: String
get() = "${beverage.description}, 스팀밀크"

}

class Mocha(val beverage: Beverage): Beverage() {

override fun cost() = 0.2 + beverage.cost()

override val description: String
get() = "${beverage.description}, 모카"

}

class Soy(val beverage: Beverage): Beverage() {

override fun cost() = 0.15 + beverage.cost()

override val description: String
get() = "${beverage.description}, 두유"

}

class Whip(val beverage: Beverage): Beverage() {

override fun cost() = 0.1 + beverage.cost()

override val description: String
get() = "${beverage.description}, 휘핑크림"

}

여기서 중요한 포인트는 재료 또한 Beverage 를 상속하고 있다는 점입니다. 그리고 기반이 되는 클래스(Beverage)를 프로퍼티로 가지고 있어 그 값을 그대로 넘겨 받아 재정의 해줄 수 있습니다.


그럼 이제 커피를 주문해 보겠습니다.


fun main() {

//에스프레소 주문
val beverage: Beverage = Espresso()
println("${beverage.description} $${beverage.cost()}")

//다크로스트에 모카 2개 휘핑크림 1개 추가
var beverage2: Beverage = DarkRoast()
beverage2 = Mocha(beverage2)
beverage2 = Mocha(beverage2)
beverage2 = Whip(beverage2)
println("${beverage2.description} $${beverage2.cost()}")

//하우스블랜드에 소이, 모카, 휘핑크림 추가
var beverage3: Beverage = HouseBlend()
beverage3 = Soy(beverage3)
beverage3 = Mocha(beverage3)
beverage3 = Whip(beverage3)
println("${beverage3.description} $${beverage3.cost()}")
}

3가지 케이스로 주문해 보았습니다. 이렇듯 재료 또한 Beverage 를 상속하고 있기 때문에 클래스를 감싸 주어도 Beverage 타입으로 유지될 수 있습니다. 또한 프로퍼티로 기존의 Beverage를 넘겨주기 때문에 내용을 얼마든지 확장할 수 있게 해줍니다. 그럼 위 결과를 살펴보겠습니다.


에스프레소 $1.99

다크로스트, 모카, 모카, 휘핑크림 $1.49

하우스 블랜드 커피, 두유, 모카, 휘핑크림 $1.34


위와 같이 추가된 재료가 description 에 표시되고 cost에 값이 차레로 더해짐을 확인할 수 있습니다. 우리는 데코레이터 패턴을 사용하여 새로운 기능을 동적으로 추가해 줄 수 있게 코딩할 수 있습니다.



코틀린에서의 위임


Delegation - Kotlin Doc

Decorator pattern with class delegates

Kotlin의 클래스 위임은 어떻게 동작하는가


데코레이터 패턴은 기반이 되는 클래스를 프로퍼티로 가지고 변경이나 추가와 같은 기능을 재정의 합니다. 하지만 기반이 되는 클래스의 기본 기능들은 요청을 전달하기 위해서 한번 Wrapping 하는 번거로운 작업이 필요하지만 kotlin 에서는 이를 도와주는 기능이 있습니다.

// C를 생성하고, A에서 정의하는 B의 모든 메서드를 C에 위임합니다.
class C : A by B

by 키워드를 사용해 interface인 A를 정의하는 B의 모든 메서드를 C에게 위임합니다. 우리는 Kotlin의 클래스 위임을 통해 상속의 방식 대신 위임 패턴(Delegate Pattern)을 응용해 볼 수 있습니다, 이렇게 클래스 위임을 사용함으로서 캡슐화와 다형성을 구현할 수 있고 모듈을 유연하게 구성할 수 있습니다. 


interface Beverage{

val description: String

fun cost(): Double
}


class Espresso: Beverage {

override var description = "에스프레소"

override fun cost() = 1.99
}

위임을 사용하기 위해서 Beverage 를 인터페이스로 변경해 줍니다.


class SteamMilk(val beverage: Beverage): Beverage by beverage {

override fun cost() = 0.1 + beverage.cost()

override val description: String
get() = "${beverage.description}, 스팀밀크"
}

스팀 밀크를 준비해 줍니다. Beverage 을 구현하고 있지만 구체적인 구현은 by 키워드를 사용해 beverage 에 위임하였습니다. 



우리는 클래스로부터 행동을 확장하기 위한 2가지 방법을 알고 있습니다.


1. 클래스를 확장하고 서브 클래스를 만들어 줍니다.


2. 데코레이터 패턴을 사용합니다.



데코레이터 패턴을 사용하기 위해서는 3가지 사항이 있습니다.


1. 데코레이터를 하기 위한 기반 클래스를 필드로 가져야 합니다.


2. 함수를 모두 오버라이드 합니다.


3. 기타 추가사항을 더해줍니다.


여기서 많은 보일러 플레이트 코드가 발생합니다. 코틀린은 by 키워드를 사용하는 위임 클래스를 사용해 이러한 데코레이터 패턴을 쉽게 구현할 수 있게 도와줍니다.


// cost 함수를 오버라이드 해주지 않으면 에러가 발생합니다.
class SteamMilk1(val beverage: Beverage): Beverage {

//override fun cost() = 0.1 + beverage.cost()

override val description: String
get() = "${beverage.description}, 스팀밀크"

}

// cost 함수를 오버라이드 해주지 않아도 동작합니다.
class SteamMilk2(val beverage: Beverage): Beverage by beverage {

//override fun cost() = 0.1 + beverage.cost()

override val description: String
get() = "${beverage.description}, 스팀밀크"
}

SteamMilk1 기존 방식의 코드로 cost() 함수를 오버라이드 해주지 않으면 컴파일 에러가 발생합니다. 하지만 위임 패턴을 사용한 SteamMilk2 는 cost() 를 오버라이드 해주지 않아도 에러가 발생하지 않습니다. 현재 오버라이드 해야되는 함수가 cost() 뿐이지만 그 갯수가 많아지게 되면 데코레이터 패턴을 구현할 때 오버라이드 해야 하는 함수가 매우 많아집니다. 이렇듯 위임을 사용하면 기존 클래스의 메소드에 위임하는 기본 구현으로 충분한 메소드는 따로 오버라이드할 필요가 없다는 큰 장점이 있습니다.





반응형
Comments