구조 패턴 - 데코레이터 패턴 (Decorator Pattern)

French Marigold·2024년 4월 3일
0

디자인패턴

목록 보기
4/10
post-thumbnail

정의

  • 데코레이터 패턴 (Decorator Pattern)이란 원본 객체가 가진 동작들을 포함하는 특수 Wrapper들을 만들어 원본 객체에 새로운 기능을 추가(중첩) 할 수 있는 패턴이다.
  • 쉽게 말하면 데코레이팅 객체 내부의 Wrapper 타입에 Components 프로토콜 타입의 값을 할당하여 계속해서 기능을 중첩시키고자 할 때 사용되는 패턴이다.

데코레이터 패턴 (Decorator Pattern) 의 구조

  • Components
    • 원본 객체와 Decorator를 모두 포함하는 프로토콜
  • ConcreteComponents
    • 원본 객체.
    • Components 프로토콜을 채택한다.
  • Decorator
    • 원본 객체에 추가할 프로토콜 객체. ConcreteDecorator는 반드시 해당 프로토콜을 채택해야 한다.
    • Components 프로토콜을 채택하는 wrapper 변수가 존재한다.
    • Components 프로토콜을 채택한다.
  • ConcreteDecorator
    • 원본 객체에 추가할 실제 객체. ConcreteDecorator는 Decorator 프로토콜을 반드시 채택해야 한다.
    • Decorator 프로토콜을 채택하므로 Wrapper 변수가 존재함. 이 변수는 Components 프로토콜 타입으로 Components 프로토콜을 채택하는 모든 타입의 값이 이곳에 할당될 수 있다.
    • 이 Wrapper 변수에 할당됨으로써 기능이 중첩될 수 있다.
  • Client
    • 데코레이터 패턴을 내부적으로 사용할 영역.

데코레이터 패턴 적용 과정 예시

  1. 원본 객체와 Decorator 프로토콜을 모두 묶는 역할을 담당하는 Components 프로토콜을 생성한다.
// Componets - 기본적인 Coffee Components를 생성.
protocol Coffee {
    func cost() -> Double
    func ingredients() -> String
}
  1. Components 프로토콜을 채택한 원본 객체를 하나 생성한다. 여기서는 기본 Coffee를 구현하도록 하겠다.
// ConcreteComponents - Coffee 프로토콜을 채택한 원본 객체를 하나 생성한다. 
 class SimpleCoffee: Coffee {
    func cost() -> Double {
        return 1.0
    }
    
    func ingredients() -> String {
        return "Coffee"
    }
}
  1. Decorator 프로토콜은 향후 원본 객체에 추가할 구체적인 객체들이 반드시 채택해야 하는 프로토콜이다. 해당 프로토콜은 반드시 wrapper 변수를 가지고 있어야만 하며, 해당 wrapper 변수에 Components(Coffee 프로토콜 타입) 객체를 할당할 것이다. 이는 원본 객체 (Concrete Components)가 될 수도 있고, 추가적인 기능 객체 (Concrete Decorator) 가 될 수도 있다. 둘 다 Components (Coffee 프로토콜 타입)을 채택하고 있기 때문에 가능한 것이다.
// Decorator - 원본 객체에 추가할 부가적인 객체들이 반드시 채택해야 하는 프로토콜 객체이다.
protocol CoffeeDecorator: Coffee {
    var baseCoffee: Coffee { get }
    init(baseCoffee: Coffee)
}
  1. ConcreteComponets에 추가할 부가 객체들을 여러 개 생성한다. 해당 객체들은 모두 Decorator 프로토콜을 반드시 채택하여야 한다. wrapper 변수에는 Components 프로토콜 타입 (Coffee 프로토콜)의 값이라면 어떤 객체든 할당될 수 있다. 여기서는 우유와 설탕을 추가하기로 한다.
// ConcreteDecorator - Wrapper 변수를 통해 원본 객체에 추가적인 기능을 할당해주는 객체들

// 우유 데코레이터
class MilkDecorator: CoffeeDecorator {
    let baseCoffee: Coffee
    
    required init(baseCoffee: Coffee) {
        self.baseCoffee = baseCoffee
    }
    
