[디자인 패턴] Decorator Pattern

전성훈·2023년 11월 3일
0

DesignPattern

목록 보기
9/18
post-thumbnail

주제: Wrapper pattern


Decorator pattern이란 주어진 상황 및 용도에 따라 어떤 객체에 책임을 덧붙이는 패턴으로, 기능 확장이 필요할 때 서브 클래싱 대신 쓸 수 있는 유연한 대안이 될 수 있다.

Decorator pattern 이란

  • Decorator Pattern은 디자인 패턴 중 하나로, 객체의 결합을 통해 기능을 동적으로 유연하게 확장할 수 있게 해주는 패턴이다. 이 패턴은 기본 기능을 수행하는 Core Component와 추가적인 기능을 제공하는 Decorator로 이루어져 있다.
  • Decorator Pattern은 자기자신을 다시 자기자신으로 감싸면서 큰 블록을 만들어나가는 패턴이다. 그리고 이러한 과정을 통해 문제를 해결하게 된다.
  • 작은 블록을 다시 작은 블록으로 감싸는 특징 때문에 Decorator pattern을 Wrapper Pattern이라고도 한다.
  • 또 작은 블록으로 큰 블록을 만들어 나간다는 점에서, Structural pattern중에 하나이기도 하다.

Decorator pattern의 구조

  1. Component
    • 모든 객체가 가져야 할 기본 기능을 정의하는 인터페이스이다.
  2. ConcreteComponent
    • 인터페이스를 구현하며, 확장될 기본 기능을 가진 클래스이다.
  3. Decorator
    • Component와 같은 인터페이스를 가지며, Decorator 역할을 수행할 수 있는 클래스이다.
  4. ConcreteDecorator
    • Decorator를 상속받아 구체적인 추가 기능을 가진 클래스이다. Concrete Decorator 객체는 ConcreteComponent 객체를 감싸며, 자신의 기능을 추가한다.

장단점

장점

  1. 기능 확장이 용이하다.
    • Decorator 패턴을 사용하면 실행 시점에서 객체에 동적으로 추가 기능을 부여할 수 있다.
  2. Single Responsibility Principle를 지킨다.
    • 각 Decorator는 자신의 특정 기능만을 담당하므로 단일 책임 원칙을 지키는 것이 가능해진다.

단점

  1. 코드 복잡성 증가
    • 많은 수의 작은 객체들이 생성되고, 이들이 상호 작용하게 되므로 코드가 복잡해질 수 있다.
  2. 디버깅 어려움
    • 런타임에 동적으로 기능이 추가되므로, 디버깅이 어려울 수 있다.

결론

  1. 자식 클래스는 하나의 부모 클래스만 상속받을 수 있다.
  2. 때문에 형제관계인 클래스 (A,B)가 가지고 있는 코드를 모두 필요로 할 때, 이 둘을 모두 상속받는 타입을 정의할 수는 없다.
    • 때문에 어느 한 쪽(A||B)을 상속을 받는 타입을 정의를 한 뒤, 다른 형제타입의 코드(B||A)를 중복으로 정의를 하거나
    • 아예 부모 클래스로부터 A와 B에 대한 기능을 모두 중복으로 정의하는 C를 정의해야 한다.
  • 이러한 상속의 문제를 해결하기 위한 아이디어가 Decorator pattern, 다른 말로는 wrapper pattern인 것이다.
  • Decorator pattern이 특정 상황을 효율적으로 풀어낼 수 있음은 분명합니다. 그러나 생각보다 이를 적극적으로 활용하는 apple의 framework은 찾아보기 힘들다. 왜냐하면 Swift는 POP(Protocol-Oriented Programming)를 통해 기능의 수평확장을 이미 언어에서부터 지원하고 있기 때문이다. 또한 Decorator pattern은 랩핑을 하는 과정이 반드시 필요하기 때문에, 랩핑에 대한 오버헤드가 발생하게 된다. 그렇기 때문에 많은 시간과 메모리를 소비하게 된다.
  • 이는 결국 순열과 조합의 문제로 이어질 수 있다. 즉 작은 블록들을 어떻게 뽑아서 큰 블록을 만들지를 미리 정의해놓는 아이디어가 필요하다는 것이다.

예시 코드

문제를 위한 예시

  • SNS 앱을 개발하려고 하는데, 이에 따라 텍스트, 이미지, 비디오를 서버에 전송할 필요가 생겼다. 이때 서버에서는 이것들을 각각의 API로 분리해서 제공할 계획이라고 한다.
class Uploader {
    private func setUp() {
        /*...*/
    }

    func upload() {
        setUp()
    }
}

class TextUploader: Uploader {
    override func upload() {
        super.upload()
        print("uploading text")
    }
}

class ImageUploader: Uploader {
    override func upload() {
        super.upload()
        print("uploading image")
    }
}

