본문 바로가기
프로그래밍/디자인 패턴

[Design Pattern] Java에서 발견한 디자인패턴_Decorator Pattern

by 사바라다 2020. 1. 30.

안녕하세요. 오늘은 Decorator Pattern에 대해서 알아보도록 하겠습니다.

Decorator Design Pattern

데코레이터 패턴에 대해서는 토비의 스프링 3.1 Vol. 1 책에 아래와 같이 서술합니다.

데코레이터 패턴은 Target Class에 부가적인 기능을 런타임 시 다이나믹하게 부여해주기 위해 Proxy를 사용하는 패턴

다이내믹하게 기능을 부가한다는 의미는 컴파일 시점, 즉 코드상에서는 어떤 방법과 순서로 프록시와 타깃이 연결되어 사용되는지 정해져 있지 않다는 뜻

즉, 데코레이터 패턴은 런타임중 다양하게 기능을 추가 할 수 있다라는 뜻입니다. 그럼 바로 한번 예제의 아키텍처와 코드를 확인해보도록 하겠습니다.

예제

데코레이터 패턴 적용전, 참조 : https://sourcemaking.com/design_patterns/decorator

윈도우 시스템을 만든다고 가정해보겠습니다. 세로스크롤, 가로스크롤, 외곽선이 있는 웹브라우저를 만들어야 한다고 한다면 재사용성을 고려하여 위와 같이 아키텍처를 구성할 수 있습니다. 이렇게 하면 우리는 Window_WIth_Vertical_and_Horizontal_Scrollbar_Border를 상속받아 사용하면 됩니다.

하지만 이런식으로 설계한다고 하면 문제점이 있습니다. 바로 기능의 추가때마다 상속구조가 복잡해진다는 것입니다. 만약 위의 구조에서 닫기, 최대화, 최소화의 기능을 추가하고 싶다면 상속구조부터 설계해야할것입니다.

하지만 데코레이터 패턴을 사용한다면 이러한 문제를 유연하게 대응할 수 있게됩니다. 위의 클래스 아키텍처는 데코레이터 패턴을 사용하면 아래와 같이 바뀝니다.

데코레이터 패턴 적용후, 참조 : https://sourcemaking.com/design_patterns/decorator

Window라는 Target Class의 메서드에 Decorator의 메서드를 붙여서 부가기능을 추가할 수 있습니다. 그러면 위의 아키텍처를 코드로 구현해보도록 하겠습니다.

소스 코드

public interface LCD {
    void draw();
}

먼저 최상위 추상화 interface인 LCD를 선언합니다.

public class Window implements LCD{
    @Override
    public void draw() {
        System.out.println("this is Window");
    }
}

그리고 실제 사용되는 구체화된 class인 Window를 구현합니다. 이렇게 하면 우리가 일반적으로 사용하는 추상화와 구체화 클래스들이 만들어집니다. 데코레이터 패턴을 이용하면 구체화된 Window Class의 소스 수정없이 부가적인 기능(Log추가 등)을 제공해 줄 수 있습니다.

public class Decorator implements LCD{

    private LCD window;

    public Decorator(LCD window) {
        this.window = window;
    }

    @Override
    public void draw() {
        window.draw();
    }
}

public class Border extends Decorator {

    public Border(LCD window) {
        super(window);
    }

    @Override
    public void draw() {
        System.out.println("add Border");
        super.draw();
    }
}

public class HorizontalSB extends Decorator {

    public HorizontalSB(LCD window) {
        super(window);
    }

    @Override
    public void draw() {
        System.out.println("add HorizontalSB");
        super.draw();
    }
}

public class VerticalSB extends Decorator {

    public VerticalSB(LCD window) {
        super(window);
    }

    @Override
    public void draw() {
        System.out.println("add VerticalSB");
        super.draw();
    }
}

데코레이터 Class인 Decorator는 Window와 동일하게 LCD를 상위 interface로 가집니다. 그리고 생성자에 LCD를 받음으로써 실제 구현 class인 window를 받을 수 있도록 합니다.

그리고 부가적으로 제공하고자 하는 실제 기능을 가진 Class인 VerticalSB, HorizontalSB, Border Class는 이런 Decorator Class를 상속받아 구현합니다.

