의도
객체에 기능을 동적으로 추가/삭제하기 위해 사용. 상속을 이용하지 않고 객체의 합성을 이용하므로 flexiblity가 높다.
클래스 상속을 이용해 기능을 추가하게 되면 이미 생성 되어 존재하던 객체에 기능이 추가 되는 것이 아니라, 상속을 통해 기존의 기능을 물려 받은 클래스에 새로운 기능을 정적으로 추가한다. 이런 경우 실행 시간에 동적으로 기능을 추가 하는 것이 아니라 기능이 추가된 새로운 객체를 생성해야만 한다.
동기
클래스 전체가 아닌 각각의 객체마다 특정한 기능을 추가 시기고자 하는 경우 사용되는 패턴이다. 예를 들어 아래와 같은 클래스 구조가 있다고 가정하자. 모든 Write 연산에 대한 최상위 추상 클래스 Stream이 있고, 쓰는 대상에 따라 Stream 클래스를 상속 받은 FileStream과 NetworkStream으로 나뉘어 있다.
using System;
class Program
{
abstract class Stream // 무엇인가를 쓰는 기능을 가진 최상위 추상 클래스
{
public abstract void Write(byte[] buff);
}
class FileStream : Stream // 파일에 무엇인가를 쓰는 기능을 가진 클래스
{
public override void Write(byte[] buff)
{
Console.WriteLine($"Write {buff.Length} bytes to File");
}
}
class NetworkStream : Stream // 네트워크에 무엇인가를 쓰는 기능을 가진 클래스
{
public override void Write(byte[] buff)
{
Console.WriteLine($"Write {buff.Length} bytes to Network");
}
}
static void Main(string[] args)
{
byte[] buff = new byte[1024];
FileStream fs = new FileStream();
fs.Write(buff);
}
}
만일 위의 파일에 Write하는 기능에 압축을 하는 기능을 추가하고자 하면 우리에게는 다음과 같은 몇가지 선택지가 있다.
- FileStream 클래스에 ZipWrite라는 압축이 추가된 Write 기능을 추가한다.
- Stream 클래스에 ZipWrite라는 압축이 추가된 Write 기능을 추가한다.
- 해서 쓰는 인터페이스를 추가하고
FileStream에 ZipWrite를 추가하는 선택은 만일 NetworkStream에서도 해당 기능이 필요하다면 NetworkStream에도 동일한 기능을 추가해줘야 한다는 문제점이 있다. 그렇다고 Stream클래스에 추가하게 되면 Stream클래스를 상속 받는 모든 클래스들에 ZipWrite를 추가해주어야만 하고 변경이 발생 할 때마다 Stream 클래스를 수정해야 하는 이슈가 발생한다.
이렇게 다이나믹한 관계를 가지는 기능들을 상속을 이용해서 구현한다면 압축 기능이 추가된 FileStream, 비동기 입출력이 추가된 FileStream, 비동기 입출력과 압축이 추가된 NetworkStream 등 클래스나 함수들의 수가 폭발적으로 늘어날 가능성이 있고, 새로운 케이스가 발생할 때 마다 기존 클래스들을 수정해야하는 단점이 있다.
이런 상속의 단점을 해결하기 위해 객체의 합성을 이용 하는, 즉, 추가 되고자 하는 특정 기능을 클래스로 구현하고, 그 인터페이스를 새로운 클래스가 내포 함으로써 동적으로 추가 기능을 변경 가능토록 하고, 또한 추가 기능 클래스가 다른 추가 기능클래스를 내포하는 재귀적인 형태를 가짐으로써 여러 가지 기능의 추가가 가능토록 할 수 있는 패턴이 데코레이터 패턴이다.
아래의 예제는 ZipStream이라는 압축을 전담하는 클래스를 새로 만들어 FileStream을 인자로 받아 내부에서 호출 할 수 있도록 만들었다.
class Program
{
// ... 생략 ...
class ZipStream : Stream
{
Stream s;
public ZipStream(Stream s)
{
this.s = s;
}
public override void Write(byte[] buff)
{
Console.WriteLine($"Compress {buff.Length} bytes");
s.Write(buff);
}
}
static void Main(string[] args)
{
byte[] buff = new byte[1024];
FileStream fs = new FileStream();
ZipStream zs = new ZipStream(fs);
zs.Write(buff);
}
}
만일 스트림에 버퍼기능을 추가하고 싶다면 위와 같은 방법으로 BufferedStream을 추가 할 수도 있다.
class Program
{
// ...생략...
class BufferedStream : Stream
{
Stream s;
public BufferedStream(Stream s)
{
this.s = s;
}
public override void Write(byte[] buff)
{
Console.WriteLine($"Buffering {buff.Length} bytes");
s.Write(buff);
}
}
static void Main(string[] args)
{
byte[] buff = new byte[1024];
FileStream fs = new FileStream();
ZipStream zs = new ZipStream(fs);
BufferedStream bs = new BufferedStream(zs);
bs.Write(buff);
}
}
구조
참여 객체
- Component의 역할
추가 기능이 되는 decorator(border)와 그것을 사용하는 concrete component(text view)의 인터페이스 - ConcreteComponent
Component의 인터페이스를 구체적으로 구현 - Decorator(장식자)
Component 역할과 동일한 인터페이스를 가짐
Decorator할 대상이 되는 Component의 역할 또한 가지고 있음 - ConcrteDecorator(구체적인 장식자)
구체적인 Decorator의 역할
결과
- 상속을 이용하여 기능을 추가 하는 것 보다, 합성을 이용하여 구현하므로 보다 높은 유연성을 제공한다.
- 동일한 기능을 반복하는 것이 간단해 진다.
- 해당패턴은 복잡한 클래스들을 미리 정의 해 놓으므로써 예측가능한 모든 특성들을 지원하는 것이 아니라 기능의 필요에 따라 '점진적으로 확장'해 나갈 수 있는 형태를 띄고 있다.
- Decorator pattern은 클래스의 수는 줄어드는 반면 객체의 수는 늘어나는 성향이 있다. 또한 이들 객체는 서로 다른 객체임에도 불구하고 연결된 형태(예를 들어 decorator가 또 다른 decorator를 내포한다던지..)에 따라 비슷하게 보이는 경우도 있다. 따라서 해당 패턴을 적용한 경우 해당 패턴에 대해서 잘 알고 있지 못하면 이해하거나 디버깅하기 어려울 수도 있다.
Decorator, Adapter, Composite 패턴이 세 개의 패턴 클래스 간에는 본질적인 유사성이 있다. Adapter 클래스는 기존의 클래스를 장식하는 역할을 수행하는 것처럼 보인다. 그러나 해당 클래스의 기능은 특정 프로그램에 대하여 좀더 편리한 클래스의 인터페이스로 변경하는 작업을 수행하는 것이다. Decorator 클래스는 클래스의 코든 인스턴스보다는 특정 인스턴스에 메소드를 추가한다.
여기에서 가능성 있는 상황과 단일 항목으로 구성된 composite 클래스가 본질적으로는 decorator 클래스라는 것이다. 그러나 다시 말하지만, 의도는 다르다.
(http://blog.naver.com/smurpettt/150000478580)
부록 1. 참고 자료
- 자바로 배우는 디자인 패턴
- GoF Design Patterns
- Headfirst Design Pattern
- http://blog.naver.com/cosmosb612?Redirect=Log&logNo=80040955894
- http://blog.naver.com/smurpettt/150000478580