Notice
Recent Posts
Recent Comments
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Archives
Today
Total
관리 메뉴

DeFacto-Standard IT

Decorator Pattern 본문

Design Pattern/Structural Pattern

Decorator Pattern

defacto standard 2017. 9. 26. 08:09

1. 가정


 - 화면에 글씨를 출력하는 프로그램 작성

 - 기본출력 : '기본기능' 을 출력하며, 기본 기능이므로 무조건 모든 기능에서 처음 수행된다.

 - 옵션에 따라 '추가기능1', '추가기능2' 글씨를 출력할 수 있음

 - 기본기능을 기본으로 하되 추가기능의 조합이 가능해야함

 - 옵션 기능은 전부 수행할 수도 있고, 몇 개만 수행할 수도 있으며, 실행순서는 존재함

 - 실행 순서가 존재한다는 것은,  기본기능 -> 추가기능 1 -> 2와

   기본기능 -> 추가기능 2-> 1 모두 지원해야 한다는 의미이다.


2. Naive Code

 - DefaultPrint

public class DefaultPrint {
public void print() {
System.out.println("기본 기능");
}
}

기본기능을 수행하는 클래스


 - DefaultOption1Print

public class DefaultOption1Print extends DefaultPrint{
@Override
public void print() {
super.print();
System.out.println("추가기능 1");
}
}

기본기능 + 추가기능 1을 수행하는 클래스.


기본기능을 수행하기 위해 DefaultPrint를 상속받고, 이 클래스의 print()를 사용하여 기본기능 수행 후 자신만의 기능을 수행한다.


 - DefaultOption2Print

public class DefaultOption2Print extends DefaultPrint{
@Override
public void print() {
super.print();
System.out.println("추가기능 2");
}
}

 - DefaultOption12Print

public class DefaultOption12Print extends DefaultPrint{
@Override
public void print() {
super.print();
System.out.println("추가기능 1");
System.out.println("추가기능 2");
}
}

 - DefaultOption21Print

public class DefaultOption21Print extends DefaultPrint{
@Override
public void print() {
super.print();
System.out.println("추가기능 2");
System.out.println("추가기능 1");
}
}


3. 문제점

위 소스코드는 '1. 가정'에서 정의한 요구사항대로 동작한다.


필요에 따라 클래스를 호출하여 사용할 수 있다.


그러나 다음과 같은 요구사항이 추가된다고 가정하자.

 - 추가기능 3을 추가한다.


여기서 해야할 일은, 추가기능 3과 더불어 기존의 기능을 충족시키도록 클래스를 만드는 것이다.

만들어지는 클래스는 다음과 같으며, 소스코드는 따로 작성하지 않는다.


DefaultOption3Print

DefaultOption13Print

DefaultOption23Print

DefaultOption31Print

DefaultOption32Print

DefaultOption123Print

DefaultOption132Print

DefaultOption213Print

DefaultOption231Print

DefaultOption312Print

DefaultOption321Print


기본기능 + 추가기능 1 + 2 + 3을 조합하여 사용하는데 위와같은 클래스가 추가로 작성된다.


만들어지는 클래스의 갯수에 대해 계산을 해보자.

 - 기본기능 1개만 있는 경우의 클래스 갯수 : 1개

 - 기본기능 + 추가기능 1     2개만 있는 경우의 클래스 갯수 : 2개

 - 기본기능 + 추가기능 1,2   3개만 있는 경우의 클래스 갯수 : 5개

 - 기본기능 + 추가기능 1,2,3 4개만 있는 경우의 클래스 갯수 : 16개

...


추가기능이 하나 추가될 때 마다, 많은 경우의 수의 클래스가 발생한다.


이러한 문제가 발생하는 이유는, '상속'을 이용했기 때문이다. 이러한 구현방법은, 추가되는 기능의 조합별로 하위 클래스를 구현해야 하는 문제가 발생한다. 그리고, 순서 또한 존재하기 때문에 순서에 맞춘 하위 클래스 역시도 구현해야한다.


문제는 이뿐만이 아니다. 기존의 추가 기능의 변화가 생긴다면, 이 추가기능을 이용하는 모든 클래스의 내용을 수정하는 '산탄총 수술'을 해야한다.


예를들어서, '추가 기능 1' 이 아니라 '추가 기능 1 수행'이라는 문자열을 출력하도록 바뀐다고 한다면,

