본문 바로가기

JAVA/Design Patterns

데코레이터 패턴

반응형

데코리이터 패턴

객체에 추가 요소를 동적으로 더할수 있다.

데이코레이터를 사용하면 서브 클래스를 만드는 경우에 비해 훨씬 유연하게 기능을 확장할 수 있다.

상속은 기능을 확장하는 방법을 제공한다.


핵심정리

- 상속을 통해 확장을 할 수도 있지만 디자인의 유연성 면에서 보면 별로 좋지 않다.

- 기존 코드를 수정하지 않고도 행동을 확장하는 방법이 필요하다.

- 구성과 위임을 통해서 실행중에 새로운 행동을 추가할 수 있다.

- 상속 대신 데코레이터 패턴을 통해 행동을 확장할 수 있다.

- 데코레이터 패턴에서는 구상 구성요소를 감싸주는 데코레이터들을 사용한다.

- 데코레이터 클래스의 형식은 그 클래스가 감싸고 있는 클래스의 형식을 반영한다.(상속 또는 인터페이스 구현을 통해서 자신을 감쌀 클래스와 같은 형식을 가지게 된다)

- 데코레이터에서는 자기가 감싸고 있는 구성요소의 메서드를 호출한 결과에 새로운 기능을 더함으로써 행동을 확장한다.

- 구성요소를 감싸는 데코레이터의 개수에는 제한이 없다.

- 구성요소의 클라이언트 입장에서는 데코레이터의 존재를 알 수 없다. 클라이언트에서 구성요소의 구체적인 형식에 의존하게 되는 경우는 예외

- 데코레이터 패턴을 사용하면 자잘한 객체들이 매우 많이 추가 될수 있고, 데코레이터를 너무 많이 사용하면 코드가 필요 이상으로 복잡해질 수 있다.


주요 사용처

- 자바 I/O

FileInputStream, BufferedInputStream, LineNumberInputStream

BufferedInputStream, LineNumberInputStream는 모두FilterInputStream을 확장한 클래스다.

FileInputStream은 추상 데코레이터 클래스 역할을 한다.



예)

데이터를 파일에 출력하는 기능을 제공하는 FileOut 클래스가 있을 때, FileOut 클래스에 버퍼 기능을 추가하거나 압축 기능을 추가하려면 상속받아서 기능을 확장해서 구현할 수 있다.


FileOut

+wirte()


BufferedOut ZipOut

+write()      +write()


상속을 이용한 기능확장 방법이 쉽긴 하지만, 다양한 조합의 기능확장이 요구될 때 클래스가 불필요하게 증가하는 문제가 발생된다.

버퍼기능과 압축기능을 함께 제공해야 한다거나, 압축한 뒤 암호화 기능을 제공한다거나, 버퍼기능과 암호화 기능을 함께 제공해야 한다면, 클래스가 증가하고 계층 구조가 복잡해진다.


FileOut


BufferedOut, ZipOut, EncryptionOut


BufferdEncOut, BufferedZipOut, EncZipOut


이런경우에는 사용할 수 있는 패턴이 데코레이터 패턴이다.

데코레이터 패턴은 상속이 아닌 위임을 하는 방식으로 기능을 확장해 나간다.


FileOut 인터페이스는 파일 출력기능을 정의하고 실제 파일 출력 기능은 FileOutImpl 클래스가 구현하고 있다.

중요한 점은 기능 확장을 위해 FileOutImpl 클래스를 상속받지 않고 Decorator라 불리는 별도 추상 클래스를 만들었다는 점이다.


Decorator 클래스는 모든 데코레이터를 위한 기반 기능을 제공하는 추상클래스이다


public abstract class Decorator implements FileOut {

private FileOut delegate; // 위임대상

public Decorator(FileOut delegate) {

this.delegate = delegate;

}

protected void doDelegate(byte[] data) {

delegate.write(data); //delegate에 쓰기 위임

}

}


doDelegate() 메서드는 생성자를 통해서 전달받은 FileOut 객체에 쓰기 기능을 위임한다.


BufferedOut 클래스, EncryptionOut 클래스, ZipOut 클래스는 모두 데코레이터 클래스로서 Decorator 클래스를 상속받고 있다.

이들 클래스는 자신의 기능을 수행한 뒤에 상위 클래스의 doDelegate() 메서드를 이용해서 파일 쓰기를 위임하도록 구현한다.


public class EncryptionOut extends Decorator {

public EncryptionOut(Fileout delegate) {

super(delegate);

}

public void write(Byte[] data) {

byte[] encryptedData = encrypt(data);

super.doDelegate(encryptedData);

}

private byte[] encrypt(byte[] data) {

...

}

}


EncryptionOut클래스의 write() 메서드는 파일에 쓸 데이터를 암호화한 뒤에 doDelegate()  메서드를 이용해서 암호화된 데이터를 delegate 객체에 전달한다.

BufferedOut 클래스와 ZipOut 클래스도 비슷한 방식으로 구현한다.


파일에 데이터를 암호화해서 쓰는 기능이 필요한 곳의 코드는

FileOut 객체를 이용해서 EncryptionOut 객체를 생성한 뒤, EncryptionOut 객체의 write() 메서드를 실행한다.


FileOut delegate = new FileOutImpl();

FileOut fileOut = new EncryptionOut(delegate);

fileOut.wirte(data);


EncryptionOut의 write() 메서드를 실행하면 EncryptionOut의 wirte() 메서드내에서 데이터를 암호화하고 FileOutImpl 객체의 wirte() 메서드에 암호화된 데이터를 전달하게 된다.