    func cost() -> Double {
        return baseCoffee.cost() + 0.5
    }
    
    func ingredients() -> String {
        return baseCoffee.ingredients() + ", Milk"
    }
}

// 설탕 데코레이터
class SugarDecorator: CoffeeDecorator {
    let baseCoffee: Coffee
    
    required init(baseCoffee: Coffee) {
        self.baseCoffee = baseCoffee
    }
    
    func cost() -> Double {
        return baseCoffee.cost() + 0.2
    }
    
    func ingredients() -> String {
        return baseCoffee.ingredients() + ", Sugar"
    }
}
  1. Clients 단에서 데코레이터 패턴의 전체 인스턴스를 활용하여 코드를 알맞게 생산해낸다.
class Clients {
    let simpleCoffee = SimpleCoffee()
    
    // SimpleCoffee(원본 객체)는 Components 프로토콜을 따르고 있으므로  
    // 당연히 Wrapper 변수에 할당될 수 있다. 
    lazy var milkCoffee = MilkDecorator(baseCoffee: simpleCoffee)
    lazy var sugarCoffee = SugarDecorator(baseCoffee: simpleCoffee)
    
    // SugarDecorator(데코레이터 객체) 역시 Decorator 프로토콜을 통해 
    // Components 프로토콜을 따르고 있으므로 Wrapper 변수에 할당될 수 있다. 
    lazy var milkAndSugarCoffee = MilkDecorator(baseCoffee: sugarCoffee)
}

let clients = Clients()
print(clients.milkCoffee.cost())
print(clients.milkCoffee.ingredients())
print(clients.milkAndSugarCoffee.ingredients())

패턴 사용 시기

  • 어떤 객체에 새로운 기능을 추가하고 싶을 때, 해당 객체를 직접 수정하지 않고 (객체 내의 코드를 건들지 않고) 새로운 기능을 확장시켜 나가고자 할 때 사용된다.
    • 예를 들어, 어떤 UIImage에 빈티지 필터를 적용하는 객체가 있다고 가정해보자. 빈티지 필터 객체 내에다가 기능을 추가시키는 것이 아니라 외부 객체를 만들고 빈티지 필터 객체 위에 쌓아올리는 방식으로 기능을 중첩시켜 나갈 수 있다.
  • 데코레이터 패턴은 기존 객체의 기능을 동적으로 추가 또는 수정할 때 유용하게 사용할 수 있는 패턴으로 상속을 통한 확장이 적절하지 않을 때 사용
  • 데코레이터 패턴은 기존 객체가 가진 동작들을 포함하는 특수 래퍼를 만들고 새로운 기능을 추가할 수 있는 디자인 패턴이다.

패턴의 장점

  • 상속보다 훨씬 더 유연하게 기능을 확장할 수 있다.
  • 여러 데코레이터를 융합하여 다양한 객체를 만들어낼 수 있다.
  • SRP ⇒ 각 데코레이터마다 역할이 다르므로 단일 책임 원칙을 준수한다.
  • DIP ⇒ 구현체를 직접 바라보는 것이 아닌 프로토콜을 바라봄으로써 의존 역전의 원칙 또한 준수한다.

패턴의 단점

  • 데코레이터를 융합할 때 코드가 복잡하게 보일 수 있음.
  • 데코레이터를 어떤 순서로 융합하느냐에 따라 결과값이 달라질 수 있으므로 사용 시에 주의가 필요하다.

참고 문헌

profile
꽃말 == 반드시 오고야 말 행복

1개의 댓글

comment-user-thumbnail
2024년 4월 5일

Coffee와 관련된 작업을 하려면 Coffee 클래스를 건드리지 말고 CoffeeDecorator를 건드리자는 전략인 거군요

패턴 사용 시기로 적어주신 내용을 봤을 때
맨 처음 코드를 구현할 때보다는 처음에 예상하지 못한 기능을 추가할 때 주로 사용되는 패턴인 것 같은데
예시도 주 사용 시기에 맞게 바꿔보는 것도 좋아 보입니다

현재의 예시는 Decorator 패턴을 쓸 것이라고 처음부터 생각하고 구성한 느낌이어서요

답글 달기