봄맞이 세일

데코레이터 패턴

다음 이름으로도 불립니다: 래퍼(Wrapper), Decorator

의도

데코레이터는 객체들을 새로운 행동들을 포함한 특수 래퍼 객체들 내에 넣어서 위 행동들을 해당 객체들에 연결시키는 구조적 디자인 패턴입니다.

데코레이터 디자인 패턴

문제

당신이 알림 라이브러리를 만들고 있다고 상상해 보세요. 이 알림 라이브러리의 목적은 다른 프로그램들이 사용자들에게 중요한 이벤트들에 대해 알릴 수 있도록 하는 것입니다.

이 라이브러리의 초기 버전은 Notifier​(알림자) 클래스를 기반으로 했으며, 이 클래스에는 몇 개의 필드들, 하나의 생성자 그리고 단일 send​(전송) 메서드만 있었습니다. 이 메서드는 클라이언트로부터 메시지 인수를 받은 후 그 메세지를 알림자의 생성자를 통해 알림자에게 전달된 이메일 목록으로 보낼 수 있습니다. 또 클라이언트 역할을 한 타사 앱은 알림자 객체를 한번 생성하고 설정한 후 중요한 일이 발생할 때마다 사용하게 되어 있었습니다.

데코레이터 패턴을 적용하기 전의 라이브러리 구조

프로그램은 알림자 클래스를 사용하여 미리 정의된 이메일들의 집합에 중요한 이벤트에 대한 알림을 보낼 수 있습니다.

당신은 어느 시점에서 라이브러리 사용자들이 이메일 알림 이상을 기대한다는 것을 알게 됩니다. 그들 중 많은 사용자는 중요한 사안에 대한 SMS 문자 메시지를 받고 싶어 했고, 다른 사용자들은 페이스북 알림을 받고 싶어 했으며 기업 사용자들은 슬랙 알림을 받고 싶어 했습니다.

다른 알림 유형을 구현한 후의 라이브러리 구조

각 알림 유형은 알림자의 자식 클래스로 구현됩니다.

이는 표면상 별로 어렵지 않아 보입니다. 당신은 Notifier​(알림자) 클래스를 확장하고 추가 알림 메서드들을 새 자식 클래스들에 넣어 이제 클라이언트가 사용자가 원하는 알림 클래스를 인스턴스화하고 모든 추가 알림에 사용하도록 앱을 설계했습니다.

그런데 누군가 당신에게 물었습니다. '여러 유형의 알림을 한 번에 사용할 수는 없나요? 집에 불이라도 난다면 사용자들은 모든 채널에서 정보를 받고 싶어 할 겁니다.'

이 문제를 해결하기 위해 당신은 하나의 클래스 내에서 여러 알림 메서드를 합성한 특수 자식 클래스들을 만들었으나, 이 접근 방식은 라이브러리 코드뿐만 아니라 클라이언트 코드도 엄청나게 부풀릴 것이라는 사실이 금세 명백해졌습니다.

클래스 조합들을 생성한 후의 라이브러리 구조

자식 클래스들의 합성으로 인한 클래스 수의 폭발

이제 당신은 알림 클래스들의 수가 지나치게 많아지지 않도록 알림 클래스들을 구성하는 다른 방법을 찾아야 합니다.

해결책

객체의 동작을 변경해야 할 때 가장 먼저 고려되는 방법은 클래스의 확장입니다. 그러나 상속에는 당신이 심각하게 주의해야 할 몇 가지 사항들이 있습니다.

  • 상속은 정적입니다: 당신은 런타임​(실행시간) 때 기존 객체의 행동을 변경할 수 없습니다. 당신은 전체 객체를 다른 자식 클래스에서 생성된 다른 객체로만 바꿀 수 있습니다.
  • 자식 클래스는 하나의 부모 클래스만 가질 수 있습니다. 대부분 언어에서의 상속은 클래스가 동시에 여러 클래스의 행동을 상속하도록 허용하지 않습니다.

이러한 주의 사항을 극복하는 방법의 하나는 대신 또는  을 사용하는 것입니다. 두 대안 모두 거의 같은 방식으로 작동합니다: 집합 관계에서는 한 객체가 다른 객체에 대한 일부 작업을 위임하는 반면, 상속을 사용하면 객체 자체가 부모 클래스에서 행동을 상속한 후 해당 작업을 .

