디자인 패턴/구조

장식자(Decorator)

쿠n크 2023. 12. 22. 09:30

의도

  • 객체에 동적으로 새로운 책임을 추가할 수 있게 함. 기능을 추가하려면, 서브클래스를 생성하는 것보다 융통성 있는 방법을 제공함.

 

다른 이름

  • 랩퍼(Wrapper)

 

언제 쓰는가?

  • 동적으로 또한 투명하게(transparent), 다시 말해 다른 객체에 영향을 주지 않고 개개의 객체에 새로운 책임을 추가하기 위해 사용함.
  • 제거될 수 있는 책임에 대해 사용함.
  • 실제 상속으로 서브클래스를 계속 만드는 방법이 실질적이지 못할 때 사용함. 너무 많은 수의 독립된 확장이 가능할 때 모든 조합을 지원하기 위해 이를 상속으로 해결하면 클래스 수가 폭발적으로 많아지게 됨.

 

구조

GoF의 디자인 패턴 중 P.244

  • Component : 동적으로 추가할 서비스를 가질 가능성이 있는 객체들에 대한 인터페이스
  • ConcreteComponent : 추가적인 서비스가 실제로 정의되어야 할 필요가 있는 객체
  • Decorator : Component 객체에 대한 참조자를 관리하면서 Component에 정의된 인터페이스를 만족하돌고 인터페이스를 정의
  • ConcreteDecorator : Component에 새롭게 추가할 서비스를 실재로 구현하는 클래스

 

협력 방법

  • Decorator는 자신의 Component 객체 쪽으로 요청을 전달. 요청 전달 전 및 전달 후에 자신만의 추가 연산을 선택적으로 수행할 수도 있음.

 

결과

  1. 단순한 상속보다 설계의 융통성을 더 많이 증대시킬 수 있음.
    1. 장식자 패턴은 새로운 행동을 추가할 수 있는 가장 효과적인 방법
    2. 장식자를 사용하면 장식자를 객체와 연결하거나 분리하는 작업을 통해 새로운 책임을 추가하거나 삭제하는 일이 런타임에 가능
  2. 클래스 계통의 상부측 클래스에 많은 기능이 누적되는 상황을 피할 수 있음.
    1. 장식자 패턴은 책임 추가 작업에서 "필요한 비용만 그때 지불하는" 방법을 제공함
    2. 예상하지 못한 특성들을 한꺼번에 다 개발하기 위해 고민하고 노력하기보다는 발견하지 못하고 누락된 서비스들을 Decorator 객체들을 통해 지속적으로 추가할 수 있음
  3. 장식자와 해당 그 장식자의 구성요소가 동일한 것은 아님.
    1. 장식자는 사용자에게 일관된 인터페이스를 제공하는 껍데기
  4. 장식자를 사용함으로써 작은 규모의 객체들이 많이 생김.
    1. 객체가 많다는 것이 클래스나 변수가 다르다는 것이 아님

 

구현 / 고려 사항

  1. 인터페이스 일치시키기 : Decorator 객체는 반드시 인터페이스 만족해야 함. ConcreteDecorator는 같은 부모 클래스를 상속해야 함.
  2. 추상 클래스로 정의되는 Decorator 클래스 생략하기 : 간혹 추상 클래스 Decorator를 설계할 필요가 없음. ConcreteDecorator와 합칠 수 있음.
  3. Component 클래스는 가벼운 무게를 유지하기 : 연산만 정의하고 변수는 정의하지 않는 것이 좋다는 의미. 
  4. 객체의 겉포장을 변경할 것인가, 속을 변경할 것인가 : 장식자는 외부를 변경하는 패턴. 내부를 변경하는 패턴은 전략패턴. Component가 무거운 클래스일 경우 전략 패턴이 더 나을 수 있음. 전략 패턴을 간단히 얘기하면 Component의 많은 책임 중 일부를 맡아 특정 책임에 대해서만 확실히 해결해 줄 수 있다고 할 수 있음.

 

예제 코드

  • 기본 피카츄 버전이 있고 이 피카츄 버전 외에 상시로 추가 혹은 제외하고 싶은 무거운 피카츄 버전이 있다고 예로 들겠습니다.
  • 무거운 피카츄는 공격 시 자신의 몸무게를 곱해서 데미지를 줄 수 있게 됩니다.
#include "Decorator1.h"
#include <iostream>

void PrintDamage(Pikachu& pikachu)
{
	std::cout << "Pikachu Damage : " << pikachu.GetDamage() << std::endl;
}

int main()
{
	Pikachu pikachu;
	PikachuDecorator_Heavy pikachu_heavy(&pikachu, 20.0f);

	PrintDamage(pikachu);
	PrintDamage(pikachu_heavy);

	return 0;
}

  • 기존 존재하는 피카츄 객체를 인자로 받아 무거운 피카츄 버전은 만들며, 몸무게 20을 추가로 속성을 추가했습니다.
  • 이 때 PrintDamage를 호출하는데 있어 무거운 피카츄도 인터페이스를 만족하기 때문에 호출할 수 있습니다.
//Decorator1.h

#pragma once

class Pikachu
{
public:
	Pikachu() {}

	virtual float GetDamage() { return attack_damage; }

private:
	float attack_damage = 20.0f;
};

class PikachuDecorator_Heavy : public Pikachu
{
public:
	PikachuDecorator_Heavy(Pikachu* pikachu, float weight) : pikachu(pikachu), weight(weight) {}

public:
	virtual float GetDamage() override { return pikachu->GetDamage() * weight; }

private:
	Pikachu* pikachu;
	float weight;
};
  • 이 패턴 예제에 의아할 수 있는 부분인데 Pikachu를 상속했으면서 멤버변수로 pikachu에 대한 포인터도 들고 있는 것이 저는 이상하게 와닿았습니다.
  • 결론적으로 Pikachu를 상속받은 것은 같은 인터페이스를 공유하기 위함이고, pikachu를 인자로 받는 것은 상속 객체가 아닌 인자로 받은 객체로부터 영향을 받겠다라는 의미로 해석했습니다.
  • PikachuDecorator_Heavy는 상속으로부터 얻은 Pikachu 객체의 프로퍼티들을 갖고 있을 것이며, 인자로 받은 pikachu 포인터 또한 유효할 것이기 때문에 어찌보면 2중으로 가리키는 구조이고, 그렇기 때문에 위에서 언급했듯이 Pikachu 클래스는 가벼워야 합니다.(멤버변수가 적어야 함, 아니라면 전략 패턴이 더 나음)
  • 또한 main에서의 관점에서 보면 pikachu를 이용하여 pikachu_heavy를 만든 시점에만 무거운 공격을 할 수 있게 됐고 pikachu_heavy가 소멸하여도 pikachu는 영향을 받지 않으며, 다시 평범한 공격을 할 수 있게 됩니다.