EncryptionOut 객체는 FileOutImpl 객체가 제공하는 파일 쓰기 기능에 암호화 기능을 추가해서 주는 역할을 수행하게 되며, 기존 기능에 새로운 기능을 추가해준다는 의미에서 EncryptionOut 객체를 데코레이터라고 부른다.


**데코레이터의 장점은 데코레이터를 조합하는 방식으로 기능을 확장할 수 있다는 데 있다.


데이터를 압축한 뒤에 암호화를 해서 파일에 쓰고 싶다면, 두개의 데코레이터 객체를 조합해서 사용하면된다.


FileOut delegate = new FileOutImpl();

FileOut fileOut = new EncryptionOut(new ZipOut(delegate));

fileout.write(data);


기능 적용 순서의 변경도 쉽다. 데코레이터의 생성 순서를 변경해주기만 하면된다.


//퍼버->암호화->압축->파일쓰기

FileOut fileOut = new BufferedOut(new EncryptionOut(new ZipOut(delegate)));


//암호화 -> 압축 -> 버퍼 -> 파일쓰기

FileOut fileOut = new EncryptionOut(new ZipOut(new BufferdEncOut(delegate)));


데코레이터 패턴을 사용하면 각 확장 기능들의 구현이 별도의 클래스로 분리되기 때문에, 각 확장 기능 및 원래 기능을 서로 영향없이 변경할 수 있도록 만들어 준다.

FileOutImpl 클래스의 구현을 변경하더라도 EncryptionOut, ZipOut, BufferdEncOut클래스는 영향을 받지 않으며, EncryptionOut 클래스가 내부 암호화 알고리즘을 변경하더라도 다른 데코레이터나 FileOutImpl 클래스는 영향을 받지 않는다.

즉, 데코레이터 패턴은 단일책임원칙을 지킬수 있도록 만들어준다.


데코레이터 패턴은 전략 패턴/템플릿 메서드패턴/상태패턴과 함께 매우 흔하게 사용되는 패턴이다.

스프링 프레임워크의 경우 트랜잭션 처리를 위해 데코레이터 패턴을 사용한다.

스프링 프레임워크에서 트랜잭션 관련 설정을 추가하려면 트랜잭션 기능이 추가된 데코레이터 객체를 런타임에 생성한다.


<<interface>>

ArticleService

+writeArticle()


TransactionArticleService, ArticleServiceImpl

스프링이 런타임에 생성


데코레이터 패턴을 적용할때 고려할 점

데코레이터 패턴을 구현할 때 고려할 점은 데코레이터 대상이 되는 타입의 기능 개수에 대한 것이다.

데코레이터 대상이 되는 FileOut 타입은 write() 메서드가 한개만 정의되어 있어 데코레이터 구현이 비교적 간단하지만, 정의되어 있는 메서드가 증가하게 되면 그 만큼 데코레이터의 구현도 복잡해진다.


데코레이터 구현에서 고려해야 할 또 다른 사항은 데코레이터 객체가 비정상적으로 동작할 때 어떻게 처리할 것이냐에 대한 것이다.

게시글작성 이후 생성된 게시글 데이터를 외부 메시지 서버에 전송해주는 기능을 별도의 데코레이터로 구현했다면,이  경우 런타임의 객체 간 메시지 흐름이 있다.


클라이언트

1. write()  호출

메시지 전송 데코레이터

2.write() 호출

트랜잭션 처리 데코레이터

3.트랜젝션 시작 후 write()호출

ArticleServiceImpl

4.리턴

트랜잭션처리 데코레이터

5.트랜잭션 커밋 후 리턴

메시지전송 데코레이터

6.메서지 서버에 전송 후 리턴


외부의 메시지 서버에 장애가 발생한다면, 실행 흐름에서 6번과정에 문제가 발생하게 된다.

하지만 1~5번까지의 과정은 정상적으로 실행되어 트랜잭션이 커밋되었기 때문에 DB에는 새로운 데이터가 정상적으로 추가된다.

이 경우, 6번 과정의 문제가 발생했다고 해서 클라이언트에 익셉션을 발생시키는 것이 올바른지 고민해봐야한다.

왜냐하면 클라이언트가 요구하는 기능인 게시글 등록 자체는 정상적으로 실행되었기 때문이다.


이런 경우 메시지 전송 데코레이터는 외부 메시지 서버에 데이터 전송이 실패하더라도 익셉션을 발생시키지는 대신 실패 로그를 남기는 방법을 선택할 수 있다.

이렇게 함으로써 외부 메시지 서버 연동에 실패하더라도 클라이언트는 에러 결과가 아닌 정상 결과를 볼 수 있으며, 향후에 실패 로그를 이용해서 데이터 재전송과 같은 사후 처리를 할 수 있게 된다.


**데코레이터의 단점은 사용자 입장에서 데코레이터 객체와 실제 구현 객체의 구분이 되지 않기 때문에 코드만으로는 기능이 어떻게 동작하는지 이해하기 어렵다는 점이다.


wirteTo() 메서드는 파라미터로 전달받은FileOut 객체를 사용하는데, writeTo()메서드는 이 FileOut 객체가 단순히 파일에 쓰기만 하는지 아니면 압출을 하는지 등의 여부를 알수 없다.

실제로 FileOut 객체가 어떻게 동작하는지 알려면 런타임에 생성된 객체의 구조를 이해해야한다.


public class ImageSource {

public void writeTo(FileOut out) {

out.write(imageData);

}

...

}

반응형

'JAVA > Design Patterns' 카테고리의 다른 글

2.옵저버 패턴  (0) 2018.12.04
디자인의 원칙  (0) 2018.11.29
상태패턴  (0) 2018.11.27
전략패턴  (0) 2018.11.26
인터페이스로 프로그래밍하기 #깨지기 쉬운 기반 클래스 문제 요약  (0) 2018.02.19