이 새로운 접근 방식을 사용하면 연결된 '도우미' 객체를 다른 객체로 쉽게 대체하여 런타임 때 컨테이너의 행동을 변경할 수 있습니다. 객체는 여러 클래스의 행동들을 사용할 수 있고, 여러 객체에 대한 참조들이 있으며 이 객체들에 모든 종류의 작업을 위임합니다. 집합 관계/합성은 데코레이터를 포함한 많은 디자인 패턴의 핵심 원칙입니다. 그러면 이제 다시 이 패턴에 대하여 살펴봅시다.

상속과 집합 관계의 차이점.

상속과 집합 관계의 차이점.

'래퍼'는 패턴의 주요 아이디어를 명확하게 표현하는 데코레이터 패턴의 별명입니다. 는 일부 객체와 연결할 수 있는 객체입니다. 래퍼에는 대상 객체와 같은 메서드들의 집합이 포함되어 있으며, 래퍼는 자신이 받는 모든 요청을 대상 객체에 위임합니다. 그러나 래퍼는 이 요청을 대상에 전달하기 전이나 후에 무언가를 수행하여 결과를 변경할 수 있습니다.

그러면 간단한 래퍼는 언제 진정한 데코레이터가 될 수 있을까요? 앞서 언급했듯이 래퍼는 래핑된 객체와 같은 인터페이스를 구현합니다. 그러므로 클라이언트의 관점에서 이러한 객체들은 같습니다. 이제 래퍼의 참조 필드가 해당 인터페이스를 따르는 모든 객체를 받도록 하세요. 이렇게 하면 여러 래퍼로 객체를 포장해서 모든 래퍼들의 합성된 행동들을 객체에 추가할 수 있습니다.

이제 당신의 알림 라이브러리에서 기초 Notifier 클래스 내에 있는 간단한 이메일 알림 행동은 그대로 두고 다른 모든 알림 메서드들을 데코레이터로 바꾸어 봅시다.

데코레이터 패턴을 사용한 해결책

다양한 알림 메서드들이 데코레이터가 됩니다.

클라이언트 코드는 기초 알림자 객체를 클라이언트의 요구사항들과 일치하는 데코레이터들의 집합으로 래핑해야 합니다. 위 결과 객체들은 스택으로 구성됩니다.

앱들은 알림 데코레이터들의 복잡한 스택들을 설정할 수 있습니다

앱들은 알림 데코레이터들의 복잡한 스택들을 설정할 수 있습니다.

스택의 마지막 데코레이터는 실제로 클라이언트와 작업하는 객체입니다. 모든 데코레이터들은 기초 알림자와 같은 인터페이스를 구현하므로 나머지 클라이언트 코드는 자신이 '순수한' 알림자 객체와 작동하든 데코레이터로 장식된 알림자 객체와 함께 작동하든 상관하지 않습니다.

메시지 형식 지정 또는 수신자 리스트 작성과 같은 다른 행동들에도 같은 접근 방식을 적용할 수 있습니다. 클라이언트는 객체가 다른 객체들과 같은 인터페이스를 따르는 한 객체를 모든 사용자 지정 데코레이터로 장식할 수 있습니다.

실제상황 적용

데코레이터 패턴 예시

여러 벌의 옷을 입으면 복합 효과를 얻을 수 있습니다.

옷을 입는 것은 데코레이터 패턴을 사용하는 예입니다. 당신은 추울 때 스웨터로 몸을 감쌉니다. 스웨터를 입어도 춥다면 위에 재킷을 입고, 또 비가 오면 비옷을 입습니다. 이 모든 옷은 기초 행동을 '확장'하지만, 당신의 일부가 아니기에 필요하지 않을 때마다 옷을 쉽게 벗을 수 있습니다.

구조

데코레이터 디자인 패턴 구조데코레이터 디자인 패턴 구조
  1. 컴포넌트는 래퍼들과 래핑된 객체들 모두에 대한 공통 인터페이스를 선언합니다.

  2. 구상 컴포넌트는 래핑되는 객체들의 클래스이며, 그는 기본 행동들을 정의하고 해당 기본 행동들은 데코레이터들이 변경할 수 있습니다.

  3. 기초 데코레이터 클래스에는 래핑된 객체를 참조하기 위한 필드가 있습니다. 필드의 유형은 구상 컴포넌트들과 구상 데코레이터들을 모두 포함할 수 있도록 컴포넌트 인터페이스로 선언되어야 합니다. 그 후 기초 데코레이터는 모든 작업들을 래핑된 객체에 위임합니다.

  4. 구상 데코레이터들은 컴포넌트들에 동적으로 추가될 수 있는 추가 행동들을 정의합니다. 그들은 기초 데코레이터의 메서드를 오버라이드​(재정의)​하고 해당 행동을 부모 메서드를 호출하기 전이나 후에 실행합니다.

  5. 클라이언트는 아래에 언급한 데코레이터들이 컴포넌트 인터페이스를 통해 모든 객체와 작동하는 한 컴포넌트들을 여러 계층의 데코레이터들로 래핑할 수 있습니다.

