오늘 알아볼 패턴은 Decorator입니다.
Wrapper라고도 알려져있는데요. 이전에 알아본 Adapter 패턴도 Wrapper라고 불렸습니다.
두 패턴 모두 Wrapper불리는 이유와 어떤 차이점 있는지도 알아보도록 합시다.
1. 목적
객체들을 특수 Wrapper 객체들 내에 넣어서 새로운 동작을 추가할 수 있도록 하는 패턴입니다.

2. 문제 상황
어떠한 프로그램에서 사용자에게 중요한 이벤트를 알려주는 알림 라이브러리를 개발하고 있다고 가정합시다.
초기에는 몇 개의 프로퍼티와 하나의 생성자 그리고 send
라는 메서드를 갖는 Notifier
클래스가 있었습니다.
send
메서드는 클라이언트로부터 메세지를 인자로 받은 후 Notifier
의 생성자를 통해 Notifier
에게 전달된 이메일 목록으로 알림을 보냅니다.
클라이언트는 알림자 객체를 한 번 만들고 알림이 필요할 때마다 사용합니다.

class Notifier {
var emailAddress: [String] = []
public func send(message: String) {
for email in emailAddress {
// Send E-mail
}
}
}
class Application {
private var notifier: Notifier?
public func setNotifier(notifier: Notifier) {
self.notifier = notifier
}
public func doSomething() {
notifier?.send(message: "Alert!")
}
}
let application = Application()
let notifier = Notifier()
notifier.emailAddress.append("client@server.com")
application.setNotifier(notifier: notifier)
application.doSomething()
이렇게 Application
에서는 Notifier
클래스를 사용하여 이벤트에 대한 알림을 미리 정의된 목록의 이메일들로 보낼 수 있습니다.
이후 라이브러리 사용자들은 알림 이상의 기능에 대한 기대가 생기게 됩니다.
누군가는 SMS로, 다른 누군가는 Facebook 알림으로, 또 다른 누군가는 Slack 알림으로 이벤트를 전달받고 싶어합니다.
다이어그램으로 표현하면 다음과 같은 상황이 됩니다.

라이브러리 개발자들은 Notifier
클래스를 확장하고자 그림의 세 가지 추가 알림 방법을 하위 클래스로 생성하였습니다.
class SMSNotifier: Notifier {
// Connect SMS
}
class FacebookNotifier: Notifier {
// Connect Facebook
}
class SlackNotifier: Notifier {
// Connect Slack
}
let application = Application()
let facebookNotifier = FacebookNotifier()
facebookNotifier.emailAddress.append("client@server.com")
application.setNotifier(notifier: facebookNotifier)
application.doSomething()
이제 클라이언트는 원하는 알림 클래스를 사용해서 추가적인 알림을 전달받으면 됩니다.
여기서 또 다른 요구조건이 생겨납니다.
동시에 여러 가지 알림을 전달 받을 수는 없나요? (SMS와 Facebook) 혹은 (SMS와 Slack) 처럼 말이죠.
이 때 라이브러리 개발자들은 아래와 같은 설계를 강행합니다.

