Chapter 4- Decorator 패턴
기본 아이디어
Decorator 패턴은 '구현 상속(Extends)' 대신 '인터페이스 상속(Implements)/위임' 전략을 사용하여
깨지기 쉬운 기반 클래스 문제를 해결한다.
public Table select( Selector where )
{
Table resultTable = new ConcreteTable( null,
(String[]) columnNames.clone() );
//result table을 Selector의 전략에 맞게 꺼내오는 부분 생략
return new UnmodifiableTable(resultTable);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
public Table select(Selector where, String[] requestedColumns )
{
Table resultTable = new ConcreteTable( null,
(String[]) requestedColumns.clone() );
//result table을 Selector의 전략에 맞게 꺼내오는 부분 생략
return new UnmodifiableTable(resultTable);
}
- ConcreteTable을 생성하고, 조회한 후에 UnmodifiableTalbe을 사용하여 실제 결과 테이블을 래핑하여 반환
public class UnmodifiableTable implements Table
{
//UnmodifialbeTalbe의 Wrapped 에 Assign 한다.
private Table wrapped;
public UnmodifiableTable( Table wrapped )
{
this.wrapped = wrapped;
}
/** Return an UnmodifieableTable that wraps a clone of the
* currently wrapped table. (A deep copy is used.)
*/
public Object clone() throws CloneNotSupportedException
{ UnmodifiableTable copy = (UnmodifiableTable) super.clone();
copy.wrapped = (Table)( wrapped.clone() );
return copy;
}
public int insert(String[] c, Object[] v ){ illegal(); return 0;}
public int insert(Object[] v ){ illegal(); return 0;}
public int insert(Collection c,Collection v ){ illegal(); return 0;}
public int insert(Collection v ){ illegal(); return 0;}
public int update(Selector w ){ illegal(); return 0;}
public int delete( Selector w ){ illegal(); return 0;}
public void begin ( ){ illegal(); }
public void commit (boolean all){ illegal(); }
public void rollback (boolean all){ illegal(); }
private final void illegal()
{
throw new UnsupportedOperationException();
}
public Table select(Selector w,String[] r,Table[] o)
{
return wrapped.select( w, r, o );
}
public Table select(Selector where, String[] requestedColumns)
{
return wrapped.select(where, requestedColumns );
}
public Table select(Selector where)
{
return wrapped.select(where);
}
public Table select(Selector w,Collection r,Collection o)
{
return wrapped.select( w, r, o );
}
public Table select(Selector w, Collection r)
{
return wrapped.select(w, r);
}
public Cursor rows()
{
return wrapped.rows();
}
public void export(Table.Exporter exporter) throws IOException
{
wrapped.export(exporter);
}
public String toString() { return wrapped.toString(); }
public String name() { return wrapped.name(); }
public void rename(String s){ wrapped.rename(s); }
public boolean isDirty() { return wrapped.isDirty(); }
public Table extract(){ return wrapped; }
}
- Decorater(UnmodifieableTable)은 Table을 수정하는 메소드(insert, update, delete)가 호출 될 때는 예외
- 나머지 메소드들은 래핑된 Decorater에 작업을 위임한다.
정리
UnmodifiableTable은 ConcreteTable에 있는 여러 메소드의 행위를 변화시켜 Table을 효과적으로 수정 불가능하게 만드는 것이다.
1. IsImmutable 플래그를 넣어 이 플래그를 검사하게 할 수도 있지만 이 해결방법은 너무 복잡하다.
2. 클래스를 상속하여 테이블을 수정할 수 있는 메소드들이 예외를 던지도록 오버라이딩할 수도 있었지만
이는 깨지기 쉬운 기반 클래스 문제를 야기할 수 있다.
a. 기반 클래스에 테이블을 수정할 수 있는 메소드를 추가했는데 , 파생 클래스에서 이 메소드를 오버라이딩하는 것을 깜빡?
b. 컴파일 타임 에러를 런타임 에러로 바꾸기 때문에 유지보수하기 어려운 코드를 만들 위험에 노출시키게 된다.
3. 구현 상속으로 발생할 수 있는 문제를 인터페이스 상속/위임 전략을 통해 해결
래핑된 객체가 구현하고 있는 인터페이스와 똑같은 인터페이스를 구현한 수에 필요한 경우 연산을 래핑되어 있는 객체에 위임
(래핑되는 객체와 인터페이스를 공유하지 못한 다면 해당 객체를 기반 클래스로 해야 할 것이다)
=> 이 방법은 Extends를 사용하지 않고 행위를 변경시킬 수 있도록 해주기 때문에 깨지기 쉬운 기반 클래스 문제를 해결해준다.
Java I/O 클래스 Decorator 패턴 - 압축되어 있는 바이트 스트림을 읽을 때 데코레이터 체인을 사용한 예
1. 바이트를 읽는다.
2. 버퍼링을 사용하여 읽기 작업을 보다 효율적으로 한다.
3. 바이트 스트림의 압축을 푼다.
자바는 위 문제를 세 개의 클래스를 이용하여 해결한다.
1. FileInputStream에서 시작하며, 다음과 같이 인스턴스화 한다.
try
{
InputStream in = new FileInputStream( "file.name" );
}
catch( IOException e )
{
System.err.println( "Couldn't open file.name" );
e.printStackTrace();
}
2. 버퍼링을 데코레이션(혹은 래핑) 전략을 사용하여 추가한다.
즉 InputStream 객체를 바이트를 버퍼링하는 또다른 InputStream 구현체를 사용하여 래핑한다.
래퍼에 바이트를 요청하면 래퍼는 다시 래핑되어 있는 스트림 객체에 여러 바이트를 요청한 후 이를 반환 한다.
try
{
InputStream in = new FileInputStream( "file.name" );
in = new BufferedInputStream( in );
}
catch( IOException e )
{
System.err.println( "Couldn't open file.name" );
e.printStackTrace();
}
3. 압축 해체는 또 다른 데코레이터를 사용하여 추가한다.
try
{
InputStream in = new FileInputStream( "file.name" );
in = new BufferedInputStream( in );
in = new GZipInputStream( in );
}
catch( IOException e )
{
System.err.println( "Couldn't open file.name" );
e.printStackTrace();
}
정리
- 이러한 해결 방법은 유연하다. 원하는 기능을 위해 여러 가지 데코레이터를 조합하여 사용하면 되는 것인다.
- 더 중요한 것은 각 데코레이터 자체는 단 하나의 문제를 해결하기 때문에 상대적으로 단순하다.
=> 결과적으로 데코레이터들은 작성 및 디버깅하기 쉬우며, 시스템에 다른 영향을 미치지 않으면서 수정이 용이
- 여러 데코레이터들을 조합하면 여러 다양한 기능을 사용할 수 있다. 데코레이터 자체는 작지만, 합쳐지면 강력하다.
- Decorator 패턴을 사용하면 향후 원하는 기능을 추가하기도 쉽다.
- 주요 목적은 구현 상속의 대안을 제공하는 것이다. 기반 클래스의 행위를 수정하거나 기능을 추가하려 한다면
Decorator 패턴의 도입을 신중히 검토해 보자
- Decorator들을 래핑하는 순서도 중요하다.
Java Collection 클래스 decorator 패턴 사용의 예
스레드 안정성을 추가하기 위해 핵심 컬렉션 클래스들을 어떻게 확장(Extends)했는지 보여준다.
결과적으로 구체 클래스들의 숫자가 2배로 늘어났다.
컬렉션에 불변 기능을 추가한다면 아마도 Lock() 메소드를 추가하여 컬렉션을 수정하는 메소드들이 예외를 던지게 할 수 있을 것이다. 하지만 구현 상속을 이용하여 이러한 수정을 했을 때 구체 클래스의 숫자는 다시 두 배가 된다.
Collection 프레임워크 디자이너는 이와 같은 문제를 Decorator 패턴을 사용하여 해결하였다.
Collection threadSafe =
Collections.synchronizedCollection( new LinkedList() );
The Collections class looks something like this:
public class Collections // utility
{
//...
public static Collection synchronizedCollection(final Collection wrapped)
{
return new SynchronizedCollection(wrapped);
}
private static class SynchronizedCollection implements Collection
{
private Collection unsafe;
public SynchronizedCollection( Collection unsafe )
{
this.unsafe = unsafe;
}
public synchronized boolean add( Object toAdd )
{
return unsafe.add( toAdd );
}
public synchronized boolean remove( Object toAdd )
{
return unsafe.remove( toAdd );
}
// Implement synchronized version of all other Collection
// methods here ...
}
}
다음과 같이 이중 래핑을 통해 동기화된 불편 컬렉션을 만들 수도 있다.
Collection myList = new LinkedList();
//...
myList = Collections.synchronizedCollection
(
Collections.unmodifiableCollection
(
myList
)
);
=> Decorator 패턴의 핵심은 총 2개의 클래스를 추가하는 것 만으로도 구현 상속 시 18개의 클래스를 필요로 했던 문제 해결
정리
- Decorator 패턴의 핵심은 객체에 새로운 능력을 추가하거나 객체의 행위를 변화시킬 수 있도록 해준다.
- Decorator 패턴에서는 상속대신 합성을 사용하여 클래스 계층 구조를 단순화시킨다.