의사코드

이 예시에서 데코레이터 패턴은 민감한 데이터를 해당 데이터를 실제로 사용하는 코드와 별도로 압축하고 암호화할 수 있도록 합니다.

데코레이터 패턴 구조 예시

암호화 및 압축화 데코레이터들의 예.

이 애플리케이션은 데이터 소스 객체를 한 쌍의 데코레이터로 래핑합니다. 두 래퍼 모두 디스크에 데이터를 쓰고 읽는 방식들을 변경합니다.

  • 데이터가 디스크에 기록되기 직전에 데코레이터들은 데이터를 암호화하고 압축합니다. 원래 클래스는 위 변경 사항에 대해 알지 못한 채 암호화되고 보호된 데이터를 파일에 씁니다.

  • 데이터는 디스크에서 읽힌 직후 같은 데코레이터들을 거쳐 가며, 이 데코레이터들은 데이터의 압축을 풀고 디코딩합니다.

데코레이터와 데이터 소스 클래스는 같은 인터페이스를 구현하므로 클라이언트 코드에서 모두 상호 교환이 가능합니다.

// 컴포넌트 인터페이스는 데코레이터들이 변경할 수 있는 작업들을 정의합니다.
interface DataSource is
    method writeData(data)
    method readData():data

// 구상 컴포넌트들은 작업들에 대한 디폴트 구현들을 제공합니다. 프로그램에는 이러한
// 클래스들의 여러 변형이 있을 수 있습니다.
class FileDataSource implements DataSource is
    constructor FileDataSource(filename) { ... }

    method writeData(data) is
        // 파일에 데이터를 씁니다.

    method readData():data is
        // 파일에서 데이터를 읽으세요.

// 기초 데코레이터 클래스는 다른 컴포넌트들과 같은 인터페이스를 따릅니다. 이
// 클래스의 주목적은 모든 구상 데코레이터에 대한 래핑 인터페이스를 정의하는
// 것입니다. 래핑 코드의 디폴트 구현에는 래핑된 컴포넌트를 저장하기 위한 필드와
// 이를 초기화하는 수단들이 포함될 수 있습니다.
class DataSourceDecorator implements DataSource is
    protected field wrappee: DataSource

    constructor DataSourceDecorator(source: DataSource) is
        wrappee = source

    // 기초 데코레이터는 단순히 모든 작업을 래핑된 컴포넌트에 위임합니다. 구상
    // 데코레이터들에는 추가 행동들이 추가될 수 있습니다.
    method writeData(data) is
        wrappee.writeData(data)

    // 구상 데코레이터들은 래핑된 객체를 직접 호출하는 대신 부모의 작업 구현을
    // 호출할 수 있습니다. 이 접근 방식은 데코레이터 클래스들의 확장을
    // 단순화합니다.
    method readData():data is
        return wrappee.readData()

// 구상 데코레이터는 래핑된 객체에 메서드를 호출해야 하지만 결과에 자기 것을 뭔가를
// 추가할 수 있습니다. 데코레이터들은 래핑된 객체에 대한 호출 전 또는 후에 추가된
// 행동을 실행할 수 있습니다.
class EncryptionDecorator extends DataSourceDecorator is
    method writeData(data) is
        // 1. 전달된 데이터를 암호화합니다.
        // 2. 암호화된 데이터를 wrapee(래핑된)​의 writeData(데이터 쓰기)
        // 메서드에 전달합니다.

    method readData():data is
        // 1. wrapee(래핑된)​의 readData 메서드에서 데이터를 가져옵니다.
        // 2. 암호화되어 있다면 암호 해독을 시도하세요.
        // 3. 결과를 반환하세요.