이를 출력하는 모든 하위 클래스의 내용을 수정해야하고, 이는 기능의 변경이 일어날 때마다 동일하다.


4. 해결방안


이러한 갯수가 기하급수적으로 늘어나는 경우와 산탄총 수술을 방지하기 위해서는,

추가기능 하나 당 한 클래스로 해결할 수 있어야 한다.


즉, 이 클래스들을 서로 조합하는 설계를 생각해볼 필요가 있다.


이렇게 한다면, 기하급수적으로 늘어나지도 않을 것이며,

클래스 하나만 변경하더라도 이를 참조하는 모든 클래스가 동일하게 수행될 것이다.


클래스들을 조합하기 위해서는 디커플링이 상당히 중요하다.

기능 하나하나를 정의하더라도, 이 모두를 포괄하는 추상적인 개념을 만들 필요가 있다. 이를 'Component'라고 한다.


추상적인 개념을 사용함으로써, 기본기능과 추가기능은 모두 비슷한 종류의 작업을 수행한다는 관점에서 바라보기 위함이다.


여기서 한가지 더 생각해야할 것이 있다.

추가기능은 분명 Component가 맞지만, Component와 추가기능 사이에는 중간에 다리를 하나 더 놓아야 한다. 이것을 'Decorator'라고 한다.


이미 추상화된 개념인 Component가 존재하는데 왜 굳이 추가기능만을 위한 추상적 개념을 추가해야 할까.


Component는 Interface이며, Decorator는 Absract Class이다.


결론부터 말하자면 이 두 요소의 쓰임새가 다르다.


Component (Interface)의 경우에는 특정 동작만을 수행하여야 한다는 것만을 의미한다. 즉, 로직을 일반화시켜서 어떠한 객체가 바인딩이 되더라도 해당 인터페이스를 구현한다면 수행할 수 있도록 구현하기 위한 것이다.


즉, Component는 기능에 대한 추상화는 맞지만, 객체의 조합은 수행할 수 없다.


Decorator 패턴은 객체를 '조합'하여 사용하는 패턴이다.


요구사항에서는 추가기능이 한 가지만 있는 것이 아니라, 여러 개의 추가기능이 조합되어 순서대로 수행될 수 있어야 한다.


하지만 이러한 추가기능이 여러개 존재한다면 '추가기능에 추가기능을 더하도록 하는 것'은 공통된 로직이라고 할 수 있다.

따라서, 이 Decorator들을 조합하는 것이 Decorator클래스의 역할이다.


이에 반해 기본기능은 ConcereteComponent에서 구현하며, 이는 추가기능의 조합에 상관없이 무조건 수행되어야 한다.

따라서, Component를 직접적으로 상속받아도 무관하다. 


추가기능의 경우는 옵션이다. 이는 동적으로 조합되어져야 한다는 것을 의미한다.

모든 추가기능은 순서에 관계없이, 선행되어야 하는 Component가 존재한다.


추가기능은 'ConcreteDecorator'로서 작성이 되며, Decorator 클래스를 상속받는다.


외부로부터 Component를 DI받아 super 클래스인 Decorator 클래스를 통해 세팅을 하며, 자신만의 로직 수행 하기 이전 super객체를 통해 지금까지 조합된 Component들에 대한 로직을 수행한다.


조합이 선행된 Component의 연산을 한 후에는, 해당 ConcreteDecorator에서 구현하는 로직을 수행한다.


5. Solution Code

 - Component

public interface Component {
public void decoratorDo();
}

'기능'을 의미하는 Component 인터페이스이다. 이는 기본기능과 추가기능을 추상화한 개념이다.


 - ConcreteComponent

// 기본 기능
public class ConcreteComponent implements Component {
public void decoratorDo() {
System.out.println("\tConcreteComponent# Do()");
}
}

ConcreteComponent는 기본적인 기능을 한다. 따라서 Component 인터페이스를 그대로 구현한다.


이는 ConcreteDecorator에서 super.operation()을 수행하면 ConcreteDecorator객체의 생성 시 인자로 넘어온 Component객체의 행동에 해당하는 operation()을 먼저 행동하게 하기 위함이다.


 - Decorator

//다양한 추가 기능에 대한 공통 클래스
public abstract class Decorator implements Component {
private Component component;

public Decorator(Component component) {
this.component = component;
}

public void decoratorDo() {
component.decoratorDo();
}
}

추가기능을 추상화한 클래스이다. 내부에 Component 레퍼런스가 있다.


