[Swift 디자인 패턴] Decorator Pattern (데코레이터) - 디자인 패턴 공부 10
안녕하세요 Pingu입니다.🐧
지난 글에서는 구조 패턴 중 Composite Pattern(컴포지트)에 대해 알아봤는데요, 이번 글에서는 계속해서 구조 패턴 중 하나인 Decorator Pattern(데코레이터)에 대해 알아보도록 하겠습니다.
데코레이터 패턴이란?
데코레이터 패턴은 기존 객체가 가진 동작들을 포함하는 특수 래퍼를 만들고 새로운 기능을 추가할 수 있는 디자인 패턴입니다.
데코레이터 패턴은 Wrapper 패턴이라고도 합니다. 기존 객체를 감싸는 Wrapper를 만들고 해당 Wrapper 객체에 다른 기능들을 넣는 패턴이기 때문이에요.
- Component
- 동적으로 추가된 기능을 가질 수 있는 객체에 대한 인터페이스를 정의합니다
- 즉 Wrapper와 Wrapper에 의해 래핑 된 객체 모두에 대한 인터페이스를 정의합니다.
- Concrete Component
- Component 인터페이스의 기능들을 실제로 구현한 객체입니다.
- 새로운 기능들이 추가될 수 있는 객체입니다.
- 즉 래핑되는 객체라고 할 수 있습니다.
- Decorator
- Component 인터페이스를 따르는 객체를 참조할 수 있는 필드가 존재합니다.
- Component 인터페이스를 따르는 인터페이스를 정의합니다.
- Concrete Decorator
- 구성 요소에 기능을 추가합니다.
- Client
- 클라이언트는 Component 인터페이스를 통해 모든 객체와 함께 동작하는 여러 계층의 Decorator로 Component를 래핑 할 수 있게 됩니다.
데코레이터 패턴은 언제 쓰나요?
- 다른 객체들에 영향을 주지 않고 개별 객체에 기능들을 추가하고 싶을 때 사용합니다.
- 추가한 기능들은 언제든 없앨 수 있어요.
- 상속을 사용하여 기능을 확장하는 것이 힘들 경우에 사용합니다.
https://refactoring.guru/design-patterns/decorator
여기에 나온 예제가 이해가 잘 되길래 해당 예제로 글을 작성해보겠습니다.
위와 같이 어떤 프로그램에서 중요한 이벤트가 발생했을 때 사용자에게 알림을 주는 라이브러리가 있다고 생각해보겠습니다. 처음에는 단순하게 이메일을 통해서만 알림을 줄 수 있는 라이브러리였는데요, 시간이 지나 다양한 플랫폼이 생기며 사용자들은 다양한 플랫폼에서 알림을 받고 싶어 졌습니다.
SMS, Slack, Facebook 메신저를 통해 알람을 받고싶어진 사용자들을 위해 라이브러리를 어떻게 수정해야 할까요? 위의 그림과 같이 가장 단순하게 Notifier 클래스의 서브 클래스들을 만들어 기능을 추가할 수 있습니다.
그런데 또 시간이 지나 사용자 중 다양한 플랫폼에서 한 번에 알람을 받기를 원하는 사용자가 발생했습니다. SMS, Facebook, Slack 플랫폼 모두에서 말이에요. 이를 위해 또 알람을 줄 플랫폼의 조합에 따라 상속을 받아 처리하는 것은 너무 비효율적입니다. 이럴 때 데코레이터 패턴을 사용하면 됩니다!
위와 같이 상속을 통해 기능을 확장하게 되면 런타임에서 기존 객체의 기능을 변경할 수 없게 됩니다. 따라서 상속말고 새로운 객체를 만들고 기존 객체를 참조만 해주면 기존 객체의 기능도 모두 할 수 있고 새로운 객체에 기능을 추가할 수도 있게 됩니다. 여기서 새로운 객체를 'Wrapper'라고도 부릅니다. Wrapper도 기존 객체의 인터페이스를 따르게 하면 데코레이터 패턴이 됩니다.
그럼 아까 본 예를 데코레이터 패턴으로 바꿔볼게요.
Notifier 클래스만 그대로 두고 추가할 플랫폼의 기능들은 모두 데코레이터를 통해 추가하면 됩니다.
위와 같이 변경하면 됩니다. 가장 하단에 존재하는 데코레이터가 클라이언트에서 실제로 작업하는 객체가 됩니다. 모든 데코레이터는 Notifier와 동일한 인터페이스를 사용하기 때문에 클라이언트에서 어떤 객체가 사용되고 있는지 몰라도 됩니다. 또한 여러개의 플랫폼에 알림을 보내고 싶을 때에도 각각의 데코레이터 객체만 새로 만들고 처리하면 쉽게 사용할 수 있게 됩니다.
데코레이터 패턴의 결과
장점
- 상속을 통한 하위 클래스를 만들지 않고도 객체의 기능을 확장할 수 있습니다.
- 런타임에서 객체에 책임을 추가하고 제거할 수 있습니다.
- 객체를 여러 데코레이터로 래핑하여 여러 동작을 합칠 수도 있습니다.
- 객체 지향 프로그래밍에서 중요한 단일 책임 원칙을 지킬 수 있습니다.
단점
- 래퍼 스택에서 특정 래퍼를 제거하는 것은 어렵습니다.
- 데코레이터 기능이 데코레이터 스택 순서에 의존해야 합니다.
- 코드가 복잡해질 수 있습니다.
예제
그럼 이제 Decorator Pattern을 Swift로 구현해보겠습니다.
아까 예로 들었던 알람 기능을 한 번 구현해볼게요.
가장 먼저 만들어야 할 것은 Component 프로토콜입니다.
// Component
protocol NotifierComponent {
func notify(message: String)
}
간단하게 알림을 보내는 기능만 만들었어요.
그럼 이제 NotifierComponent 프로토콜을 채택한 기본이 되는 Concrete Component를 만듭니다.
// Concrete Component
class Notifier: NotifierComponent {
func notify(message: String) {
print("\(message) 전송완료")
}
}
이렇게만 쓰고 있었는데 이제 추가적인 플랫폼에도 알람을 보내고 싶어 져서 기존 프로토콜을 준수하는 Decorator 프로토콜도 만듭니다.
// Decorator
protocol NotifierDecorator: NotifierComponent {
var wrappee: NotifierComponent { get set }
init(notifier: NotifierComponent)
}
Decorator는 Wrapper의 역할을 한다고 했었는데요, 그래서 객체를 하나 참조해줄 wrappee 필드를 만들어줘야 합니다.
그런 뒤 Decorator 프로토콜을 채택하는 Email, Slack Notifier를 만들어냅니다.
// Concrete Decorator
class EmailDecorator: NotifierDecorator {
var wrappee: NotifierComponent
required init(notifier: NotifierComponent) {
self.wrappee = notifier
}
func notify(message: String) {
let to = getInfo()
print("\(to)에게 Email로 \(message) 전송완료")
self.wrappee.notify(message: message)
}
func getInfo() -> String {
print("보낼 Email 주소를 입력하세요 : ", separator: "", terminator: "")
return readLine()!
}
}
// Concrete Decorator
class SlackDecorator: NotifierDecorator {
var wrappee: NotifierComponent
required init(notifier: NotifierComponent) {
self.wrappee = notifier
}
func notify(message: String) {
let to = companyInfo()
print("\(to) Slack 채널에 \(message) 정송완료")
wrappee.notify(message: message)
}
func companyInfo() -> String {
print("보낼 Company 채널을 입력하세요 : ", separator: "", terminator: "")
return readLine()!
}
}
EmailDecorator에서는 이메일 주소를 입력받는 기능이, SlackDecorator에서는 회사의 이름을 입력받는 기능이 추가되었습니다.
이제 이렇게 만든 클래스들을 직접 사용해볼게요.
일단 Concrete Component만 사용하면 아래와 같아요.
Email도 함께 보내고 싶을 땐 EmailDecorator를 만들면 되겠죠?
이메일 주소를 입력받는 기능을 수행한 뒤 알림을 전송하는 로직이 잘 실행된 것을 볼 수 있습니다.
그럼 만약 Slack, Email 모두 보내고 싶다면 어떻게 하면 될까요?
이렇게 래핑을 다중으로 해줘서 여러 개의 플랫폼에 한 번에 보낼 수 있게 됩니다.
이렇게 구조 패턴 중 하나인 Decorator Pattern에 대해 알아보고 간단하게 구현도 해봤습니다.
혹시라도 틀린 부분이 있다면 알려주시면 감사하겠습니다.
전체 코드는 여기에서 볼 수 있습니다.
감사합니다.