class SMSAndFacebookNotifier: Notifier {
// Connect SMS & Facebook
}
class SMSAndSlackNotifier: Notifier {
// Connect SMS & Slack
}
// Other Child Class code ...
N개의 알림 채널이 존재할때 ∑(nCx)
의 하위 클래스가 생겨버리는 셈입니다.
과연 이러한 상황을 올바르게 해결하려면 어떻게 해야할까요?
3. 해결책
3-1. Inheritance vs Aggregation(Composition)
우선 상속으로 문제를 해결한다는 접근에는 문제가 없습니다.
하지만 그전에 상속에 대해 몇가지 주의해야할 점들을 알아보고 넘어갑시다.
첫번째로 상속은 정적(static
)으로 작동합니다. 따라서 런타임 때 객체의 행동을 변경할 수 없습니다.
만약 바꾸고자 한다면, 객체를 다른 하위 클래스의 인스턴스로 교체해야 합니다.
문제 상황으로 예시를 들어보죠.
let application = Application()
let notifier: Notifier = FacebookNotifier()
notifier.emailAddress.append("client@server.com")
application.setNotifier(notifier: notifier)
application.doSomething()
이미 FacebookNotifier
인스턴스를 사용하고 있는 상황에서 SMS 알림이 필요하다면 어떻게 해야할까요?
notifier
의 키워드를 변수(var
)로 바꾸고 새로운 SMSNotifier
인스턴스를 생성하여 application
에 등록합니다.
notifier = SMSNotifier()
notifier.emailAddress.append("client@server.com")
application.setNotifier(notifier: notifier)
application.doSomething()
이처럼 사전에 인스턴스를 교체하는 코드가 준비되어야하며, 이미 코드가 컴파일되고 실행되는 런타임에는 변경이 불가능함을 의미합니다.
두번째는 대부분의 언어에는 다중 상속을 지원하지 않습니다. 따라서 자식 클래스는 하나의 부모 클래스만 가질 수 있습니다.
이는 어떠한 클래스가 동시에 여러가지 클래스를 행동을 할 수 없다는 것을 의미합니다.
이러한 문제점들은 Aggregation(집합 관계) 혹은 Composition(합성)을 이용해서 해결할 수 있습니다.
모두 두 클래스 간의 관계에 대한 개념입니다.
Aggregation의 경우 A 클래스는 B 클래스를 포함하며, B 클래스는 A 클래스에 없이도 사용이 가능합니다.
Composition의 경우 A 클래스가 B 클래스의 Life Cycle을 관리하기 때문에 B 클래스는 A 클래스 없이는 사용이 불가능합니다.
즉 어떠한 객체가 다른 객체에 대한 Reference(참조)를 가지고 일부 작업을 Delegate(위임)합니다.
아래 그림은 상속과 집합 관계의 구조적 차이를 보여줍니다.

Aggregation/Composition 방식을 이용하면 연결된 Helper 객체를 다른 객체로 쉽게 대체하여 런타임에도 행동을 변경할 수 있습니다.
그러면 하나의 클래스에서 여러 클래스의 행동들을 사용할 수 있고, 각 클래스에 대한 Reference들이 모든 종류의 작업을 위임하게 됩니다.
Aggregation/Composition은 Decorator 패턴을 포함한 많은 디자인 패턴의 핵심 원칙입니다.
3-2. Decorator 패턴
다시 Decorator 패턴으로 돌아옵시다.
Wrapper는 Decorator 패턴의 또 다른 이름으로 패턴의 주요 특징을 명학하게 표현하며, 일부 객체들과 연결될 수 있는 또 다른 객체입니다.
Decorator 패턴에서 Wrapper가 어떻게 작동하는지 알아봅시다.
먼저 기존 Notifier
(email)를 유지한 상태로 BaseDecorator
(Wrapper)를 정의합니다.
class BaseDecorator {
private var wrapper: Notifier
init(notifier: Notifier) {
self.wrapper = notifier
}
public func send(message: String) {
self.wrapper.send(message: message)
}
}
그리고 나머지(추가된) 알림 서비스들을 Decorator로 바꿔줍니다.
class SMSDecorator: BaseDecorator {
public func sendSMS(message: String) {
// SMS Service Logic
}
override public func send(message: String) {
super.send(message: message)
sendSMS(message: message)
}
}
class FacebookDecorator: BaseDecorator {
public func sendFacebook(message: String) {
// Facebook Service Logic
}
override public func send(message: String) {
super.send(message: message)
sendFacebook(message: message)
}
}
class SlackDecorator: BaseDecorator {
public func sendSlack(message: String) {
// Slack Service Logic
}
override public func send(message: String) {
super.send(message: message)
sendSlack(message: message)
}
}
현재 상황을 다이어그램으로 보면 다음과 같습니다.