// 여러 계층의 데코레이터들로 객체들을 래핑할 수 있습니다.
class CompressionDecorator extends DataSourceDecorator is
    method writeData(data) is
        // 1. 전달된 데이터를 압축하세요.
        // 2. 압축된 데이터를 wrapee(래핑된)​의 writeData 메서드에
        // 전달하세요.

    method readData():data is
        // 1. wrappee(래핑된)​의 readData(데이터 읽기) 메서드에서 데이터를
        // 가져오세요.
        // 2. 압축되어 있으면 압축을 풀어보세요.
        // 3. 결과를 반환하세요.


// 옵션 1. 데코레이터 조립의 간단한 예시.
class Application is
    method dumbUsageExample() is
        source = new FileDataSource("somefile.dat")
        source.writeData(salaryRecords)
        // 대상 파일은 일반 데이터로 작성되었습니다.

        source = new CompressionDecorator(source)
        source.writeData(salaryRecords)
        // 대상 파일은 압축된 데이터로 작성되었습니다.

        source = new EncryptionDecorator(source)
        // 이제 소스 변수에 다음이 포함됩니다. 암호화 > 압축 > 파일 데이터 소스
        source.writeData(salaryRecords)
        // 파일은 압축 및 암호화된 데이터로 작성되었습니다.


// 옵션 2. 외부 데이터 소스를 사용하는 클라이언트 코드. SalaryManager 객체들은
// 데이터 저장 세부 사항들을 알지도 못하고 신경 쓰지도 않습니다. 이들은 앱 설정
// 프로그램에서 받은 사전 구성된 데이터 소스로 작업합니다.
class SalaryManager is
    field source: DataSource

    constructor SalaryManager(source: DataSource) { ... }

    method load() is
        return source.readData()

    method save() is
        source.writeData(salaryRecords)
    // …다른 유용한 메서드들…


// 앱은 설정 또는 환경에 따라 런타임 때 다양한 데코레이터 스택들을 조합할 수
// 있습니다.
class ApplicationConfigurator is
    method configurationExample() is
        source = new FileDataSource("salary.dat")
        if (enabledEncryption)
            source = new EncryptionDecorator(source)
        if (enabledCompression)
            source = new CompressionDecorator(source)

        logger = new SalaryManager(source)
        salary = logger.load()
    // …

적용

데코레이터 패턴은 이 객체들을 사용하는 코드를 훼손하지 않으면서 런타임에 추가 행동들을 객체들에 할당할 수 있어야 할 때 사용하세요.

데코레이터는 비즈니스 로직을 계층으로 구성하고, 각 계층에 데코레이터를 생성하고 런타임에 이 로직의 다양한 조합들로 객체들을 구성할 수 있도록 합니다. 이러한 모든 객체가 공통 인터페이스를 따르기 때문에 클라이언트 코드는 해당 모든 객체를 같은 방식으로 다룰 수 있습니다.

이 패턴은 상속을 사용하여 객체의 행동을 확장하는 것이 어색하거나 불가능할 때 사용하세요.

많은 프로그래밍 언어에는 클래스의 추가 확장을 방지하는 데 사용할 수 있는 final 키워드가 있습니다. Final 클래스의 경우 기존 행동들을 재사용할 수 있는 유일한 방법은 데코레이터 패턴을 사용하여 클래스를 자체 래퍼로 래핑하는 것입니다.

구현방법

  1. 당신의 비즈니스 도메인이 여러 선택적 계층으로 감싸진 기본 컴포넌트로 표시될 수 있는지 확인하세요.

  2. 기본 컴포넌트와 선택적 계층들 양쪽에 공통적인 메서드들이 무엇인지 파악하세요. 그곳에 컴포넌트 인터페이스를 만들고 해당 메서드들을 선언하세요.

  3. 구상 컴포넌트 클래스를 만든 후 그 안에 기초 행동들을 정의하세요.

  4. 기초 데코레이터 클래스를 만드세요. 이 클래스에는 래핑된 객체에 대한 참조를 저장하기 위한 필드가 있어야 합니다. 이 필드는 데코레이터들 및 구상 컴포넌트들과의 연결을 허용하기 위하여 컴포넌트 인터페이스 유형으로 선언하셔야 합니다. 기초 데코레이터는 모든 작업을 래핑된 객체에 위임해야 합니다.

  5. 모든 클래스들이 컴포넌트 인터페이스를 구현하도록 하세요.

  6. 기초 데코레이터를 확장하여 구상 데코레이터들을 생성하세요. 구상 데코레이터는 항상 부모 메서드 호출 전 또는 후에 행동들을 실행해야 합니다. (부모 메서드는 항상 래핑된 객체에 작업을 위임합니다).

  7. 데코레이터들을 만들고 이러한 데코레이터들을 클라이언트가 필요로 하는 방식으로 구성하는 일은 반드시 클라이언트 코드가 맡아야 합니다.

