티스토리 뷰

Java

DesignPattern#03. Decorator Pattern

LichKing 2019. 10. 8. 20:20

햄버거 클래스를 만든다고 생각해보자. 이번 예제부터는 코틀린으로 작성해보려한다. 자바랑 좀 다르긴 하지만 이해하는데 큰 어려움을 없을거라고... 생각한다.....

open class Hamburger(private val bun: String,
                private val patty: String) {
    open fun materials(): String = "$bun $patty"
}

기본 햄버거다. 햄버거 번과 패티를 갖고있다. message() 메서드는 그저 재료들을 나열하는 문자열을 반환하는 간단한 클래스다. 치즈토핑이 추가된 치즈버거를 만들고싶다. 어떻게 구현할 수 있을까?

open class CheeseBurger(private val cheese: String,
                   bun: String,
                   patty: String): Hamburger(bun, patty) {
    override fun materials(): String {
        return super.materials() + " $cheese"
    }
}

상속을 이용해서 구현했다. 이번엔 토마토 토핑이 들어간 토마토 버거를 만들어보자.

open class TomatoBurger(private val tomato: String,
                        bun: String,
                        patty: String): Hamburger(bun, patty) {
    override fun materials(): String {
        return super.materials() + " $tomato"
    }
}

치즈버거랑 똑같이 구현됐다. 사실 지금의 구현으로는 치즈버거와 토마토버거를 구분할 필요가 없다. 토핑이 어차피 String 타입이기때문이다. 이건 샘플코드니까 여기까지는 신경쓰지않기로하자. 이번엔 베이컨버거를 만들자.

open class BaconBurger(private val bacon: String,
                       bun: String,
                       patty: String): Hamburger(bun, patty) {
    override fun materials(): String {
        return super.materials() + " $bacon"
    }
}

예상했겠지만 베이컨 버거도 똑같다. 자 이제 토핑종류를 섞어보자. 토마토치즈버거를 만드려면 어떻게하면 될까? 토마토 버거를 상속받아 치즈를 덧붙인 클래스를 만들면 될까? 베이컨 치즈버거도 똑같이 만들면될까? 그럼 토마토 치즈 베이컨 버거를 만들려면 어떤 클래스를 상속해서 만들어야할까? 포스팅이 이렇게 금방 끝나진 않을테니 예상했겠지만 상속을 이용한 문제해결은 이런 여러가지 조합을 만드는것에 있어 매우 취약하다. 이런식으로 각 조합별로 클래스가 폭발적으로 늘어나는걸 막을 수 없다.

 

1. 컴포지션

해결책은 역시 상속이 아니라 컴포지션을 통해 해결하는 방법이다.

interface MaterialGetter {
    fun materials(): String
}

재료를 구하는 MaterialGetter 인터페이스를 정의한다.

open class Hamburger(private val bun: String,
                private val patty: String): MaterialGetter {
    override fun materials(): String = "$bun $patty"
}

뼈대가 되는 기본 Hamburger 를 정의한다. MaterialGetter 를 구현한다. 여기까지는 아까와 큰 차이가 없다. 상속을 통해 문제를 해결했을때 패턴을 잘 보면 상위 클래스의 메서드 결과에 하위 클래스가 하나씩 결과를 덧붙이고있다. 이걸 decorate 이라고 부르며 탄생한게 Decorator Pattern 인건데 디자인 패턴을 이용해 어떻게 해결하는지 알아보자.

abstract class MaterialDecorator(private val materialGetter: MaterialGetter): MaterialGetter {
    override fun materials(): String = materialGetter.materials()
}

먼저 MaterialGetter 를 구현하는 추상클래스를 하나 만든다. 이 추상클래스는 skeleton class 로 볼 수 있을듯하다. 이 클래스에서 중요한점은 MaterialGetter 를 구현하면서 MaterialGetter 를 내부 필드로 참조한다는 점이다. MaterialDecorator 는 Hamburger 처럼 그 자신이 직접 핵심 클래스라기보다는 핵심클래스를 내부에 참조로 두면서 그 객체를 꾸미는 역할을 하게된다. 일단 이 클래스는 MaterialGetter 를 내부로 참조하며 materials() 메서드를 호출하고있다.

class BaconBurger(materialGetter: MaterialGetter): MaterialDecorator(materialGetter) {
    override fun materials(): String = super.materials() + " bacon"
}
class CheeseBurger(materialGetter: MaterialGetter): MaterialDecorator(materialGetter) {
    override fun materials(): String = super.materials() + " cheese"
}
class TomatoBurger(materialGetter: MaterialGetter): MaterialDecorator(materialGetter) {
    override fun materials(): String = super.materials() + " tomato"
}

MaterialDecorator 를 확장하는 베이컨, 치즈, 토마토 클래스를 정의했다. 얼핏보면 상속을 통해 문제를 해결하는 것처럼 보이지만 핵심은 MaterialDecorator 가 참조하고있는 내부 필드다. 처음 상속을 통해 문제를 해결할때는 2단, 3단 조합이 나오면 클래스 상속이 점점 증가하게되는 구조였다. 하지만 지금은 이 3개의 클래스로 모든 조합을 만들어 낼 수 있다.

 

2. Decorator Pattern

fun main() {
    val hamburger = Hamburger("호밀번", "불고기패티")
    val cheeseBurger = CheeseBurger(hamburger)
    val baconCheeseBurger = BaconBurger(cheeseBurger)
    val tomatoBaconCheeseBurger = TomatoBurger(baconCheeseBurger)

    println(hamburger.materials())
    println(cheeseBurger.materials())
    println(baconCheeseBurger.materials())
    println(tomatoBaconCheeseBurger.materials())
}

가장 처음 Hamburger 객체만 직접 생성하고 이후 치즈, 베이컨, 토마토 객체를 생성할때는 기존 인스턴스를 생성자로 전달해서 객체를 생성하고있다. decorator 용인 MaterialDecorator 는 인터페이스인 MaterialGetter 를 이용해서 참조를 하고있기때문에 MaterialGetter 만 구현했으면 그게 Hamburger 인지, decorator 인지는 중요하지않다. 이런 방식을 이용해 모든 조합가능한 클래스를 생성해야하는 상속대신에 필요 구성 클래스들만 정의해서 컴포지션을 통해 문제를 해결한다.

 

개인적으로 데코레이터 패턴을 공부하면서 상속의 단점을 많이 몸으로 느끼게됐는데, 상속의 가장 큰 단점은 상위 클래스와 하위 클래스가 단단하게 결합된다는 점이다. 이 말이 무슨 말이냐하면 다형성 등을 이용해 상위 클래스를 런타임에 교체하지 못한다는 의미다. 그때문에 Hamburger 를 확장한 BaconBurger 클래스는 추가적인 조합을 위해 자신의 상위 클래스를 동적으로 CheeseBurger로 변경하지못한다. 이로인해 런타임에 뭔가 확장하기보다는 추가적인 하위 클래스를 만들어줘야하고, 이게 클래스 조합 폭발로 이어지는 것이다.

 

decorator pattern 도 분명 상속을 이용하긴하지만 정말 필요한 부분에 있어서만 상속을 활용하고, 동적으로 변경되어야 하는 부분에 대해선 컴포지션을 사용하여 조합 폭발 문제를 끊어냈다. Decorator Pattern 의 클래스다이어그램은 아래와 같다.

 

 

'Java' 카테고리의 다른 글

JDK 14 Release note  (0) 2020.02.07
DesignPattern#04. Singleton Pattern  (0) 2019.10.19
'generic array creation' compile error  (0) 2019.09.27
DesignPattern#02. Observer Pattern  (0) 2019.09.22
DesignPattern#01. Strategy Pattern  (0) 2019.09.21
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함