Table of contents
지난 포스팅에선 의존성을 줄이고 객체들에게 상태의 변화를 알릴 수 있는 옵저버 패턴에 대해 알아보았다. 이번에는 실행중에 클래스를 꾸미거나 기능을 확장할 수 있는 데코레이터 패턴에 대해 복습한다.
정의
객체에 추가적인 요건을 동적으로 첨가한다. 데코레이터 패턴은 서브클래스를 만드는 것을 통해서 기능을 유연하게 확장할 수 있는 방법을 제공한다.
데코레이터 패턴을 사용하면 원래 클래스의 코드는 전혀 바꾸지 않고도 객체에 새로운 임무를 부여할 수 있다. 클래스 다이어그램을 살펴보면 그 원리를 알 수 있다.
다이어그램
데코레이터가 감싸는 대상이 될 클래스를 Component라고 한다. 공통적인 부분을 Component 라는 인터페이스로 정의하고 각각 필요한 구현을 한다. 여기까진 일반적인 인터페이스 사용법과 다름이 없다.
데코레이터 패턴과 OCP
클래스는 변경에 대해서는 닫혀있지만 확장에 대해서는 열려있어야 한다.
앞서 복습한 디자인패턴 원칙에서 **OCP(Open-Closed Principle)**가 등장했었다. 위 다이어그램에서 Component 인터페이스의 methodA() 메소드에 로직을 추가한다고 하자. 메소드에 로직을 추가하려면 기존 코드를 변경하는것 말고는 방법이 없다. 그렇게 되면 이 Component는 변경에 대해서도 열려있는 객체가 되어 OCP 원칙에 어긋난다. 객체에 동적으로 추가적인 요소를 가미할 때 하나의 해결책으로 데코레이터 패턴을 사용할 수 있다.
구현방법
Component 인터페이스를 감싸기 위한 Decorator 인터페이스를 정의한다.
Decorator는 반드시 대상이 되는 Component를 상속해야한다. 그 이유는 행동 상속하는 것이 아닌 원래 있던 구성요소가 들어갈 자리에 Decorator가 들어가야하기 때문에 Decorator를 Component와 똑같은 타입으로 취급하기 위해서다.
Decorator 인터페이스를 구현하는 구상클래스를 만든다.
Decorator의 구상클래스는 반드시 자신이 감싸는 Component를 구성요소로 가져야 한다. 기능을 확장하기 위해서는 Component의 메소드를 한번 호출해야 하기 때문이다. 데코레이터 패턴은 기능을 확장할 때 Component의 원래 메소드를 호출하기 전, 또는 후에 별도의 작업을 처리한다.물론 데코레이터만의 메소드도 가질 수 있다.
인터페이스의 상속?
Decorator가 Component인터페이스를 상속했으므로 상속보다는 구성을 활용하라는 원칙을 어긴것처럼 보인다. 합성에 관한 원칙은 상속이 코드 중복, 동적 변경 불가, 코드 변경 시의 위험성 등등 때문에 되도록 상속보다는 구성을 이용하라는 것이다. 위에서는 인터페이스가 인터페이스를 상속했는데, Decorator가 Component의 행동이나 상태를 물려받기 위해서 상속하는 것이 아니라 클라이언트나 다른 클래스에서 Component가 들어갈 자리에 Decorator가 들어가기 위해서 사용한다. 책에서는 이를 형식을 맞춘다고 표현했다.
예시
데코레이터 패턴이 적용된 클래스는 흔히 볼 수 있다. java.io 패키지에 있는 InputStream에 데코레이터 패턴이 사용되었다.
- InputStream: 대상이 되는 추상 컴포넌트.
- InputStream을 구현하는 구상 컴포넌트는 모두 구상 데코레이터으로 감쌀 수 있다.
- FilterInputStream: InputStream을 감싸는 추상 데코레이터.
우리가 자바에서 파일과 관련된 작업을 할때 주로 사용하는 BufferedInputStream등이 구상 데코레이터이다. 성능을 위해 버퍼를 사용하는 BufferedInputStream, 라인넘버를 위해 사용하는 LineNumberInputStream 등 다양한 구상 데코레이터가 제공된다.
자바 API문서를 보면 FilterInputStream가 왜 추상 데코레이터인지 알 수 있다. Component인 InputStream을 상속했으며 구성요소로 InputStream을 갖고 있다.
public class BufferedInputStream extends FilterInputStream {
// ....
}
class Main {
public static void main(String[] args) {
// .... 초기화작업, try ~ catch 생략
FileInputStream fins = new FileInputStream(filePath); // 파일 스트림 생성
BufferedInputStream bfins = new BufferedInputStream(fins); // 데코레이터로 감싼다.
}
}