본문 바로가기

OOP, FP/디자인패턴

헤드퍼스트 디자인 패턴: 3. 데코레이터 패턴


지난 포스팅에선 의존성을 줄이고 객체들에게 상태의 변화를 알릴 수 있는 옵저버 패턴에 대해 알아보았다.

이번에는 실행중에 클래스를 꾸미거나 기능을 확장할 수 있는 데코레이터 패턴에 대해 복습한다.


데코레이터 패턴

객체에 추가적인 요건을 동적으로 첨가한다. 데코레이터 패턴은 서브클래스를 만드는 것을 통해서

기능을 유연하게 확장할 수 있는 방법을 제공한다.


우선 데코레이터 패턴을 사용하면 원래 클래스의 코드는 전혀 바꾸지 않고도 객체에 새로운 임무를 부여할 수 있다.

클래스 다이어그램을 살펴보면 그 원리를 알 수 있다.


감싸는 대상이 될 클래스를 Component라고 한다.

우선 공통부분을 Component 라는 인터페이스로 정의하고 각각 필요한 구현을 한다.

여기까진 일반적인 인터페이스 사용법과 다름이 없다.


이런 상황에서 Component인터페이스의 methodA()라는 메소드에 로직을 추가하고 싶다.

앞서 복습한 디자인패턴 원칙에서 OCP(Open-Closed Principle)가 등장했었다.

클래스는 변경에 대해서는 닫혀있지만 확장에 대해서는 열려있어야 한다는 원칙이었다.

위의 상황에서 메소드에 로직을 추가하려면 기존 코드를 변경하는것 말고는 방법이 없다.

그렇게 되면 이 Component는 변경에 대해서도 열려있는 객체가 되어버리므로 원칙에 어긋난다.


객체에 동적으로 추가적인 요소를 가미할 때 하나의 해결책으로 데코레이터 패턴을 사용할 수 있다.

(1) Component 인터페이스를 감싸기 위한 Decorator 인터페이스를 정의한다.

- 이 때 Decorator는 반드시 대상이 되는 Component를 상속해야한다.

  아마도 이렇게 하는 이유는 클라이언트에서 Decorator를 Component와 똑같은 객체라고 생각해야하기 때문인것 같다.

  상속을 통해서 행동을 물려받으려고 하는게 아니라 원래 있던 구성요소가 들어갈 자리에 Decorator가 들어가야 하니까.


(2) Decorator 인터페이스를 구현하는 구상클래스를 만든다.

- 여기서도 주의해야할 점이 있다. Decorator의 구상클래스는 반드시 자신이 감싸는 Component를 구성요소로 가져야 한다.

  기능을 확장하기 위해서는 Component의 메소드를 한번 호출해야 하기 때문이다.

  데코레이터 패턴은 기능을 확장할 때 Component의 원래 메소드를 호출하기 전, 또는 후에 별도의 작업을 처리한다.


* 데코레이터만의 메소드도 가질 수 있다.


Decorator가 Component인터페이스를 상속했으므로 상속보다는 구성을 활용하라는 원칙을 어긴것처럼 보인다.

책에서 두번째로 저 등장한 원칙은 상속이 코드 중복, 동적 변경 불가, 코드 변경 시의 위험성 등등 때문에

되도록 상속을 활용하지 말라고 말한다.


여기서 인터페이스가 인터페이스를 상속했는데, 보통 상속을 사용하는 이유는 행동이나 상태를 물려받기 위함이다.

하지만 여기서는 Decorator가 Component의 행동이나 상태를 물려받기 위해서 상속하는 것이 아니라

클라이언트나 다른 클래스에서 Component가 들어갈 자리에 Decorator가 들어가기 위해서 사용한다.

책에서는 이를 형식을 맞춘다고 표현했다.




데코레이터 패턴은 자바에서 자주 접할 수 있다.

java.io 패키지에 있는 InputStream에 데코레이터 패턴이 사용되었다.


InputStream이 대상이 되는 Component에 해당한다.

FilterInputStream는 InputStream을 감싸는 추상 데코레이터이다.


InputStream을 구현하는 객체는 모두 FilterInputStream으로 감쌀 수 있다.

FileInputStream은 대상이 되는 구상 컴포넌트이다.

우리가 자바에서 파일과 관련된 작업을 할때 주로 사용하는 BufferedInputStream등이 구상 데코레이터이다.


성능을 위해 버퍼를 사용하는 BufferedInputStream,

라인넘버를 위해 사용하는 LineNumberInputStream... 등등 다양한 구상 데코레이터가 제공된다.




자바 API문서를 보면 FilterInputStream가 왜 추상 데코레이터인지 알 수 있다.

Component인 InputStream을 상속했으며 구성요소로 InputStream을 갖고 있다.


BufferedInputStream은 추상 데코레이터인 FilterInputStream을 상속하고 있다.

우리는 new BufferedInputStream(in);과 같이 사용한다.

InputStream을 BufferedInputStream으로 감싸서 사용하기 위함이다.



객체를 감싸서 새로운 임무를 부여하는 데코레이터 패턴에 대한 복습을 마치겠다.


1. 스트래티지 패턴

2. 옵저버 패턴

3. 데코레이터 패턴