[Design Pattern] Decorator Pattern

Loopy·2022년 2월 8일
0

디자인패턴

목록 보기
2/9
post-thumbnail

☁️ 데코레이터 패턴 개념

데코레이터 패턴은 구조 패턴중 하나로, 핵심은 상속을 사용해 구체 클래스들을 만들지 않아도 유연하게 기능 확장을 할 수 있는데 존재한다. 즉, 객체에 추가적인 책임과 요건을 동적으로 부여할 수 있다.

구조 패턴이란, 클래스나 객체들을 조합해 더 큰 구조로 만들 수 있게 해주는 패턴이다. 구조 클래스 패턴은 상속을 통해 클래스나 인터페이스를 합성하고, 구조 객체 패턴은 객체를 합성하는 방법을 정의한다.

상속 대신 컴포지션(구성)을 사용하라!

  • Component : 기능을 제공하는 추상클래스 또는 인터페이스
  • Decorator : Component 객체를 상속하면서, 동시에 필드로 가지고 있다. 그래야 원래 기능에 추가적으로 decorate 할 수 있기 때문이다.
  • ConcreteDecorator : Decorator 를 확장하는 실질적 클래스이다.

중요한 점은 Component 객체를 내부 필드로 가지고 있다는 점인데, 이렇게 해야 자신이 감싸고 있는 구성 요소의 메서드를 호출한 결과에 새로운 기능을 더함으로써 행동을 유연하게 확장할 수 있다.

참고로 데코레이터 패턴에서의 상속은 기능을 물려받기 위해서가 아닌, 언제든지 갈아끼울 수 있도록 부모 형식을 맞추기 위해서임에 주의하자.

☁️ 데코레이터 패턴 예시

1. 추상 클래스 + 상속 활용

관리해야 하는 클래스가 무한대로 늘어나는 단점이 존재한다. 즉, 기능을 추가할 때 각 종류, 조합에 대한 클래스를 계속 생성해야해서 관리가 어려워진다.

2. 멤버 변수 선언

부모 클래스에서 if 문으로 각 토핑에 대한 가격 계산을 위한 분기 처리가 필요하다. 또한, 새로운 토핑이 추가되거나 첨가물 가격이 바뀔 때마다cost() 메서드가 수정되어야 한다.

무엇보다도 특정 첨가물이 안되는 음료에서도 hasWhip() 같은 메서드가 상속되어버리는 문제가 발생한다.

3. Decorator 패턴 사용: OCP 를 지키자!

상속 대신 데코레이터를 사용하면, OCP 를 지킬 수 있다. 자신이 감싸고 있는 구성 요소의 메서드를 호출한 결과에 새로운 기능을 더함으로써 행동을 유연하게 확장하는 방식이기 때문이다. 그렇다면 어떻게 동작하는지 자세히 보자.

우선적으로, 최상위 음료 클래스를 생성한다.

public abstract class Beverage { // Component
    String description = "제목 없음";
  
    public String getDescription() {
        return description;
    }
    public abstract double cost();
}

Mocha, Whip, DarkRoast 모두 Beverage 객체이므로 cost() 메소드 호출이 가능하다. 따라서 가격을 계산하고자 한다면, 바깥에서부터 각 객체가 장식하고 있는 객체한테 계산을 위임 후, 결과를 반환할 수 있다.

public class Mocha extends CondimentDecorator { // Decorator
    Beverage beverage;    // 내부 필드로 감싸고 있다.
    
    public Mocha(Beverage beverage) {  
        this.beverage = beverage;
    }
    
    public String getDescription() {
        return beverage.getDescription() + ", 모카";
    }
    
    public double cost() {
        return beverage.cost() + .20;  // 재귀 호출
    }
}

즉 가장 바깥쪽에 있는 데코레이터의 cost() 만 호출해도, 재귀 형식으로 자동으로 내부에서 바깥으로 올라오면서 값 계산이 된다.

{
    Beverage b2 = new DarkRoast();
    b2 = new Mocha(b2);
    b2 = new Mocha(b2); // 모카 한 개 더 추가
    b2 = new Whip(b2);
    System.out.println(b2.getDescription() + " $" + b2.cost());
}

☁️ 데코레이터 패턴 예시: JAVA I/O

