데코레이터 패턴
의도
데코레이터는 객체들을 새로운 행동들을 포함한 특수 래퍼 객체들 내에 넣어서 위 행동들을 해당 객체들에 연결시키는 구조적 디자인 패턴입니다.
문제
당신이 알림 라이브러리를 만들고 있다고 상상해 보세요. 이 알림 라이브러리의 목적은 다른 프로그램들이 사용자들에게 중요한 이벤트들에 대해 알릴 수 있도록 하는 것입니다.
이 라이브러리의 초기 버전은 Notifier
(알림자) 클래스를 기반으로 했으며, 이 클래스에는 몇 개의 필드들, 하나의 생성자 그리고 단일 send
(전송) 메서드만 있었습니다. 이 메서드는 클라이언트로부터 메시지 인수를 받은 후 그 메세지를 알림자의 생성자를 통해 알림자에게 전달된 이메일 목록으로 보낼 수 있습니다. 또 클라이언트 역할을 한 타사 앱은 알림자 객체를 한번 생성하고 설정한 후 중요한 일이 발생할 때마다 사용하게 되어 있었습니다.
당신은 어느 시점에서 라이브러리 사용자들이 이메일 알림 이상을 기대한다는 것을 알게 됩니다. 그들 중 많은 사용자는 중요한 사안에 대한 SMS 문자 메시지를 받고 싶어 했고, 다른 사용자들은 페이스북 알림을 받고 싶어 했으며 기업 사용자들은 슬랙 알림을 받고 싶어 했습니다.
이는 표면상 별로 어렵지 않아 보입니다. 당신은 Notifier
(알림자) 클래스를 확장하고 추가 알림 메서드들을 새 자식 클래스들에 넣어 이제 클라이언트가 사용자가 원하는 알림 클래스를 인스턴스화하고 모든 추가 알림에 사용하도록 앱을 설계했습니다.
그런데 누군가 당신에게 물었습니다. '여러 유형의 알림을 한 번에 사용할 수는 없나요? 집에 불이라도 난다면 사용자들은 모든 채널에서 정보를 받고 싶어 할 겁니다.'
이 문제를 해결하기 위해 당신은 하나의 클래스 내에서 여러 알림 메서드를 합성한 특수 자식 클래스들을 만들었으나, 이 접근 방식은 라이브러리 코드뿐만 아니라 클라이언트 코드도 엄청나게 부풀릴 것이라는 사실이 금세 명백해졌습니다.
이제 당신은 알림 클래스들의 수가 지나치게 많아지지 않도록 알림 클래스들을 구성하는 다른 방법을 찾아야 합니다.
해결책
객체의 동작을 변경해야 할 때 가장 먼저 고려되는 방법은 클래스의 확장입니다. 그러나 상속에는 당신이 심각하게 주의해야 할 몇 가지 사항들이 있습니다.
- 상속은 정적입니다: 당신은 런타임(실행시간) 때 기존 객체의 행동을 변경할 수 없습니다. 당신은 전체 객체를 다른 자식 클래스에서 생성된 다른 객체로만 바꿀 수 있습니다.
- 자식 클래스는 하나의 부모 클래스만 가질 수 있습니다. 대부분 언어에서의 상속은 클래스가 동시에 여러 클래스의 행동을 상속하도록 허용하지 않습니다.
이러한 주의 사항을 극복하는 방법의 하나는 상속 대신 집합 관계 또는 합성 을 사용하는 것입니다. 두 대안 모두 거의 같은 방식으로 작동합니다: 집합 관계에서는 한 객체가 다른 객체에 대한 참조를 갖고 일부 작업을 위임하는 반면, 상속을 사용하면 객체 자체가 부모 클래스에서 행동을 상속한 후 해당 작업을 수행할 수 있습니다.
이 새로운 접근 방식을 사용하면 연결된 '도우미' 객체를 다른 객체로 쉽게 대체하여 런타임 때 컨테이너의 행동을 변경할 수 있습니다. 객체는 여러 클래스의 행동들을 사용할 수 있고, 여러 객체에 대한 참조들이 있으며 이 객체들에 모든 종류의 작업을 위임합니다. 집합 관계/합성은 데코레이터를 포함한 많은 디자인 패턴의 핵심 원칙입니다. 그러면 이제 다시 이 패턴에 대하여 살펴봅시다.
'래퍼'는 패턴의 주요 아이디어를 명확하게 표현하는 데코레이터 패턴의 별명입니다. 래퍼는 일부 대상 객체와 연결할 수 있는 객체입니다. 래퍼에는 대상 객체와 같은 메서드들의 집합이 포함되어 있으며, 래퍼는 자신이 받는 모든 요청을 대상 객체에 위임합니다. 그러나 래퍼는 이 요청을 대상에 전달하기 전이나 후에 무언가를 수행하여 결과를 변경할 수 있습니다.
그러면 간단한 래퍼는 언제 진정한 데코레이터가 될 수 있을까요? 앞서 언급했듯이 래퍼는 래핑된 객체와 같은 인터페이스를 구현합니다. 그러므로 클라이언트의 관점에서 이러한 객체들은 같습니다. 이제 래퍼의 참조 필드가 해당 인터페이스를 따르는 모든 객체를 받도록 하세요. 이렇게 하면 여러 래퍼로 객체를 포장해서 모든 래퍼들의 합성된 행동들을 객체에 추가할 수 있습니다.
이제 당신의 알림 라이브러리에서 기초 Notifier
클래스 내에 있는 간단한 이메일 알림 행동은 그대로 두고 다른 모든 알림 메서드들을 데코레이터로 바꾸어 봅시다.
클라이언트 코드는 기초 알림자 객체를 클라이언트의 요구사항들과 일치하는 데코레이터들의 집합으로 래핑해야 합니다. 위 결과 객체들은 스택으로 구성됩니다.
스택의 마지막 데코레이터는 실제로 클라이언트와 작업하는 객체입니다. 모든 데코레이터들은 기초 알림자와 같은 인터페이스를 구현하므로 나머지 클라이언트 코드는 자신이 '순수한' 알림자 객체와 작동하든 데코레이터로 장식된 알림자 객체와 함께 작동하든 상관하지 않습니다.
메시지 형식 지정 또는 수신자 리스트 작성과 같은 다른 행동들에도 같은 접근 방식을 적용할 수 있습니다. 클라이언트는 객체가 다른 객체들과 같은 인터페이스를 따르는 한 객체를 모든 사용자 지정 데코레이터로 장식할 수 있습니다.
실제상황 적용
옷을 입는 것은 데코레이터 패턴을 사용하는 예입니다. 당신은 추울 때 스웨터로 몸을 감쌉니다. 스웨터를 입어도 춥다면 위에 재킷을 입고, 또 비가 오면 비옷을 입습니다. 이 모든 옷은 기초 행동을 '확장'하지만, 당신의 일부가 아니기에 필요하지 않을 때마다 옷을 쉽게 벗을 수 있습니다.
구조
-
컴포넌트는 래퍼들과 래핑된 객체들 모두에 대한 공통 인터페이스를 선언합니다.
-
구상 컴포넌트는 래핑되는 객체들의 클래스이며, 그는 기본 행동들을 정의하고 해당 기본 행동들은 데코레이터들이 변경할 수 있습니다.
-
기초 데코레이터 클래스에는 래핑된 객체를 참조하기 위한 필드가 있습니다. 필드의 유형은 구상 컴포넌트들과 구상 데코레이터들을 모두 포함할 수 있도록 컴포넌트 인터페이스로 선언되어야 합니다. 그 후 기초 데코레이터는 모든 작업들을 래핑된 객체에 위임합니다.
-
구상 데코레이터들은 컴포넌트들에 동적으로 추가될 수 있는 추가 행동들을 정의합니다. 그들은 기초 데코레이터의 메서드를 오버라이드(재정의)하고 해당 행동을 부모 메서드를 호출하기 전이나 후에 실행합니다.
-
클라이언트는 아래에 언급한 데코레이터들이 컴포넌트 인터페이스를 통해 모든 객체와 작동하는 한 컴포넌트들을 여러 계층의 데코레이터들로 래핑할 수 있습니다.
의사코드
이 예시에서 데코레이터 패턴은 민감한 데이터를 해당 데이터를 실제로 사용하는 코드와 별도로 압축하고 암호화할 수 있도록 합니다.
이 애플리케이션은 데이터 소스 객체를 한 쌍의 데코레이터로 래핑합니다. 두 래퍼 모두 디스크에 데이터를 쓰고 읽는 방식들을 변경합니다.
-
데이터가 디스크에 기록되기 직전에 데코레이터들은 데이터를 암호화하고 압축합니다. 원래 클래스는 위 변경 사항에 대해 알지 못한 채 암호화되고 보호된 데이터를 파일에 씁니다.
-
데이터는 디스크에서 읽힌 직후 같은 데코레이터들을 거쳐 가며, 이 데코레이터들은 데이터의 압축을 풀고 디코딩합니다.
데코레이터와 데이터 소스 클래스는 같은 인터페이스를 구현하므로 클라이언트 코드에서 모두 상호 교환이 가능합니다.
적용
데코레이터 패턴은 이 객체들을 사용하는 코드를 훼손하지 않으면서 런타임에 추가 행동들을 객체들에 할당할 수 있어야 할 때 사용하세요.
데코레이터는 비즈니스 로직을 계층으로 구성하고, 각 계층에 데코레이터를 생성하고 런타임에 이 로직의 다양한 조합들로 객체들을 구성할 수 있도록 합니다. 이러한 모든 객체가 공통 인터페이스를 따르기 때문에 클라이언트 코드는 해당 모든 객체를 같은 방식으로 다룰 수 있습니다.
이 패턴은 상속을 사용하여 객체의 행동을 확장하는 것이 어색하거나 불가능할 때 사용하세요.
많은 프로그래밍 언어에는 클래스의 추가 확장을 방지하는 데 사용할 수 있는 final
키워드가 있습니다. Final 클래스의 경우 기존 행동들을 재사용할 수 있는 유일한 방법은 데코레이터 패턴을 사용하여 클래스를 자체 래퍼로 래핑하는 것입니다.
구현방법
-
당신의 비즈니스 도메인이 여러 선택적 계층으로 감싸진 기본 컴포넌트로 표시될 수 있는지 확인하세요.
-
기본 컴포넌트와 선택적 계층들 양쪽에 공통적인 메서드들이 무엇인지 파악하세요. 그곳에 컴포넌트 인터페이스를 만들고 해당 메서드들을 선언하세요.
-
구상 컴포넌트 클래스를 만든 후 그 안에 기초 행동들을 정의하세요.
-
기초 데코레이터 클래스를 만드세요. 이 클래스에는 래핑된 객체에 대한 참조를 저장하기 위한 필드가 있어야 합니다. 이 필드는 데코레이터들 및 구상 컴포넌트들과의 연결을 허용하기 위하여 컴포넌트 인터페이스 유형으로 선언하셔야 합니다. 기초 데코레이터는 모든 작업을 래핑된 객체에 위임해야 합니다.
-
모든 클래스들이 컴포넌트 인터페이스를 구현하도록 하세요.
-
기초 데코레이터를 확장하여 구상 데코레이터들을 생성하세요. 구상 데코레이터는 항상 부모 메서드 호출 전 또는 후에 행동들을 실행해야 합니다. (부모 메서드는 항상 래핑된 객체에 작업을 위임합니다).
-
데코레이터들을 만들고 이러한 데코레이터들을 클라이언트가 필요로 하는 방식으로 구성하는 일은 반드시 클라이언트 코드가 맡아야 합니다.
장단점
- 새 자식 클래스를 만들지 않고도 객체의 행동을 확장할 수 있습니다.
- 런타임에 객체들에서부터 책임들을 추가하거나 제거할 수 있습니다.
- 객체를 여러 데코레이터로 래핑하여 여러 행동들을 합성할 수 있습니다.
- 단일 책임 원칙. 다양한 행동들의 여러 변형들을 구현하는 모놀리식 클래스를 여러 개의 작은 클래스들로 나눌 수 있습니다.
- 래퍼들의 스택에서 특정 래퍼를 제거하기가 어렵습니다.
- 데코레이터의 행동이 데코레이터 스택 내의 순서에 의존하지 않는 방식으로 데코레이터를 구현하기가 어렵습니다.
- 계층들의 초기 설정 코드가 보기 흉할 수 있습니다.
다른 패턴과의 관계
-
어댑터는 기존 객체의 인터페이스를 변경하는 반면 데코레이터는 객체를 해당 객체의 인터페이스를 변경하지 않고 향상합니다. 또한 데코레이터는 어댑터를 사용할 때는 불가능한 재귀적 합성을 지원합니다.
-
어댑터는 다른 인터페이스를, 프록시는 같은 인터페이스를, 데코레이터는 향상된 인터페이스를 래핑된 객체에 제공합니다.
-
책임 연쇄 패턴과 데코레이터는 클래스 구조가 매우 유사합니다. 두 패턴 모두 실행을 일련의 객체들을 통해 전달할 때 재귀적인 합성에 의존하나, 몇 가지 결정적인 차이점이 있습니다.
책임 연쇄 패턴 핸들러들은 서로 독립적으로 임의의 작업을 실행할 수 있으며, 또한 해당 요청을 언제든지 더 이상 전달하지 않을 수 있습니다. 반면에 다양한 데코레이터들은 객체의 행동을 확장하며 동시에 이러한 행동을 기초 인터페이스와 일관되게 유지할 수 있습니다. 또한 데코레이터들은 요청의 흐름을 중단할 수 없습니다.
-
복합체 패턴 및 데코레이터는 둘 다 구조 다이어그램이 유사합니다. 왜냐하면 둘 다 재귀적인 합성에 의존하여 하나 또는 불특정 다수의 객체들을 정리하기 때문입니다.
데코레이터는 복합체 패턴과 비슷하지만, 자식 컴포넌트가 하나만 있습니다. 또 다른 중요한 차이점은 데코레이터는 래핑된 객체에 추가 책임들을 추가하는 반면 복합체 패턴은 자신의 자식들의 결과를 '요약'하기만 합니다.
그러나 패턴들은 서로 협력할 수도 있습니다: 데코레이터를 사용하여 복합체 패턴 트리의 특정 객체의 행동을 확장할 수 있습니다.
-
데코레이터 및 복합체 패턴을 많이 사용하는 디자인들은 프로토타입을 사용하면 종종 이득을 볼 수 있습니다. 프로토타입 패턴을 적용하면 복잡한 구조들을 처음부터 다시 건축하는 대신 복제할 수 있기 때문입니다.
-
데코레이터는 객체의 피부를 변경할 수 있고 전략 패턴은 객체의 내장을 변경할 수 있다고 비유할 수 있습니다.
-
데코레이터와 프록시의 구조는 비슷하나 이들의 의도는 매우 다릅니다. 두 패턴 모두 한 객체가 일부 작업을 다른 객체에 위임해야 하는 합성 원칙을 기반으로 합니다. 이 두 패턴의 차이점은 프록시는 일반적으로 자체적으로 자신의 서비스 객체의 수명 주기를 관리하는 반면 데코레이터의 합성은 항상 클라이언트에 의해 제어된다는 점입니다.