데코레이터패턴
데코레이터 패턴이 뭔가요?
간단히 말하면 기존 객체를 수정하지 않고 새로운 기능을 추가하는 방법입니다.
스타벅스에서 커피를 주문한다고 생각해보세요. 기본 아메리카노에 샷 추가, 휘핑크림 추가, 시럽 추가...
이런 식으로 원하는 옵션을 계속 덧붙일 수 있죠. 데코레이터 패턴이 바로 이런 개념입니다.
왜 좋은가요?
상속의 한계를 극복할 수 있어요. 상속으로 모든 조합을 만들려면 클래스가 엄청 많아집니다:
- 라떼 클래스
- 휘핑라떼 클래스
- 바닐라휘핑라떼 클래스
- 샷추가바닐라휘핑라떄 클래스...
이건 너무 비효율적이죠. 데코레이터 패턴을 쓰면 런타임에 필요한 기능만 조합할 수 있습니다
핵심 특징:
- 기존 객체를 수정하지 않고 새로운 기능 추가
- 런타임에 동적으로 데코레이션 적용/제거
- 여러 데코레이터를 체인 형태로 연결 가능
- 단일 책임 원칙(SRP)과 개방/폐쇄 원칙(OCP) 준수
데코레이터 패턴의 구조:
Component (프로토콜)
├── ConcreteComponent (기본 구현)
└── Decorator (데코레이터 기본 클래스)
├── ConcreteDecorator A
├── ConcreteDecorator B
└── ConcreteDecorator C
- Component: 기본 기능을 정의하는 인터페이스(프로토콜). 모든 객체가 따라야 하는 규칙.
- ConcreteComponent: 기본 기능을 구현하는 클래스
- Decorator: 컴포넌트를 감싸는 추상 클래스 / 컴포넌트와 동일한 인터페이스를 구현하며 내부에 감쌀 객체를 참조.
- ConcreteDecorator: 추가 기능을 구현하는 데코레이터
예시를 들어보자
- 기본 객체(피자): 원래의 기능(컴포넌트).
- 토핑(데코레이터): 추가 기능. 기본 객체를 감싸서 새로운 행동이나 속성을 더함.
- 결과: 원래 객체를 수정하지 않고, 원하는 기능만 자유롭게 추가하거나 조합 가능.
- 상속은 클래스를 고정적으로 확장하지만, 데코레이터는 런타임에 동적으로 기능을 추가합니다.
- 객체를 래핑(wrapping)하여 기존 기능을 유지하면서 새로운 기능을 덧붙입니
🏠 2. iOS에서의 데코레이터 패턴 실제 구현
커피숍에서 음료에 우유, 시럽, 휘핑크림 등을 추가하는 상황을 생각해봅시다. 데코레이터 패턴을 사용하면 기본 음료에 원하는 옵션을 동적으로 추가할 수 있습니다.
// Component 프로토콜
protocol BeverageComponent {
var description: String { get }
var cost: Double { get }
}
// ConcreteComponent
class Espresso: BeverageComponent {
var description = "Espresso"
var cost: Double = 1.99
}
class HouseBlend: BeverageComponent {
var description = "House Blend Coffee"
var cost: Double = 0.89
}
// Decorator 기본 클래스
class BeverageDecorator: BeverageComponent {
private let beverage: BeverageComponent
init(beverage: BeverageComponent) {
self.beverage = beverage
}
var description: String {
return beverage.description
}
var cost: Double {
return beverage.cost
}
}
// ConcreteDecorator 1
class MilkDecorator: BeverageDecorator {
override var description: String {
return super.description + ", Milk"
}
override var cost: Double {
return super.cost + 0.10
}
}
// ConcreteDecorator 2
class SoyDecorator: BeverageDecorator {
override var description: String {
return super.description + ", Soy"
}
override var cost: Double {
return super.cost + 0.15
}
}
// ConcreteDecorator 3
class MochaDecorator: BeverageDecorator {
override var description: String {
return super.description + ", Mocha"
}
override var cost: Double {
return super.cost + 0.20
}
}
// 기본 에스프레소
var beverage: BeverageComponent = Espresso()
print("\(beverage.description) $\(beverage.cost)")
// Espresso $1.99
// 우유 추가
beverage = MilkDecorator(beverage: beverage)
print("\(beverage.description) $\(beverage.cost)")
// Espresso, Milk $2.09
// 모카 추가
beverage = MochaDecorator(beverage: beverage)
print("\(beverage.description) $\(beverage.cost)")
// Espresso, Milk, Mocha $2.29
// 소이 추가
beverage = SoyDecorator(beverage: beverage)
print("\(beverage.description) $\(beverage.cost)")
// Espresso, Milk, Mocha, Soy $2.44
설명:
- Espresso는 기본 음료(구체적 컴포넌트).
- MilkDecorator와 MochaDecorator는 각각 우유와 모카를 추가하는 데코레이터.
- 데코레이터는 기존 음료를 감싸서 설명과 비용을 확장.
- 클라이언트는 단순히 Beverage 타입으로 작업하므로, 데코레이터와 컴포넌트를 구분할 필요 없음.
SwiftUI에서도 데코레이터 패턴
ViewModifier는 데코레이터패턴의 예시이다. View에 추가적인 수정사항을 동적으로 적용할 수 있습니다.
// ViewModifier를 활용한 데코레이터 패턴
struct ShadowModifier: ViewModifier {
let radius: CGFloat
let x: CGFloat
let y: CGFloat
func body(content: Content) -> some View {
content
.shadow(radius: radius, x: x, y: y)
}
}
struct BorderModifier: ViewModifier {
let color: Color
let width: CGFloat
func body(content: Content) -> some View {
content
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(color, lineWidth: width)
)
}
}
// 사용 예시
Text("Hello, World!")
.padding()
.modifier(BorderModifier(color: .blue, width: 2))
.modifier(ShadowModifier(radius: 5, x: 2, y: 2))
- ShadowModifier와 BorderModifier는 각각 그림자와 테두리를 추가하는 데코레이터.
- Text 뷰에 이들을 체이닝하여 스타일을 동적으로 추가.
- SwiftUI의 modifier 체인은 데코레이터 패턴의 전형적인 예.
3. 다른 패턴과 비교
- 어댑터 패턴(Adapter Pattern):
- 목적: 서로 다른 인터페이스를 호환되도록 변환.
- 예: 새로운 API를 기존 코드에 맞게 변환.
- 차이점: 데코레이터는 기능을 추가하지만, 어댑터는 인터페이스를 변경.
- 프록시 패턴(Proxy Pattern):
- 목적: 객체에 대한 접근을 제어(예: 지연 로딩, 권한 체크).
- 예: 네트워크 요청을 실제로 보내기 전에 캐시 확인.
- 차이점: 데코레이터는 기능 추가에 초점, 프록시는 접근 제어에 초점.
- 컴포지트 패턴(Composite Pattern):
- 목적: 객체를 트리 구조로 구성하여 부분-전체 관계를 표현.
- 예: UI 컴포넌트 계층 구조.
- 차이점: 데코레이터는 단일 객체의 기능 확장, 컴포지트는 복합 객체 관리.
🏢 4. 실무에서의 데코레이터 패턴 적용 시나리오
이제 실제 iOS 개발 현장에서 데코레이터 패턴이 어떻게 활용되는지 구체적인 시나리오를 통해 살펴보겠습니다.
4.1 네트워크 모니터링과 로깅 데코레이터
앱 개발에서 네트워크 요청에 다양한 기능(로깅, 사용자 인증, 오류 처리, 캐싱 등)을 추가하는 상황이 빈번히 발생합니다. 데코레이터 패턴으로 이를 우아하게 해결할 수 있습니다.
// 네트워크 레이어 기본 프로토콜
protocol NetworkManager {
func request(url: URL, completion: @escaping (Result<Data, Error>) -> Void)
}
class BaseNetworkManager: NetworkManager {
private let session = URLSession.shared
func request(url: URL, completion: @escaping (Result<Data, Error>) -> Void) {
session.dataTask(with: url) { data, response, error in
if let error = error {
completion(.failure(error))
} else if let data = data {
completion(.success(data))
}
}.resume()
}
}
// 데코레이터 기본 클래스
class NetworkDecorator: NetworkManager {
private let manager: NetworkManager
init(manager: NetworkManager) {
self.manager = manager
}
func request(url: URL, completion: @escaping (Result<Data, Error>) -> Void) {
manager.request(url: url, completion: completion)
}
}
// 로깅 데코레이터
class LoggingNetworkDecorator: NetworkDecorator {
override func request(url: URL, completion: @escaping (Result<Data, Error>) -> Void) {
print("[Network] Request to: \\(url.absoluteString)")
let startTime = CFAbsoluteTimeGetCurrent()
super.request(url: url) { result in
let endTime = CFAbsoluteTimeGetCurrent()
let duration = endTime - startTime
switch result {
case .success(let data):
print("[Network] Success - \\(data.count) bytes in \\(String(format: "%.2f", duration))s")
case .failure(let error):
print("[Network] Error: \\(error.localizedDescription)")
}
completion(result)
}
}
}
4.2 고성능 비디오 스트리밍
실시간 비디오 스트리밍 앱에서는 대역폭 최적화, 이미지 품질 조절, 캐싱 전략 등 다양한 기능을 영상 통신에 동적으로 적용해야 합니다.
📝 핵심 요약
- 데코레이터 패턴은 기존 객체에 기능을 동적으로 연결하여 기능을 확장하는 구조적 패턴입니다.
- iOS 개발에서 SwiftUI의 ViewModifier, 네트워크 처리 레이어, 다국어 지원과 같은 다양한 시나리오에서 희지발적으로 사용됩니다.
- 성능 최적화를 위해 체이닝 깊이를 제한하고, 지연 초기화를 적리하게 활용하여 메모리 사용량을 최적화합니다.
- 다른 디자인 패턴(어댑터, 프록시, 빌더)과의 차이점을 명확히 이해하고 상황에 맞는 패턴을 선택해야 합니다.
장점:
- 유연성: 런타임에 기능을 추가/제거 가능.
- 단일 책임 원칙(SRP): 각 데코레이터가 하나의 기능만 담당.
- 개방/폐쇄 원칙(OCP): 기존 코드를 수정하지 않고 확장 가능.
- 재사용성: 데코레이터를 다른 객체에 적용 가능.
단점:
- 복잡성: 데코레이터가 많아지면 코드가 복잡해질 수 있음.
- 디버깅 어려움: 체이닝이 깊어지면 어떤 데코레이터에서 문제가 생겼는지 추적하기 어려움.
- 메모리 사용: 많은 데코레이터 객체가 생성될 수 있음.
🚀 마치며
데코레이터 패턴은 단순히 '기능을 추가하는' 패턴이 아니라, 소프트웨어 아키텍처를 보다 유연하고 확장 가능하게 만드는 강력한 도구입니다. iOS 개발자로서 실제 프로젝트에 적용할 때는 성능과 가독성의 균형을 맞추는 것이 중요합니다.
헤드퍼스트의 데코레이터 패턴 예시에서 드러나듯이, 어떤 기능을 딱하다 위시 또는 제거할 수 있는 유연성이 이 패턴의 가장 큰 장점입니다. 특히 사용자 설정, A/B 테스트, 그리고 다양한 엔진에서 매우 유용하게 활용될 것입니다.