생성자에서 Component를 DI받아서 로컬 레퍼런스인 Component에 바인딩한다.


'조합'이란, 결국 

Decorator의 Component내에 있는,

Decorator의 Component내에 있는,

Decorator의 Component내에 있는 ...  과 같이 계속 레퍼런스를 참조하여 '조합'하는 것이다.


 - ConcreteDecorator1, 2, 3

// 추가 기능 1
public class ConcreteDecorator1 extends Decorator {
public ConcreteDecorator1(Component component) {
super(component);
}

public void decoratorDo() {
super.decoratorDo();
concreteDecorator1Do();
}

private void concreteDecorator1Do() {
System.out.println("\tConcreteDecorator1# Do()");
}
}
// 추가 기능 2
public class ConcreteDecorator2 extends Decorator {
public ConcreteDecorator2(Component component) {
super(component);
}

public void decoratorDo() {
super.decoratorDo();
concreteDecorator2Do();
}

private void concreteDecorator2Do() {
System.out.println("\tConcreteDecorator2# Do()");
}
}
// 추가 기능 3
public class ConcreteDecorator3 extends Decorator {
public ConcreteDecorator3(Component component) {
super(component);
}

public void decoratorDo() {
super.decoratorDo();
concreteDecorator3Do();
}

private void concreteDecorator3Do() {
System.out.println("\tConcreteDecorator3# Do()");
}
}

ConcreteDecorator들은 Decorator 클래스로 캡슐화 되어있다. 클래스 하나 당 추가 기능 하나를 의미한다.

Decorator의 operation()은 abstract method로서 하위에서 구현해야 한다.


Decorator 클래스는 생성자에서, 인자로 넘어온 Component변수를 멤버 변수로 세팅시킨다.

ConcreteDecorator 역시 super 클래스의 생성자를 이용하고,


이를 통해 Decorator의 지역객체에 Component가 할당되므로 결국 지금까지 조합된

(정확히는 여러개의 Component들이 엮이고 엮인 모습) Component 를 수행할 수 있다.


super객체를 수행한 후, 자신이 담당하는 역할을 수행한다.


여러가지 ConcreteDecorator를 조합하고 싶다면 인자로서 새로운 Decorator를 할당하여 넘기면 된다.

각 ConcreteDecorator의 addedBehavior()는 자신이 수행할 행동을 기술하며, 모든 operation()의 시작부는 super.operation()으로 시작한다.

 

즉, 자신이 만들어지기 전에 이미 만들어진 Component객체가 인자로서 넘어오는데, 이 지역변수로 저장된 Component객체의 행동(super.operation())을 먼저 한 후에 자신이 할 행동(addedBehavior)을 하는 것이다.


자신의 addedBehavior()를 먼저 호출한 후 Component의 operation()를 호출하는 방식으로도 구현할 수 있다.


 - Client

public class Client {
public static void main(String[] args) {
Component component = new ConcreteComponent();

System.out.println("- 기본 -");
component.decoratorDo();

System.out.println("- 기본 -> 추가기능1 -");
component = new ConcreteDecorator1(component);
component.decoratorDo();

System.out.println("- 기본 -> 추가기능1 - 2");
component = new ConcreteDecorator2(component);
component.decoratorDo();

System.out.println("- 기본 -> 추가기능1 -> 2 -> 3 -");
component = new ConcreteDecorator3(component);
component.decoratorDo();

Component component2 = new ConcreteComponent();
System.out.println("- 기본 -> 추가기능2 -> 3 -> 1 -");
component2 = new ConcreteDecorator1(new ConcreteDecorator3(new ConcreteDecorator2(component2)));
component2.decoratorDo();

}
}


수행 결과는 다음과 같다.


6. Decorator Pattern

기본 기능에 추가할 수 있는 기능의 종류가 많은 경우에 각 추가 기능을 Decorator 클래스로 정의한 후 필요한 Decorator 객체를 조합함으로써 추가 기능의 조합을 설계하는 방식.


예를 들어 기본 기능에 4가지 추가 기능이 있다고 하자.

추가 기능의 모든 조합은, Decorator Pattern을 사용하면, 개별 추가 기능에 해당하는 Decorator 클래스 4개만 구현하고 개별 추가 기능을 객체의 형태로 조합함으로써 구현할 수 있다.


또한 프로그램을 실행하는 중에도 Decorator 객체의 조합이 가능하므로 필요한 추가 기능의 조합을 동적으로 생성하는 것도 가능하다