자바의 입출력은 io 패키지에서 처리하고, 4개의 클래스 중심으로 데코레이터 패턴을 사용한다.

  1. FileInputStream : ConcreteComponent

파일에서부터 데이터를 입력받기 위해 만들어진 클래스이다. 하지만 byte 단위로 하나하나 접근 하다보니 하드디스크의 탐색 시간(SEEK TIME) 은 높고, 전송 시간(Transfer Time) 은 작아져 비효율적이다.

  1. BufferedInputStream: Decorator

바이트 입력 스트림에 연결되어서 버퍼를 제공해주는 성능 향상 보조 스트림이다. 데이터를 덩어리째 가져와, 입력된 내용을 버퍼에 저장한다.

BufferedReader : 문자 입력 스트림에 연결되어서 버퍼를 제공

이로써 디스크 탐색 시간(SEEK TIME) 을 줄이고, 전송 시간(Transfer Time) 을 늘릴 수 있다.

입출력 예제

public class ReadFile { 
  public static void main(String[] args) {
      try {
        BufferedInputStream readme = new BufferedInputStream(new FileInputStream("readme.txt")); 
        int b = readme.read();
        System.out.println("b = " + b);
      }
      catch (Exception e) {
        e.printStackTrace();
      }
  }
}

1개 바이트씩 내용을 읽는데, 실제 하드디스크에서 1바이트씩 계속 가져오는게 아니라 파일 전체 내용을 초기에 8192 단위로 버퍼에 올려두고 가져와서 처리한다.

InputStream

public abstract class InputStream implements Closeable { // Component
 	public abstract int read() throws IOException;
    ...
}

FileInputStream

public class FileInputStream extends InputStream { // ConcreteComponent

}

FilterInputStream

public class FilterInputStream extends InputStream { // Decorator

    protected volatile InputStream in;  // composition(구성)

    protected FilterInputStream(InputStream in) {
        this.in = in;
    }

    public int read() throws IOException {
        return in.read();
    }
    
    public int read(byte b[]) throws IOException {
        return read(b, 0, b.length);
    }
    ...
}

FilterInputStream 는 데코레이터로, InputStream 클래스를 상속 받아 형태를 같게 맞춘 것을 볼 수 있다.

BufferedInputStream

public class BufferedInputStream extends FilterInputStream {

    private static int DEFAULT_BUFFER_SIZE = 8192; // 기본 버퍼 사이즈
    protected volatile byte[] buf;  // 버퍼 필드 (volatile: 캐시가 아닌 메모리로 직접 접근)

    public BufferedInputStream(InputStream in, int size) {
        super(in);
        if (size <= 0) {
            throw new IllegalArgumentException("Buffer size <= 0");
        }
        buf = new byte[size];
    }
    
    private InputStream getInIfOpen() throws IOException {
        InputStream input = in;
        if (input == null)
            throw new IOException("Stream closed");
        return input;
    }
    
    private void fill() throws IOException {
        byte[] buffer = getBufIfOpen();
        ...
        int n = getInIfOpen().read(buffer, pos, buffer.length - pos);
    }
    
     public synchronized int read() throws IOException {
        if (pos >= count) {
            fill();
            if (pos >= count)
                return -1;
        }
        return getBufIfOpen()[pos++] & 0xff;
    }
}

버퍼 기능을 추가한 BufferedInputStream 에서는 생성자에서InputStream 을 받고 부모 클래스인 FilterInputStream 에 저장해둔다. 이후 파일을 읽어올 때 fill 메서드를 통해 부모의 read 메서드를 먼저 호출하여 버퍼에 저장해두고 사용한다.

☁️ 데코레이터 패턴 예시: Spring Framework

HttpServletRequestWrapper / HttpServletResponseWrapper

☁️ 데코레이터 패턴의 단점

자잘한 객체들이 생성되고 코드가 필요 이상으로 복잡해진다. 이후에 배울 팩토리 메서드 패턴과, 빌더 패턴이 바로 해당 문제를 해결할 수 있다.

참고 자료
데코레이터(Decorator) 패턴 - 완벽 마스터하기

profile
개인용으로 공부하는 공간입니다. 잘못된 부분은 피드백 부탁드립니다!

0개의 댓글