본문 바로가기
Concepts/Design Pattern

구조 패턴 - 장식자(Decorator)

by ocwokocw 2021. 5. 11.

- 참조: GoF의 디자인 패턴

- 다른 패턴과의 관계: https://refactoring.guru/design-patterns/decorator

- 장식자(Decorator)

아마 한번쯤은 들어봤을법한 랩퍼(Wrapper) 라고도 불리우는 패턴이다. 객체에 동적으로 새로운 책임을 추가할 수 있게 한다. 기능을 추가하려면 서브 클래스를 생성하는것보다 융통성 있는 방법을 제공한다.


- 시나리오

GUI 툴킷이 있다고 가정해보자. 모든 사용자 UI 요소에는 필요 없지만, 어떤 특정 사용자 UI 요소에만 스크롤링이나 테두리 같은 속성을 추가할 필요가 있다고 해보자.

 

Text 를 출력하는 서비스를 제공하는 TextView 클래스가 있다고 가정하자. 이 TextView에 스크롤 기능이나 두꺼운 테두리가 필요하다면 어떻게 해결해야 할까?

 

이런 문제를 해결하기 위한 직관적인 해결책은 상속이다. 이미 존재하는 클래스를 상속받고, 또 다른 클래스에서 테두리 속성을 상속받으면 이 서브 클래스 인스턴스는 테두리라는 속성을 갖는다. 이렇게 하면 문제는 해결되지만 만약 언제 어떻게 테두리를 장식해야할 지 제어해야한다면 난감한 상황이 펼쳐진다.


- 문제 해결과 패턴의 적용

모든 TextView가 아니라 특정 TextView 에만 스크롤이나 테두리 기능이 필요하다면, 상속보다는 장식자(Decorator) 패턴을 사용하는것이 좋다. 테두리를 추가해야하는 객체를 한번 더 감싸는 것이다. 장식자는 자신이 감싼 객체의 인터페이스를 동일하게 제공하므로 사용자에게는 장식자의 존재가 감춰진다. 또한 요청을 중간에 가로채기 때문에 전달 과정 앞뒤에 다른 작업들을 추가할 수 있다.

위의 객체 다이어그램은 BorderDecorator 와 ScrollDecorator 객체로 TextView를 복합하는 방법을 보여주고 있다.

 

위 그림은 클래스 다이어그램이다. 패턴의 참여자를 통해 다이어그램을 분석해보자

  • Component(VisualComponent): 비주얼 객체의 추상 클래스. 그리기와 이벤트 처리에 필요한 인터페이스를 정의한다.
  • ConcreteComponent(TextView): 추가적인 기능을 제공할 대상이다. 장식자 패턴은 TextView 에만 기능을 추가적으로 제공하는게 아니라 VisualComponent 의 서브클래스라면 모두 적용대상이 된다.
  • Decorator(Decorator): 단순하게 Draw 에 대한 요청을 자신이 갖는 요소에 전달한다. (component->Draw())
  • ConcreteDecorator(ScrollDecorator, BorderDecorator): Decorator의 서브 클래스로 특정 기능을 수행하는 메서드를 추가로 구현한다. 예를 들어 BorderDecorator의 경우 부모 클래스의 Decorator::Draw() 를 호출하고 자신의 특수한 기능인 DrawBorder() 연산을 호출해서 테두리를 그리도록 한다.

- 다른 패턴과의 관계

  • Adapter: Adapter는 인터페이스를 변환하지만 Decorator는 인터페이스의 변환없이 객체를 감싼다.
  • Composite: 비슷한 구조 다이어그램을 지녔다. 두 패턴 모두다 재귀 composition 형태를 갖는다. Decorator는 자식 component 가 1개인 Composite 패턴과 비슷하다. Decorator는 감싸진 객체에 추가적인 책임들을 더하는 반면 Composite는 단지 자식들의 결과를(연산을) 합산한다.

https://refactoring.guru/design-patterns/decorator 에서 위의 차이점 외에도 다른 패턴과 관계를 더 설명하고 있지만, 다루지 않은 패턴들도 같이 설명하고 있어서 모두 쓰지는 않았다. 해당 링크는 디자인패턴의 전반에 대해서도 다루고 있으므로, 영어에 익숙하다면 해당 링크를 참조하면 많은 도움이 된다.


- Java 예제

먼저 VisualComponent 와 TextView 를 구현해보자.

 

public abstract class VisualComponent {

	abstract public void draw();
}

public class TextView extends VisualComponent{

	@Override
	public void draw() {
		System.out.println("TextView draw()");
	}
}

 

아래는 Decorator 코드이다. 장식자의 대상(TextView) 이나 또 다른 장식자(이 클래스를 상속받은 클래스가 BorderDecorator 라면 다른 장식자인 ScrollDecorator)를 참조하는 VisualComponent 를 생성자 인자로 정의했다. draw() 메소드에서는 해당 component의 draw 연산을 호출해주었다.

 

public class Decorator extends VisualComponent{

	private VisualComponent component;
	
	public Decorator(VisualComponent component) {
		this.component = component;
	}
	
	@Override
	public void draw() {
		System.out.println("Thru the decorator draw()");
		component.draw();
	}
}

 

장식자의 구현체인 BorderDecorator와 ScrollDecorator 코드는 아래와 같이 작성하였다. ScrollDecorator와 BorderDecorator 모두 생성자 인자로 VisualComponent 를 받고 있다.

 

ScrollDecorator 클래스에서는 draw() 메소드에서는 super.draw()로 부모 클래스의 draw() 메소드를 호출하여 component.draw()를 호출하고 자신이 장식할 기능인 scrollTo() 메소드를 호출하였다. scrollTo는 draw() 메소드에서 기능을 추가적으로 장식하고 있으므로 굳이 접근제어자를 public 으로 선언할 필요는 없어서 private 로 하였다.

 

public class ScrollDecorator extends Decorator{

	public ScrollDecorator(VisualComponent component) {
		super(component);
	}

	private int scrollPosition = 0;
	
	public void draw() {
		super.draw();
		scrollTo();
	}
	
	private void scrollTo() {
		System.out.println("scrollTo: " + scrollPosition);
	}
}
public class BorderDecorator extends Decorator{

	public BorderDecorator(VisualComponent component) {
		super(component);
	}

	private int borderWidth = 1;
	
	public void draw() {
		super.draw();
		drawBorder();
	}
	
	private void drawBorder() {
		System.out.println("drawBorder: " + borderWidth);
	}
}

 

Test 는 아래와 같이 작성하였다. TextView 의 인스턴스를 생성하고, 이에 ScrollDecorator와 BorderDecorator 장식자를 추가하였다. 마지막으로 장식된 Decorator의 인스턴스인 borderDecorator 의 draw() 메소드를 호출하면 장식자의 대상인 TextView의 draw() 메소드부터 장식한 순서대로 호출이 된다.

 

public static void main(String[] args) {
		
		VisualComponent textView = new TextView();
		Decorator scrollDecorator = new ScrollDecorator(textView);
		Decorator borderDecorator = new BorderDecorator(scrollDecorator);
		
		borderDecorator.draw();
	}
Thru the decorator
Thru the decorator
TextView
scrollTo: 0
drawBorder: 1

댓글