본문 바로가기
Design Pattern

Decorator

by 다미르 2022. 3. 15.

자바에서 파일 입출력을 구현할 때를 생각해봅시다.
아래와 같은 예제 코드를 많이 찾아볼 수 있습니다(비교를 위해 Reader Type으로 선언하였습니다).

try (Reader reader = new BufferedReader(new InputStreamReader(new FileInputStream("file path"), "UTF-8"));) {
    // read files..
} catch (Exception e) {
    throw e;
}


파일 입출력 기능을 구현할 때 왜 이런식으로 한 객체의 인스턴스를
다른 객체 생성자의 인수로 던지는 방식의 코드를 사용할까요?

이러한 질문에 답을 줄 수 있는 세 번째 디자인 패턴, 데코레이터(Decorator) 패턴에 대해서 알아보고자 합니다.

* Decorator?
- a person whose job is to design the interior of someone's home, by choosing colors, carpets, materials, and furnishings.
- 인테리어 디자이너
- 내부를 꾸며주는 사람


1. Definition

the decorator pattern is a design pattern that allows behavior to be added to an individual object, dynamically, without affecting the behavior of other objects from the same class.
The decorator pattern is often useful for adhering to the Single Responsibility Principle,
as it allows functionality to be divided between classes with unique areas of concern.
Decorator use can be more efficient than subclassing,
because an object's behavior can be augmented without defining an entirely new object.

- 다른 형제 객체에 영향을 주지 않으면서, 객체에 행동을 유연하게 동적으로 추가
- 기본 기능 외의 추가 기능을 Decorator 클래스로 정의한 후에 필요한 Decorator 객체를 조합

- 서브 클래스의 다른 대안

단순히 정의만 보면 무슨 디자인패턴인지 감이 잘 안 올테니, 처음에 소개했던 예제를 다시 보도록합시다.

ex) file을 byte 기반으로 읽은 후 UTF-8 인코딩 처리한 후에 buffer를 적용
(FileInputStream, InputStreamReader, BufferedReader 세 가지 추가기능을 가진 객체를 조합)

try (Reader reader = new BufferedReader(new InputStreamReader(new FileInputStream("file path"), "UTF-8"));) {
    // read files..
} catch (Exception e) {
    throw e;
}


파일을 읽는 기능(기본 기능)을 구현할 때 여러 가지 기능(buffer, encoding..)을 추가적으로 조립해서 사용하는 방식을
java.io 패키지에서 구현해 놓은 것입니다.
그렇다면 file을 읽는 기능의 상위 인터페이스를 정의하고,
buffer만을 적용한 구현체, encoding과 buffer 기능이 같이 있는 구현체, encoding 기능만 있는 구현체 등
여러가지 필요한 구현체를 만들어놓고 필요한 구현체만 생성하면 코드가 좀 더 깔끔해지지 않을까요?

// BufferedEncodedReader = buffer + encoding 기능을 가진 가상의 구현체 
try (Reader reader = new BufferedEncodedReader("file path")) {
    // read files..
} catch (Exception e) {
    throw e;
}

사용하는 코드는 좀 깔끔해졌네요.
근데 사용자가 어느 추가기능을 필요할지 모르는 상태에서 해당 구현체를 다 직접 만드려면,
서브 클래스(해당 예제에서는 Reader의 구현체)가 추가기능의 경우의 수만큼 폭발적으로 늘어나겠네요.
이러한 문제를 Decorator pattern은 기능을 동적으로 추가하는 방식으로 해결하고 있습니다.

2. Structure

https://www.javacodegeeks.com/2015/09/decorator-design-pattern.html

- Component : 기능을 정의한 인터페이스(해당 기능 명세의 최상위 인터페이스)
- ConcreteComponent : 기본 기능을 구현하는 구체 클래스
- Decorator : Component 객체의 참조를 가지면서, ConcreteDecorator 객체와 Component를 연결
- ConcreteDecorator : 추가 기능을 구현하는 구체 클래스

- 각 구성요소는 직접 사용될수도 있고, 데코레이터로 감싸져서 사용될수도 있습니다.

3. Example

java.io의 Reader(문자열 기반 입력) 객체의 class diagram을 보도록 합시다.
Reader는 문자열(character array) 기반 입력 스트림의 최상위 추상 클래스입니다.
모든 문자 기반 입력 스트림은 Reader 클래스를 상속받도록 되어있습니다.
대표적으로 FileReader, InputStreamReader, BufferedReader 클래스가 있습니다.

프로그램의 입력 소스는 크게 세 가지가 있습니다.
- 키보드
- 파일
- 다른 프로그램

1) Component
- Readable
- 다양한 입력소스(키보드, 파일, 다른 프로그램)으로부터 읽는 기능을 추상화한 최상위 인터페이스

2) Decorator
- Reader
- 구성요소(Reader)와 같은 type의 추상 클래스
- 구성요소의 기능을 확장

3) ConcreteDecorator
- BufferedReader
- 문자열 기반 Stream(다른 Reader의 구체 클래스)로부터 buffering을 적용하여 문자를 읽는 객체
- Decorator(Reader)의 구체 클래스
- 내부적으로 구성요소(Reader) type의 인스턴스 변수 보유
- 생성자를 통해서 다른 Reader 객체의 인스턴스를 주입받아서 사용

위의 예제를 클래스 다이어그램과 비교하면서 다시 복기해봅시다.

// a. FileInputStream : 파일을 읽어서 byte 기반 InputStream(byte기반 stream) 생성
// b. InputStreamReader : InputStream(byte기반 stream)과 Reader(문자열기반 stream)의 연결 역활
// c. BufferedReader : 다른 Reader의 구체 클래스를 주입받아서 buffering 적용
try (Reader reader = new BufferedReader(new InputStreamReader(new FileInputStream("file path"), "UTF-8"));) {
    // read files..
} catch (Exception e) {
    throw e;
}

// StringReader는 구체 클래스이기에 단독으로도 사용이 가능합니다.
try (Reader reader = new StringReader("abc") ) {
    reader.read();
} catch (Exception e) {
    throw e;
}

// BufferedReader 생성자의 인수로 활용이 가능합니다.
// InputStreamReader와 같은 Reader 객체의 구체클래스니까요.
try (Reader reader = new BufferedReader(new StringReader("abc")) ) {
    reader.read();
} catch (Exception e) {
    throw e;
}

4. Pros vs Cons

*) Pros
- 기존 코드를 수정하지 않고 행동을 확장시킬 수 있습니다.
- 구성과 위임을 통해서 런타임에서 동작을 자유롭게 추가할 수 있습니다.

*) Cons
- 코드가 복잡해질 수 있습니다.

5. Appendix
- Strategy : 동적으로 전략을 변경(change)
- Decorator : 동적으로 전략을 추가(add)

'Design Pattern' 카테고리의 다른 글

Adapter(How slf4j works?)  (0) 2022.05.07
Observer  (0) 2022.03.02
Strategy  (0) 2022.02.17