ConcreteComponent로 c가 정의되어 있고, ConcreteDecorator A의 객체 a와 이에 대한 데커레이터로 ConcreteDecoratorB의 객체 b가 있다고 가정한다. 즉, 다음과 같이 객체가 생성된다.


Component c = new ConcreteComponent();

Component a = new ConcreteDecoratorA(c);

Component b = new ConcreteDecoratorB(a);


Client가 객체 b의 operation()을 호출하면 객체 b가 가리키는 Component, 즉 ConcreteDecoratorA 객체 a의 operation()을 호출한다.

객체 a 역시 자신이 가리키는 Component, 즉 ConcreteComponent 객체 c의 operation()을 호출한 후 자신의 addedBehavior()를 호출.


객체 b 역시 객체 a의 operation() 호출 후 자신의 addedBehavior()를 호출한다.


이와 같은 순서는 addedBehavior()가 자신의 지역 Component 레퍼런스를 통해 동작한 후 호출되는 경우를 보여준다. 


또한 

Compoent component = new ConcreteDecorator3(

new ConcreteDecorator2(

new ConcreteDecorator1(

new ConcreteComponent())));


의 경우, 가장 먼저 ConcreteComponent 객체가 만들어지며, 이는 기본 기능을 수행하는 역할이다.


이를 ConcreteDecorator1객체를 생성할 때의 인자로 넘긴다. ConcreteDecorator1의 생성자의 내용은 super(component);에 해당한다.

ConcreteDecorator1은 Decorator를 상속받으므로 Decorator의 생성자를 실행한다. Decorator의 생성자의 내용은 인자로 넘어온 componet객체를 자신의 멤버 변수로 세팅하는 역할을 한다.


이 행동을 ConcreteDecorator3까지 반복하여 최종 Component객체를 리턴한다.


따라서 ConcreteDecorator3의 지역변수 component에는 ConcreteDecorator2 객체가,

따라서 ConcreteDecorator2의 지역변수 component에는 ConcreteDecorator1 객체가,

따라서 ConcreteDecorator1의 지역변수 component에는 ConcreteComponent 객체가 저장된다.


component.operation();을 호출하면

해당 super.operation(); addedBehavior();  2개의 문장이 호출된다.


따라서 ConcreteDecorator3의 operation()은 ConcreteDecorator2의 operation()을 수행한 후 자신의 addedBehavior()를 수행,

ConcreteDecorator2의 operation()은 ConcreteDecorator1의 operation()을 수행한 후 자신의 addedBehavior()를 수행,

ConcreteDecorator1의 operation()은 ConcreteComponent의 operation()을 수행한 후 자신의 addedBehavior()를 수행한다.

 

따라서 ConcreteComponent -> ConcreteDecorator 1 -> 2 -> 3 에 대한 기능이 순서대로 호출된다. 


Component로 추상화를 하였기 때문에, 순서는 섞거나 변경되어도 상관없다.


하지만 ConcreteComponent는 기본기능이므로, 반드시 Component객체 생성 시 최종인자로 들어가서 가장 먼저 생성이 되게 한 후 다른 ConcreteDecorator 객체의 인자로 넘겨야 한다.


- UML & Sequence Diagram




 - Component : 기본 기능을 뜻하는 ConcreteComponent와 추가 기능을 뜻하는 Decorator의 공통 기능을 정의한다. 즉, 클라이언트는 Component를 통해 실제 객체를 사용한다.

 - ConceteComponent : 기본 기능을 구현하는 클래스다.

 - Decorator : 많은 수가 존재하는 구체적인 Decorator의 공통 기능을 제공한다.

 - ConcreteDecorator A, B : Decorator의 하위 클래스로 기본 기능에 추가되는 개별적인 기능을 뜻한다.


7. 장단점


 장점 : 기능이 하나 추가될 때마다 클래스를 만드는 것은 동일하지만, 이 클래스들을 조합하여 사용하는 경우에는

엄청난 수의 클래스와 중복코드가 만들어지기 때문에 이를 방지할 수 있다.


 단점 : 

'Design Pattern > Structural Pattern' 카테고리의 다른 글

Facade Pattern (기본)  (4) 2018.02.16
Bridge Pattern (기본)  (3) 2018.02.16
Adapter Pattern  (0) 2018.02.16
Proxy Pattern  (0) 2018.02.16
Composite Pattern  (0) 2017.09.26
Comments