이제 클라이언트에서는 초기의 Notifier
객체를 원하는 서비스(Decorator)들의 집합으로 Wrapping합니다.

class Application {
private var notifier: Notifier?
public func setNotifier(notifier: Notifier) {
self.notifier = notifier
}
public func doSomething() {
notifier?.send(message: "Alert!")
}
}
let app = Application()
var stack = Notifier()
stack.emailAddress.append("client@server.com")
let facebookEnabled = true
let slackEnabled = true
if facebookEnabled {
stack = FacebookDecorator(notifier: stack)
}
if slackEnabled {
stack = SlackDecorator(notifier: stack)
}
app.setNotifier(notifier: stack)
app.doSomething()
이렇게 되면 실제로 Stack 구조처럼 작동하여 원하는 서비스의 알림이 작동하게 됩니다.
(사이트에서 Stack으로 설명되나 정확히는 Wrapping 순서대로 Email -> Facebook -> Slack 이니 Queue에 좀 더 가깝다고 생각합니다.)
또한 이메일 추가 등의 작업도 stack 변수에서 여전히 처리가 가능하며 새로운 알림 서비스를 추가하는 것도 어렵지 않습니다.
4. 구조

5. 장단점
새로운 하위 클래스를 만들지 않고도 객체의 행동을 확장할 수 있습니다.
런타임에 객체의 행동을 추가하거나 제거할 수 있게 됩니다.
객체를 여러개의 Decorator를 Wrapping하면 여러 동작을 결합할 수 있습니다.
SOLID 원칙 중 SRP(Single Responsibility Principle, 단일 책임 원칙)이 준수됩니다.
하지만 Stack(혹은 Queue) 구조의 특성상 결합된 Wrapper 중 특정 Wrapper를 제거하는 것이 어렵습니다.
또한 결합된 기능이 Wrapper의 순서에 영향을 받지 않도록 설계해야합니다.
6. 실제 사용 사례
실전에서 어떻게 사용될 수 있을까요?
두 가지 사례를 살펴봅시다.
6-1. 데이터 입력 검증
입력 형식을 검증해야할 때 단순히 if문을 통과시킬 수도 있습니다.
하지만 여러 필드폼(아이디, 비밀번호, 이메일 등)에 하나의 검증 객체를 활용해본다면 어떨까요?
먼저 Component에 해당 하는 코드를 작성합니다.
// Component
protocol TextValidatorProtocol {
// execute()
func validate(_ text: String) -> Bool
}
// Concrete Component
class TextValidator: TextValidatorProtocol {
// execute()
func validate(_ text: String) -> Bool {
return !text.isEmpty
}
}
Base Decorator를 작성합니다.
여기에는 별도의 로직이 들어가지 않고 그저 TextValidator를 감싸기 위한 코드만 존재합니다.
// Base Decorator
class BaseValidator: TextValidatorProtocol {
// wrappee: Component
public var validator: TextValidatorProtocol
// BaseDecorator(c: Component)
init(validator: TextValidatorProtocol) {
// wrappee = c
self.validator = validator
}
// execute()
func validate(_ text: String) -> Bool {
// wrapee.execute()
self.validator.validate(text)
}
}
이후 다양한 검증 로직을 지닌 Decorator들을 정의합니다.
// Concrete Decorator 0
class LengthValidator: BaseValidator {
private let minLength: Int
init(validator: TextValidatorProtocol, minLength: Int) {
self.minLength = minLength
super.init(validator: validator)
}
// extra()
private func validateLength(_ text: String) -> Bool {
text.count >= minLength
}
// execute()
override func validate(_ text: String) -> Bool {
// super.execute() extra()
super.validate(text) && self.validateLength(text)
}
}
// Concrete Decorator 1
class SpecialCharacterValidator: BaseValidator {
private let specialCharacters = "!@#$%^&*()_+{}|:<>?[];'.,"
// extra()
private func validateSpecialCharacter(_ text: String) -> Bool {
text.contains { specialCharacters.contains($0) }
}
// execute()
override func validate(_ text: String) -> Bool {
// super.execute() extra()
super.validate(text) && self.validateSpecialCharacter(text)
}
}
// Concrete Decorator 2
class NumberValidator: BaseValidator {
// extra()
private func validateNumber(_ text: String) -> Bool {
text.contains { $0.isNumber }
}
// execute()
override func validate(_ text: String) -> Bool {
// super.execute() extra()
super.validate(text) && self.validateNumber(text)
}
}
Client에서는 아래와 같이 사용됩니다.
var textValidator: TextValidatorProtocol = TextValidator()
textValidator = BaseValidator(validator: textValidator)
textValidator = LengthValidator(validator: textValidator, minLength: 8)
textValidator = SpecialCharacterValidator(validator: textValidator)
textValidator = NumberValidator(validator: textValidator)
let isValid = textValidator.validate("@pple.c0m")
print("Validation result: \(isValid)")
// Validation result: true
이를 활용하면 검증해야하는 문자열의 종류과 조건이 추가되어도 쉽게 검증 로직을 추가할 수 있습니다.
6-2. Image Loader
또 다른 사례는 Image Loader입니다.
사진을 로드할때 표시하는 페이지에 따라 Resizing, Compressing Data, Removing Meta Data 등 다양한 작업들을 선택적으로 수행해야할 경우가 많습니다.
이 또한 위와 Decorator 패턴을 활용하면 하나의 객체로 다양한 영역에서 이미지 데이터를 처리할 수 있으며,
각 처리과정을 테스트하기에도 쉬운 구조를 가져가게됩니다.
7. Adapter 패턴과의 차이점
Adapter 패턴도 Wrapper라는 별명이 있습니다.
하지만 몇가지 차이점이 존재합니다.
Adapter는 기존 객체의 인터페이스를 변경하는 방법으로 기능을 확장합니다.
하지만 Decorator는 인터페이스를 변경하지 않고 재귀적 합성을 통해 기능을 확장합니다.
그렇기에 Adapter는 다른 인터페이스로, Decorator는 향상된 인터페이스로 Wrapping 한다고 볼 수 있습니다.
특징 | Adapter | Decorator |
형태 | 인터페이스 변환 | 인터페이스 확장(기능 확장) |
사용 목적 | 호환되지 않는 인터페이스에 맞추기 위해 사용 | 기본 기능에 추가 기능을 동적으로 확장하기 위해 사용 |
동작 방식 | 기존 클래스의 인터페이스를 변경하거나 매핑 | 동일한 인터페이스로 감싸서 기능을 추가함 |
예시 | 외부 API와 호환시키기 | 기존 객체에 로깅, 인증, 캐싱 등 기능 추가 |
Conclusion
Decorator 패턴을 잘 활용하면 필요에 따라 기능을 쉽게 추가하고 제거할 수 있어 재사용성과 유연성이 높은 코드가 작성됩니다.
iOS 개발시에도 다양한 영역에서 활용될 수 있다고 생각되는 패턴입니다.
Reference
https://refactoring.guru/design-patterns/decorator
Decorator
/ Design Patterns / Structural Patterns Decorator Also known as: Wrapper Intent Decorator is a structural design pattern that lets you attach new behaviors to objects by placing these objects inside special wrapper objects that contain the behaviors. Prob
refactoring.guru
'Design Pattern > GoF - Structural Patterns' 카테고리의 다른 글
[GoF Design Patterns] Flyweight (2) | 2024.11.28 |
---|---|
[GoF Design Patterns] Facade (0) | 2024.11.27 |
[GoF Design Patterns] Composite (0) | 2024.11.10 |
[GoF Design Patterns] Bridge (4) | 2024.10.28 |
[GoF Design Patterns] Adapter (1) | 2024.10.24 |