객체에 장식을 더해 각각의 목적에 맞는 객체로 만들어 가는 패턴을 Decorator 패턴이라고 한다. 여기서 장식은 추가적인 기능을 의미한다.
public abstract class Display {
public abstract int getColumns(); // 가로 문자 수를 얻는다
public abstract int getRows(); // 세로 행수를 얻는다
public abstract String getRowText(int row); // row행째 문자열을 얻는다
// 모든 행을 표시한다
public void show() {
for (int i = 0; i < getRows(); i++) {
System.out.println(getRowText(i));
}
}
}
위와 같이 여러 행으로 만들어진 문자열을 표시하는 추상클래스가 있다고 하고, 아래에 문자열을 디스플레이하는 클래스를 만들었다고 생각해보자.
public class StringDisplay extends Display {
private String string; // 표시 문자열
public StringDisplay(String string) {
this.string = string;
}
@Override
public int getColumns() {
return string.length();
}
@Override
public int getRows() {
return 1; // 행수는 1
}
@Override
public String getRowText(int row) {
if (row != 0) {
throw new IndexOutOfBoundsException();
}
return string;
}
}
하지만, 이렇게 보니 좀 심심하다. 이 객체를 장식을 하고 싶어졌고, '장식틀'을 나타내는 추상 클래스를 만들자. 아래와 같이 장식틀을 나타내는 추상클래스가 Display를 상속하여 Display의 모든 기능을 갖도록 한다.
public abstract class Border extends Display {
protected Display display; // 이 장식틀이 감싸는 '내용물'
protected Border(Display display) { // 인스턴스 생성 시 '내용물'을 인수로 지정
this.display = display;
}
}
추상 클래스 장식틀(Border)을 상속해 이번에는 문자열 양쪽에 장식 문자를 붙이는 클래스를 만들자. 여기서 중요한 점은 장식틀(Border)를 상속했지만, Border가 Display를 상속 중이므로 Display의 메소드를 오버라이드하게 된다. 그리고 중요한 점은 super(display)를 통해 이 SideBorder가 Display 객체를 field로 갖도록 한다.
public class SideBorder extends Border {
private char borderChar; // 장식 문자
// 내용물이 될 Display와 장식 문자를 지정
public SideBorder(Display display, char ch) {
super(display);
this.borderChar = ch;
}
@Override
public int getColumns() {
// 문자 수는 내용물의 양쪽에 장식 문자만큼 더한 것
return 1 + display.getColumns() + 1;
}
@Override
public int getRows() {
// 행수는 내용물의 행수와 같다
return display.getRows();
}
@Override
public String getRowText(int row) {
// 지정 행의 내용은 내용물의 지정 행 양쪽에 장식 문자를 붙인 것
return borderChar + display.getRowText(row) + borderChar;
}
}
위와 같이 구성한 후, 새로운 SideBorder 객체를 생성하고 show()에서 getRows()를 부르기 때문에 -> SideBorder.getRows() -> StringDisplay.getRows()를 실행하게 된다. 이렇게 각 만들어진 객체의 순서에 따라 String을 불러오게 된다.
public static void main(String[] args) {
Display b1 = new StringDisplay("Hello, world.");
Display b2 = new SideBorder(b1, '#');
b2.show();
}
// 결과 : #Hello, world.#
Component의 역할 : 기능을 추가할 때 핵심이 되는 역할이다. 이 Component는 예제 프로그램에서 Display이며, 장식하기 전 객체의 틀로 볼 수 있다.
ConcreteComponent의 역할 : 이 Component는 예제 프로그램에서 StringDisplay이며, 실제로 꾸미려는 대상 객체라고 볼 수 있을 것 같다. 예제 프로그램에서는 String-Display이다.
Decorator(장식자)의 역할 : Component와 같은 인터페이스를 가지며, 이 장식자는 Component 객체를 가진다. 이 역할은 자신이 장식할 대상을 위임을 통해 알고 있다. 예제 프로그램에서는 Border 클래스이다.
ConcreteDecorator(구체적인 장식자)의 역할 : 구체적인 Decorator이며, 예제 프로그램에서는 SideBorder이다.
내용물을 바꾸지 않고 기능을 추가할 수 있다.
위의 예제에서 보다 싶이, Decorator를 추가할 때, StringDisplay를 전혀 수정하지 않고, 기능을 추가한 것을 보았다. 이처럼, 내용물은 변경하지 않고, 기능을 추가할 수 있도록 해준다. Decorator 패턴에서도 위임을 사용하여 이를 처리한다.
동적으로 기능을 추가할 수 있다.
Decorator 패턴에서 사용되는 위임은 클래스 사이를 동적으로 결합하기 때문에, 프레임워크의 소스를 변경하지 않고 객체의 관계를 변경한 새로운 객체를 만들 수 있다. 런타임에 객체들에서부터 책임들을 추가하거나 제거할 수 있다.
단순한 구성이어도 다양한 기능을 추가할 수 있다.
다양한 요구에 대응하기에 적합하다. 여러가지로 데코레이터를 만들어내는데 용이하다.
Reader reader = new LineNumberReader(new BufferedReader(new FileReader("data")));
reader.read();
가장 흔히 볼 수 있는 Decorator 패턴을 적용한 예이다. 위와 같이 FileReader에서 데코레이터 패턴을 통해 여러번 감싸게 되면 read()를 하는데 버퍼링 기능과 행 관리 기능을 추가적으로 행하게 된다. 추가적으로, 구체적인 클래스인 LineNumberReader와 같은 클래스를 받아서 사용한다면 추가적인 기능을 사용할 수 있게 된다. ex) reader.getLineNumber();
참조 : Java 언어로 배우는 디자인 패턴 입문