[디자인 패턴] 데코레이터 패턴
상속의 문제
A회사의 알림 메세지 기능이 존재한다. 알림 메세지 기능은 특정 이벤트가 발생한 경우 수신자에게 메세지로 알림을 전송하는 역할을 한다. 근데 갑자기 협력사에서도 알람을 달라고 한다. B협력사는 슬랙으로, C협력사는 카카오톡으로, D협력사는 슬랙과 메세지지로....
이 상황에서 우리는 다음과 같은 구성을 쉽게 생각할 수 있다.
위와 같은 구조의 문제점을 생각해보자. 메세지의 종류가 10가지 종류가 있다면 어떻게 해야할까? 전체 구현해야하는 클래스의 갯수는
10! -1 이다. 생각만 해도 끔찍하다. 그리고 더 끔찍한거는 메세지의 전송방식이 바뀌는 상황이다. 그러면 일일이 해당 방식이 구현된 곳을 찾아 코드를 변경해야한다. 이 시점에서 우리는 아래의 디자인 원칙을 다시 살펴볼 필요가 있다.
디자인 원칙
OCP (Open - Closed Pattern)
클래스는 확장에 열려 있어야하며 코드 변경에 대해서는 닫혀있어야한다.
우리가 원하는 코드는 특정 방식이 추가되었을 때 기존의 방식을 건드리지 않고 새롭게 방식을 추가하는 것이다. 이를 위한 데코레이터 패턴을 알아보자.
데코레이터 패턴
기본적인 기능을 하는 구현체와 새롭게 추가하는 기능에 해당 하는 데코레이터를 이용해서 기능을 합성하는 패턴을 의미한다.
구성 요소
- Component : 기본 기능의 Concrete Component와 Decorator의 공통 기능을 정의한다.
- ConcreteComponent : 기본 기능을 구현한 클래스
- Decorator : Decorator 구현체들의 공통 기능을 제공하는 역할
- ConcreteDecorator : Decorator의 하위 클래스로 기본 기능에 추가되는 개별적인 기능을 뜻함.
클래스 다이어그램
구현
다음은 사용자가 여러가지 방식으로 메세지를 보내는 상황을 가정했다. 사용자는 메세지를 plain하게 보낼 수도 있고 암호화를 할수도, 압축을 할 수도 있다.
사용자 마다 요구하는 방식은 조합의 형태로 구성이 될 것이고 추후에 관련 기능적 요구사항이 추가 될 수 있다. 이러한 경우 서브클래스로 대응하게 되는 경우 만들어져야하는 클래스가 기하급수적으로 많아지고 암호화 알고리즘이 바뀌는 등의 변경사항에 대해 대응하기 어려워진다. 따라서 데코레이터 패턴을 사용해서 향후 확장이 가능한 형태로 코드를 구현하였다.
Component > sender/sender.go
메세지를 보내는 가장 기본적인 기능에 대한 인터페이스를 정의한다.
package sender
type MessageSender interface {
Send(message string) error
}
ConcreteComponent > sender/base_sender.go
가장 기본적인 plain text의 형태로 메세지를 보내는 상황의 코드이다.
package sender
import (
"fmt"
)
type SimpleMessageSender struct{}
func (sms *SimpleMessageSender) Send(message string) error {
fmt.Println("Sending message:", message)
return nil
}
Decorator > decorators/base_decorator.go
Component 인퍼페이스를 래핑해서 데코레이터의 인터페이스로 사용하는 것을 알 수 있다.
package decorators
import "github.com/RicardoKim/Study/DesignPattern/Decorator/sender"
// MessageSenderDecorator 구조체
type MessageSenderDecorator struct {
Wrapped sender.MessageSender
}
func NewMessageSenderDecorator(ms sender.MessageSender) *MessageSenderDecorator {
/*
1. Component에 해당하는 MessageSender 인터페이스 타입을 파라미터로 받는 것을 알 수 있다.
2. MessageSenderDecorator의 새 인스턴스를 생성한다. 이 구조체는 Wrapped필드를 가지고 있으며 이 필드에 메세지 전송기능을 실제로 구현하는 인스턴스를 저장.
*/
return &MessageSenderDecorator{Wrapped: ms}
}
func (msd *MessageSenderDecorator) Send(message string) error {
return msd.Wrapped.Send(message)
}
ConcreteDecorator > decorators/compress_decorator.go
앞서 정의한 Decorator 인터페이스에 대한 구현체이다.
package decorators
import (
"fmt"
"github.com/RicardoKim/Study/DesignPattern/Decorator/sender"
)
type CompressDecorator struct {
*MessageSenderDecorator
}
func NewCompressDecorator(ms sender.MessageSender) *CompressDecorator {
return &CompressDecorator{NewMessageSenderDecorator(ms)}
}
func (cd *CompressDecorator) Send(message string) error {
compressedMessage := "compressed(" + message + ")"
fmt.Println("Compressing message...")
return cd.Wrapped.Send(compressedMessage)
}
main.go
상황에 따라 데코레이터로 감싸서 유연하게 확장 가능한 것을 볼 수 있다.
// main.go
package main
import (
"github.com/RicardoKim/Study/DesignPattern/Decorator/decorators"
"github.com/RicardoKim/Study/DesignPattern/Decorator/sender"
)
func main() {
message := "Hello, Decorator Pattern!"
sms := &sender.SimpleMessageSender{}
encrypted := decorators.NewEncryptDecorator(sms)
encryptedAndCompressed := decorators.NewCompressDecorator(encrypted)
// 기본 메시지 전송
sms.Send(message)
// 암호화된 메시지 전송
encrypted.Send(message)
// 암호화되고 압축된 메시지 전송
encryptedAndCompressed.Send(message)
}
출력
❯ go run main.go
Sending message: Hello, Decorator Pattern!
Encrypting message...
Sending message: encrypted(Hello, Decorator Pattern!)
Compressing message...
Encrypting message...
Sending message: encrypted(compressed(Hello, Decorator Pattern!))
데코레이터 패턴의 단점
1. 구조 파악이 어렵다.
- 데코레이터 객체와 실제 객체 사이의 구별이 어렵다.
- 실제 동작 방식을 알기 위해서는 래퍼 클래스를 계속 타고 들어가야한다.
2. 특정 래퍼의 삭제가 어렵다.
3. 행동이 스택의 구성방식에 영향을 받는다.
긴 글 읽어주셔서 감사합니다.
틀린 부분이 있으면 댓글을 달아주시면 감사하겠습니다.
📧 : realhwan1202@gmail.com