장단점

  • 새 자식 클래스를 만들지 않고도 객체의 행동을 확장할 수 있습니다.
  • 런타임에 객체들에서부터 책임들을 추가하거나 제거할 수 있습니다.
  • 객체를 여러 데코레이터로 래핑하여 여러 행동들을 합성할 수 있습니다.
  • . 다양한 행동들의 여러 변형들을 구현하는 모놀리식 클래스를 여러 개의 작은 클래스들로 나눌 수 있습니다.
  • 래퍼들의 스택에서 특정 래퍼를 제거하기가 어렵습니다.
  • 데코레이터의 행동이 데코레이터 스택 내의 순서에 의존하지 않는 방식으로 데코레이터를 구현하기가 어렵습니다.
  • 계층들의 초기 설정 코드가 보기 흉할 수 있습니다.

다른 패턴과의 관계

  • 어댑터는 기존 객체의 인터페이스를 변경하는 반면 데코레이터는 객체를 해당 객체의 인터페이스를 변경하지 않고 향상합니다. 또한 를 사용할 때는 불가능한 재귀적 합성을 지원합니다.

  • 어댑터는 다른 인터페이스를, 프록시는 같은 인터페이스를, 데코레이터는 향상된 인터페이스를 래핑된 객체에 제공합니다.

  • 책임 연쇄 패턴과 데코레이터는 클래스 구조가 매우 유사합니다. 두 패턴 모두 실행을 일련의 객체들을 통해 전달할 때 재귀적인 합성에 의존하나, 몇 가지 결정적인 차이점이 있습니다.

    패턴 핸들러들은 서로 독립적으로 임의의 작업을 실행할 수 있으며, 또한 해당 요청을 언제든지 더 이상 전달하지 않을 수 있습니다. 반면에 다양한 은 객체의 행동을 확장하며 동시에 이러한 행동을 기초 인터페이스와 일관되게 유지할 수 있습니다. 또한 데코레이터들은 요청의 흐름을 중단할 수 없습니다.

  • 복합체 패턴 및 데코레이터는 둘 다 구조 다이어그램이 유사합니다. 왜냐하면 둘 다 재귀적인 합성에 의존하여 하나 또는 불특정 다수의 객체들을 정리하기 때문입니다.

    과 비슷하지만, 자식 컴포넌트가 하나만 있습니다. 또 다른 중요한 차이점은 는 래핑된 객체에 추가 책임들을 추가하는 반면 은 자신의 자식들의 결과를 '요약'하기만 합니다.

    그러나 패턴들은 서로 협력할 수도 있습니다: 를 사용하여 트리의 특정 객체의 행동을 확장할 수 있습니다.

  • 데코레이터복합체 패턴을 많이 사용하는 디자인들은 프로토타입을 사용하면 종종 이득을 볼 수 있습니다. 프로토타입 패턴을 적용하면 복잡한 구조들을 처음부터 다시 건축하는 대신 복제할 수 있기 때문입니다.

  • 데코레이터는 객체의 피부를 변경할 수 있고 전략 패턴은 객체의 내장을 변경할 수 있다고 비유할 수 있습니다.

  • 데코레이터프록시의 구조는 비슷하나 이들의 의도는 매우 다릅니다. 두 패턴 모두 한 객체가 일부 작업을 다른 객체에 위임해야 하는 합성 원칙을 기반으로 합니다. 이 두 패턴의 차이점은 는 일반적으로 자체적으로 자신의 서비스 객체의 수명 주기를 관리하는 반면 의 합성은 항상 클라이언트에 의해 제어된다는 점입니다.

코드 예시

C#으로 작성된 데코레이터 C++로 작성된 데코레이터 Go로 작성된 데코레이터 자바로 작성된 데코레이터 PHP로 작성된 데코레이터 파이썬으로 작성된 데코레이터 루비로 작성된 데코레이터 러스트로 작성된 데코레이터 스위프트로 작성된 데코레이터 타입스크립트로 작성된 데코레이터