class VideoUploader: Uploader {
    override func upload() {
        super.upload()
        print("uploading video")
    }
}

decorator|600

  • 각각의 타입은 자신이 어떤 역할을 가지고 있는지 매우 분명했고, 운이 좋게도 버그가 없었다. 하지만 위 코드는 한번에 한 종류만 올릴 수 있기 때문에 수정이 요하다.
class VideoAndTextUploader: VideoUploader {
    override func upload() {
        super.upload()
        print("uploading text")
    }
}

class ImageAndTextUploader: ImageUploader {
    override func upload() {
        super.upload()
        print("uploading text")
    }
}
  • 위 코드처럼 수정하면 동시 업로드가 가능하다. 하지만 모든 조건을 다 확인해야되서 오히려 힘들어질수도 있다. 이에 따라 Decorator 패턴을 활용하면 된다.
  • 먼저 Uploader를 상속받는 DecoratedUploader라는 클래스를 정의하며, DecoratedUploader는 프로퍼티로 옵셔널인 업로더 타입을 가지고 있다.
  • 즉 DecoratedUploader는 그 자체로 하나의 Uploader이면서, 내부에 Uploader를 가질 수 있게 된다.
class Uploader {
    private func setUp() {
        /*...*/
  }

  func upload() {
    setUp()
  }
}

class DecoratedUploader: Uploader {
  var uploader: Uploader?

    init(_ uploader: Uploader? = nil) {
    self.uploader = uploader
  }

  override func upload() {
    super.upload()
    uploader?.upload()
  }
}

class TextUploader: DecoratedUploader {
    override func upload() {
        super.upload()
        print("uploading text")
    }
}

class ImageUploader: DecoratedUploader {
    override func upload() {
        super.upload()
        print("uploading image")
    }
}

class VideoUploader: DecoratedUploader {
    override func upload() {
        super.upload()
        print("uploading video")
    }
}

class FileUploader: DecoratedUploader {
    override func upload() {
        super.upload()
        print("uploading file")
    }
}

let textAndImageAndVideoUploader = TextUploader(ImageUploader(VideoUploader()))
let imageAndVideoUploader = ImageUploader(VideoUploader())
let everythingUplodaer = TextUploader(ImageUploader(VideoUploader(FileUploader())))

decorator|600

Decorator pattern을 활용한 http header 동적 추가

  • 네트워크 요청시, 헤더는 일반적으로 인증, 캐싱, Content-Type, Accept 등의 정보를 포함하고 있다. 이러한 요청 헤더는 서버에 대한 정보나 클라이언트가 어떤 응답을 원하는지 등을 알려준다.
  • Decorator 패턴을 사용하면, 네트워크 요청에 여러 가지 헤더를 동적으로 추가할 수 있다. 예를 들어, 기본 요청에 추가적으로 Authorization 헤더를 추가하거나, 다른 요청에는 Accept-Language 헤더를 추가하고 싶을 때, 각각을 별도의 Decorator로 구현하여 기능을 확장할 수 있다.
import UIKit

// 네트워크 요청을 표현하는 인터페이스 정의
protocol NetworkRequest {
    var headers: [String: String] { get }
}

// 기본 요청 클래스
class BasicRequest: NetworkRequest {
    var headers: [String : String] {
        return ["Content-Type":"application/json"]
    }
}

// NetworkRequest 인터페이스를 따르는 Decorator
class NetworkRequestDecorator: NetworkRequest {
    private let decoratedRequest: NetworkRequest
    
    init(_ decoratedRequest: NetworkRequest) {
        self.decoratedRequest = decoratedRequest
    }
    
    var headers: [String : String] {
        return decoratedRequest.headers
    }
}

// 이 Decorator를 상속받아 각각의 추가 헤더를 구현하는 클래스들을 정의
class AuthenticatedRequest: NetworkRequestDecorator {
    override var headers: [String: String] {
        var headers = super.headers
        headers["Authorization"] = "Bearer some_auth_token"
        return headers
    }
}

class AcceptLanguageRequest: NetworkRequestDecorator {
    override var headers: [String : String] {
        var headers = super.headers
        headers["Accept-Language"] = "ko-KR"
        return headers
    }
}

var request: NetworkRequest = BasicRequest()
print(request.headers)

request = AuthenticatedRequest(request)
print(request.headers)

request = AcceptLanguageRequest(request)
print(request.headers)

["Content-Type": "application/json"]
["Content-Type": "application/json", "Authorization": "Bearer some_auth_token"]
["Content-Type": "application/json", "Authorization": "Bearer some_auth_token", "Accept-Language": "en-US"]

출처(참고문헌)

코드

https://github.com/Jeon0976/ToyProject/tree/main/Coordinator/Coordinator

제가 학습한 내용을 요약하여 정리한 것입니다. 내용에 오류가 있을 수 있으며, 어떠한 피드백도 감사히 받겠습니다.

감사합니다.

0개의 댓글