디자인패턴 (Decorator)

백종현·2023년 5월 23일
0

Decorator 패턴

객체에 장식을 더해 각각의 목적에 맞는 객체로 만들어 가는 패턴을 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.#

Decorator 패턴의 요소

Component의 역할 : 기능을 추가할 때 핵심이 되는 역할이다. 이 Component는 예제 프로그램에서 Display이며, 장식하기 전 객체의 틀로 볼 수 있다.
ConcreteComponent의 역할 : 이 Component는 예제 프로그램에서 StringDisplay이며, 실제로 꾸미려는 대상 객체라고 볼 수 있을 것 같다. 예제 프로그램에서는 String-Display이다.
Decorator(장식자)의 역할 : Component와 같은 인터페이스를 가지며, 이 장식자는 Component 객체를 가진다. 이 역할은 자신이 장식할 대상을 위임을 통해 알고 있다. 예제 프로그램에서는 Border 클래스이다.
ConcreteDecorator(구체적인 장식자)의 역할 : 구체적인 Decorator이며, 예제 프로그램에서는 SideBorder이다.

왜 Decorator를 사용할까?

  1. 내용물을 바꾸지 않고 기능을 추가할 수 있다.
    위의 예제에서 보다 싶이, Decorator를 추가할 때, StringDisplay를 전혀 수정하지 않고, 기능을 추가한 것을 보았다. 이처럼, 내용물은 변경하지 않고, 기능을 추가할 수 있도록 해준다. Decorator 패턴에서도 위임을 사용하여 이를 처리한다.

  2. 동적으로 기능을 추가할 수 있다.
    Decorator 패턴에서 사용되는 위임은 클래스 사이를 동적으로 결합하기 때문에, 프레임워크의 소스를 변경하지 않고 객체의 관계를 변경한 새로운 객체를 만들 수 있다. 런타임에 객체들에서부터 책임들을 추가하거나 제거할 수 있다.

  3. 단순한 구성이어도 다양한 기능을 추가할 수 있다.
    다양한 요구에 대응하기에 적합하다. 여러가지로 데코레이터를 만들어내는데 용이하다.

실전에서 사용할만한 예시

Reader reader = new LineNumberReader(new BufferedReader(new FileReader("data")));
reader.read();

가장 흔히 볼 수 있는 Decorator 패턴을 적용한 예이다. 위와 같이 FileReader에서 데코레이터 패턴을 통해 여러번 감싸게 되면 read()를 하는데 버퍼링 기능과 행 관리 기능을 추가적으로 행하게 된다. 추가적으로, 구체적인 클래스인 LineNumberReader와 같은 클래스를 받아서 사용한다면 추가적인 기능을 사용할 수 있게 된다. ex) reader.getLineNumber();

참조 : Java 언어로 배우는 디자인 패턴 입문

profile
노력하는 사람

0개의 댓글