public class Main {
    public static void main(String[] args) {

        System.out.println("=======original=======");

        final LCD window = new Window();
        window.draw();

        System.out.println("=======add border=======");

        final LCD windowWithBorder = new Border(window);
        windowWithBorder.draw();

        System.out.println("=======add vertical & horizontal SB=======");

        final VerticalSB windowWithBorderAndSB = new VerticalSB(new HorizontalSB(windowWithBorder));
        windowWithBorderAndSB.draw();
    }
}

Client에서는 3가지의 경우를 테스트해보았습니다.

  1. 단일 출력
  2. 1개의 데코레이터를 주입
  3. 2개의 데코레이터를 주입

실제 출력되는 결과는 아래와 같습니다.

=======original=======
this is Window
=======add border=======
add Border
this is Window
=======add vertical & horizontal SB=======
add VerticalSB
add HorizontalSB
add Border
this is Window

In Java

Java에서는 어디에서 사용될까요? 바로 우리가 쉽게 접할 수 있는 File 부분입니다. File은 다양하게 부가적인 기능을 다이나믹하게 덧붙일 수 있습니다. 파일을 읽거나 파일에 쓴다고 하면 보통 File을 열고 File에 쓰기를 시작합니다. 아래는 파일을 쓸때 Class입니다.

File file = new File("classpath:application.yml");
FileWriter fw = new FileWriter(file);
BufferedWriter bufferedWriter = new BufferedWriter(fw);

bufferedWriter.write("hello");
bufferedWriter.close();

file io에는 데코레이터 패턴이 사용되고 있습니다. 확인해보도록 하겠습니다. BufferedWriter는 FileWriter에 Buffer를 추가해 disk IO의 횟수를 줄여주는 역할을 합니다.

그렇다면 실제로 파일에 쓰는 Class인 FileWriter는 위의 예제 Window처럼 구체화된 Class이며, BufferedWriter는 이러한 FileWriter에 부가적인 기능을 제공해주는 데코레이터가 됩니다. 실제로 그런지 소스를 보도록 하겠습니다.

먼저 FileWriter 입니다.

public class FileWriter extends OutputStreamWriter
{
    ...
}

public class OutputStreamWriter extends Writer
{
    ...
}

public abstract class Writer implements Appendable, Closeable, Flushable
{
    ...


}

그리고 BufferedWriter를 보겠습니다.

public class BufferedWriter extends Writer
{
    private Writer out;

    public BufferedWriter(Writer out) {
        this(out, defaultCharBufferSize);
    }
    ...
}

FileWriter와 BufferedWriter 모두 Writer 추상화 class를 상속받고 있으며 BufferedWriter는 Writer interface의 구현체를 받아서 out에 주입하고 있습니다. 이때 write() 메서드를 하게되면 BufferedWriter에서는 아래와 같이 코드가 실행됩니다.

public void write(char cbuf[], int off, int len) throws IOException {
    synchronized (lock) {
        ensureOpen();
        if ((off < 0) || (off > cbuf.length) || (len < 0) ||
            ((off + len) > cbuf.length) || ((off + len) < 0)) {
            throw new IndexOutOfBoundsException();
        } else if (len == 0) {
            return;
        }

        if (len >= nChars) {
            /* If the request length exceeds the size of the output buffer,
                flush the buffer and then write the data directly.  In this
                way buffered streams will cascade harmlessly. */
            flushBuffer();
            out.write(cbuf, off, len);
            return;
        }

        int b = off, t = off + len;
        while (b < t) {
            int d = min(nChars - nextChar, t - b);
            System.arraycopy(cbuf, b, cb, nextChar, d);
            b += d;
            nextChar += d;
            if (nextChar >= nChars)
                flushBuffer();
        }
    }
}

코드를 보시면 본인의 코드를 실행하다가 특정시점 if (len >= nChars)out.write(cbuf, off, len);를 실행하는 것을 알 수 있었습니다. 해당 method는 FileWriter에서 주입받은 메서드입니다. 이로써 우리는 데코레이터 패턴이 사용되었다는 것을 알 수 있었습니다.

마무리

오늘 배운 테코레이터 패턴은 프록시 패턴과 동일하게 Proxy를 사용합니다. Proxy를 어떻게 사용하는가에 따라 Decorator Pattern, Proxy Pattern으로 나뉘어집니다. 다음시간에는 Decorator Pattern과 Proxy Pattern을 비교해보도록 하겠습니다.

참조

토비의 스프링 3.1 Vol. 1

https://www.baeldung.com/java-decorator-pattern

https://sourcemaking.com/design_patterns/decorator

댓글