티스토리 뷰
‘개발자가 반드시 정복해야 할 객체 지향과 디자인 패턴' 책을 보고 정리한 글입니다.
상속은 기능을 확장하는 방법을 제공한다.
상속을 이용한 기능 확장 방법이 쉽긴 하지만, 다양한 조합의 기능 확장이 요구될 때 클래스가 불필요하게 증가하는 문제가 발생된다.
예를 들어 버퍼 기능과 압축 기능을 함께 제공해야 한다거나, 압축한 뒤 암호화 기능을 제공해야 한다면 클래스가 증가하고 계층 구조가 복잡해진다.
이런 경우에 사용할 수 있는 패턴이 데코레이터 패턴이다.
데코레이터 패턴은 상속이 아닌 위임을 하는 방식으로 기능을 확장해 나간다.
FileOut 인터페이스는 파일 출력 기능을 정의하고 있고, 실제 파일 출력 기능은 FileOutImpl 클래스가 구현한다.
여기서 중요한 건 기능 확장을 위해 FileOutImpl 클래스를 상속받지 않고 Decorator라 불리는 별도의 추상 클래스를 만들었다는 점이다.
Decorator 클래스는 모든 데코레이터를 위한 기반 기능을 제공하는 추상 클래스이다.
이 클래스의 doDelegate() 메서드는 생성자를 통해서 전달받은 FileOut 객체에 쓰기 기능을 위임한다.
public class Decorator implements FileOut {
private FileOut delegate;
public Decorator(FileOut delegate) {
this.delegate = delegate;
}
protected void doDelegate(byte[] data) {
delegate.write(data); // delegate에 쓰기 위임
}
}
BuffedOut, EncryptionOut, ZipOut 클래스는 모두 데코레이터 클래스로서 Decorator 클래스를 상속받는다.
이들 클래스는 자신의 기능을 수행한 뒤에 상위 클래스의 doDelegate() 메서드를 이용해서 파일 쓰기를 위임하도록 구현한다.
다음은 EncryptionOut 클래스 코드이다.
public class EncryptionOut extends Decorator {
public EncryptionOut(FileOut delegate) {
super(delegate);
}
@Override
public void write(byte[] data) {
byte[] encryptedData = encrypt(data);
super.doDelegate(encryptedData);
}
private byte[] encrypt(byte[] data) {
// 암호화하는 로직
return data;
}
}
위 클래스의 write() 메서드는 파일에 쓸 데이터를 암호화한 뒤에, doDelegate() 메서드를 이용해서 암호화된 데이터를 delegate 객체에 전달한다.
이제 EncryptionOut을 사용하는 코드를 살펴보자.
이 코드는 다음과 같이 FileOut 객체를 이용해서 EncryptionOut 객체를 생성한 뒤에, EncryptionOut 객체의 wirte() 메서드를 실행한다.
FileOut delegate = new FileOutImpl();
FileOut fileOut = new EncryptionOut(delegate);
fileOut.write(data);
EncryptionOut의 write() 메서드를 실행하면 다음 그림과 같이 EncryptionOut의 write() 메서드에서 데이터를 암호화하고, FileOutImpl 객체의 write() 메서드에 암호화된 데이터를 전달하게 된다.
여기서 EncryptionOut 객체는 FileOutImpl 객체가 제공하는 파일 쓰기 기능에 암호화 기능을 추가해 주는 역할을 수행하게 되며, 기존 기능에 새로운 기능을 추가해 준다는 의미에서 EncryptionOut 객체를 데코레이터라고 부른다.
데코레이터 패턴의 장점은 데코레이터를 조합하는 방식으로 기능을 확장할 수 있다는 데에 있다.
데이터를 압축한 뒤에 암호화를 해서 파일에 쓰고 싶다면, 다음과 같이 두 개의 데코레이터 객체를 조합해서 사용하면 된다.
FileOut delegate = new FileOutImpl();
FileOut fileOut = new EncryptionOut(new ZipOut(delegate);
fileOut.write(data);
기능 적용 순서의 변경도 쉽다.
아래 코드처럼 데코레이터의 생성 순서를 변경해주면 된다.
FileOut delegate = new FileOutImpl();
// 버퍼 -> 암호화 -> 압축 -> 파일 쓰기
FileOut fileOut = new BufferedOut(new EncryptionOut(new ZipOut(delegate)));
// 암호화 -> 압축 -> 버퍼 -> 파일 쓰기
FileOut fileOut = new EncryptionOut(new ZipOut(new BufferedOut(delegate)));
fileOut.write(data);
데코레이터 패턴을 사용하면 각 확장 기능들의 구현이 별도의 클래스로 분리되기 때문에, 각 확장 기능 및 원래 기능을 서로 영향 없이 변경할 수 있도록 만들어 준다.
즉, 데코레이터 패턴은 단일 책임 원칙을 지킬 수 있도록 만들어 준다.
데코레이터 패턴은 전략 패턴/템플릿 메서드 패턴/상태 패턴과 함께 매우 흔하게 사용되는 패턴이다.
스프링 프레임워크의 경우 트랜잭션 처리를 위해 데코레이터 패턴을 사용한다.
스프링 프레임워크에서 트랜잭션 관련 설정을 추가하면 트랜잭션 기능이 추가된 데코레이터 객체를 런타임에 생성한다.
데코레이터 패턴을 적용할 때 고려할 점
데코레이터 대상이 되는 타입의 기능 개수에 대한 것이다.
정의되어 있는 메서드가 증가하게 되면 그 만큼 데코레이터의 구현도 복잡해진다.
데코레이터 구현에서 고려해야 할 또 다른 사항은 데코레이터 객체가 비정상적으로 동작할 때 어떻게 처리할 것이냐에 대한 것이다.
예시로, 게시글 작성 이후 생성된 게시글 데이터를 외부 메시지 서버에 전송해 주는 기능을 별도의 데코레이터로 구현했다고 해보자.
이 경우 런타임의 객체 간 메시지 흐름은 다음과 같다.
외부의 메시지 서버에 장애가 발생하면 6번 과정에 문제가 발생한다.
하지만, 1~5번까지의 과정은 정상적으로 실행이 되어 트랜잭션이 커밋되었기 때문에 DB에는 새로운 데이터가 정상적으로 추가된다.
이 경우, 6번 과정의 문제가 발생되면 클라이언트에 익셉션을 발생시키는 것이 올바른지 고민해 봐야 한다.
이런 경우 메시지 전송 데코레이터는 외부 메시지 서버에 데이터 전송이 실패하더라도 익셉션을 발생시키는 대신 실패 로그를 남기는 방법을 선택할 수 있다.
이렇게 함으로써 외부 메시지 서버 연동에 실패하더라도 클라이언트는 에러 결과가 아닌 정상 결과를 볼 수 있으며, 향후에 실패 로그를 이용해서 데이터 재전송과 같은 사후 처리를 할 수 있게 된다.
데코레이터 단점
사용자 입장에서 데코레이터 객체와 실제 구현 객체의 구분이 되지 않기 때문에 코드만으로는 기능이 어떻게 동작하는지 이해하기 어렵다는 점이다.
실제 객체가 어떻게 동작하는지 알려면 런타임에 생성된 객체의 구조를 이해해야 한다.
'JAVA > 디자인 패턴' 카테고리의 다른 글
어댑터(Adapter) 패턴 (0) | 2022.03.01 |
---|---|
프록시(proxy) 패턴 (0) | 2022.03.01 |
상태(State) 패턴 (0) | 2022.03.01 |
템플릿 메서드 패턴 (0) | 2022.02.23 |
전략(Strategy) 패턴 (0) | 2022.02.23 |
- Total
- Today
- Yesterday
- BOJ
- kotest
- programmers
- Spring
- AWS
- 프로그래머스
- 이팩티브 자바
- 코테
- 백준
- node.js
- Algorithm
- 알고리즘
- MSA
- 이펙티브 자바
- C++
- Olympiad
- 클린 아키텍처
- 정규표현식
- 객체지향
- JPA
- Java
- Effective Java
- Kotlin
- Spring Boot
- 디자인패턴
- 디자인 패턴
- BAEKJOON
- 테라폼
- kkoon9
- 클린 코드
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |