...
Decorator Pattern
데코레이터 패턴(Decorator Pattern)은 대상 객체에 대한 기능 확장이나 변경이 필요할때 객체의 결합을 통해 서브클래싱 대신 쓸수 있는 유연한 대안 구조 패턴이다.
Decorator을 해석하자면 '장식자' 라는 의미를 가지고 있는데, 마치 기본 제품에 포장지나 외부 디자인을 살짝 변경해 줌으로써 새로운 기능을 부여하는 것과 같이, 객체 지향 프로그래밍에서 원본 객체에 대해서 무언가를 장식하여 더 멋진 기능을 가지게 만드는 것이기 때문에 이 명칭이 붙었다고 보면 된다.
데코레이터 패턴을 이용하면 필요한 추가 기능의 조합을 런타임에서 동적으로 생성할 수 있다. 데코레이터할 대상 객체를 새로운 행동들을 포함한 특수 장식자 객체에 넣어서 행동들을 해당 장식자 객체마다 연결시켜, 서브클래스로 구성할때 보다 훨씬 유연하게 기능을 확장 할 수 있다. 그리고 기능을 구현하는 클래스들을 분리함으로써 수정이 용이해진다.
데코레이터 패턴 구조
- Component (Interface) : 원본 객체와 장식된 객체 모두를 묶는 역할
- ConcreteComponent : 원본 객체 (데코레이팅 할 객체)
- Decorator : 추상화된 장식자 클래스
- 원본 객체를 합성(composition)한 wrappee 필드와 인터페이스의 구현 메소드를 가지고 있다
- ConcreteDecorator : 구체적인 장식자 클래스
- 부모 클래스가 감싸고 있는 하나의 Component를 호출하면서 호출 전/후로 부가적인 로직을 추가할 수 있다.
데코레이터 패턴 흐름
클래스 구조
// 원본 객체와 장식된 객체 모두를 묶는 인터페이스
interface IComponent {
void operation();
}
// 장식될 원본 객체
class ConcreteComponent implements IComponent {
public void operation() {
}
}
// 장식자 추상 클래스
abstract class Decorator implements IComponent {
IComponent wrappee; // 원본 객체를 composition
Decorator(IComponent component) {
this.wrappee = component;
}
public void operation() {
wrappee.operation(); // 위임
}
}
// 장식자 클래스
class ComponentDecorator1 extends Decorator {
ComponentDecorator1(IComponent component) {
super(component);
}
public void operation() {
super.operation(); // 원본 객체를 상위 클래스의 위임을 통해 실행하고
extraOperation(); // 장식 클래스만의 메소드를 실행한다.
}
void extraOperation() {
}
}
class ComponentDecorator2 extends Decorator {
ComponentDecorator2(IComponent component) {
super(component);
}
public void operation() {
super.operation(); // 원본 객체를 상위 클래스의 위임을 통해 실행하고
extraOperation(); // 장식 클래스만의 메소드를 실행한다.
}
void extraOperation() {
}
}
클래스 흐름
public class Client {
public static void main(String[] args) {
// 1. 원본 객체 생성
IComponent obj = new ConcreteComponent();
// 2. 장식 1 하기
IComponent deco1 = new ComponentDecorator1(obj);
deco1.operation(); // 장식된 객체의 장식된 기능 실행
// 3. 장식 2 하기
IComponent deco2 = new ComponentDecorator2(obj);
deco2.operation(); // 장식된 객체의 장식된 기능 실행
// 4. 장식 1 + 2 하기
IComponent deco3 = new ComponentDecorator1(new ComponentDecorator2(obj));
}
}
데코레이터 된 객체는 메서드를 호출할때 장식한 메서드를 호출하여 반환 로직에 추가적으로 더 덧붙여서 결과값을 반환할 수 있다. 장식 중첩 부분만 시퀀스 다이어그램으로 나타내면 다음과 같이 표현 된다.
IComponent deco = new ComponentDecorator1(new ComponentDecorator2(new ConcreteComponent()));
데코레이터 패턴 특징
패턴 사용 시기
- 객체 책임과 행동이 동적으로 상황에 따라 다양한 기능이 빈번하게 추가/삭제되는 경우
- 객체의 결합을 통해 기능이 생성될 수 있는 경우
- 객체를 사용하는 코드를 손상시키지 않고 런타임에 객체에 추가 동작을 할당할 수 있어야 하는 경우
- 상속을 통해 서브클래싱으로 객체의 동작을 확장하는 것이 어색하거나 불가능 할 때
패턴 장점
- 데코레이터를 사용하면 서브클래스를 만들때보다 훨씬 더 유연하게 기능을 확장할 수 있다.
- 객체를 여러 데코레이터로 래핑하여 여러 동작을 결합할 수 있다.
- 컴파일 타임이 아닌 런타임에 동적으로 기능을 변경할 수 있다.
- 각 장식자 클래스마다 고유의 책임을 가져 단일 책임 원칙(SRP)을 준수
- 클라이언트 코드 수정없이 기능 확장이 필요하면 장식자 클래스를 추가하면 되니 개방 폐쇄 원칙(OCP)을 준수
- 구현체가 아닌 인터페이스를 바라봄으로써 의존 역전 원칙(DIP) 준수
패턴 단점
- 만일 장식자 일부를 제거하고 싶다면, Wrapper 스택에서 특정 wrapper를 제거하는 것은 어렵다.
- 데코레이터를 조합하는 초기 생성코드가 보기 안좋을 수 있다.
new A(new B(new C(new D()))) - 어느 장식자를 먼저 데코레이팅 하느냐에 따라 데코레이터 스택 순서가 결정지게 되는데, 만일 순서에 의존하지 않는 방식으로 데코레이터를 구현하기는 어렵다.
예제를 통해 알아보는 Decorator 패턴
라이플 악세서리 설계
데코레이터 패턴 클래스 구조 예제 중에 재미있는 그림이 있어서 소개해본다.
기본 기능만 있는 총(BaseWeapon)을 여러 악세서리(WeaponAccessory)를 총에 장착(Decorator)시킴으로써 유탄 발사도 되고 스코프 기능도 되는 멋진 총 객체를 구성하려고 한다.
클린하지 않은 문제의 코드 ❌
만일 데코레이터 패턴을 생각지 못하고 다양한 악세서리 기능을 장착한 라이플 클래스를 구성하려고 했으면 어떻게 했을까? 아무래도 유탄발사기가 되는 총을 원하면 GeneradeBaseWeapon 클래스를 따로 정의하고, 스코프가 달린 총을 원하면 ScopedBaseWeapon 클래스를 따로 정의하고, 개머리판을 견착하고 조준하여 유탄발사하는 총을 원하면 ButtstockScopedGeneradeBaseWeapon 클래스를 정의해 인스턴스화 하여 사용해왔을 것이다.
class BaseWeapon implements Weapon {
@Override
public void aim_and_fire() {
System.out.println("총알 발사");
}
}
class GeneradeBaseWeapon implements Weapon {
@Override
public void aim_and_fire() {
System.out.println("총알 발사");
}
public void generade_fire() {
System.out.println("유탄 발사");
}
}
class ScopedBaseWeapon implements Weapon {
@Override
public void aim_and_fire() {
aiming();
System.out.println("조준하여 총알 발사");
}
public void aiming() {
System.out.println("조준 중..");
}
}
class ButtstockScopedGeneradeBaseWeapon implements Weapon {
@Override
public void aim_and_fire() {
holding();
aiming();
System.out.println("조준하여 총알 발사");
}
public void generade_fire() {
System.out.println("유탄 발사");
}
public void aiming() {
System.out.println("조준 중..");
}
public void holding() {
System.out.println("견착 완료");
}
}
이런식으로 구성하면 라이플 악세서리가 종류가 많아지면 많아질수록 클래스가 계속 추가되게 되고 코드량이 길어져 결국 유지보수하기 매우 불편하게 된다.
데코레이터 패턴을 적용한 코드 ✔️
따라서 각 악세서리를 장착한 상태의 라이플 클래스를 일일히 구현하는게 아니라, 각기 무기 악세서리 클래스들을 미리 정의해주고, new BaseWeapon(new Generage(new Scoped())) 이런식으로 생성자를 감싸듯이 구성하여, 마치 동적으로 무기 악세서리를 자유롭게 붙이도록 구성해주면 된다.
- 먼저 대상 클래스와 장식자 클래스를 모두 묶어 다형성 처리를 위한 Weapon 인터페이스를 선언한다.
- 데코레이터를 추상화한 WeaponAccessory를 선언한다. 굳이 추상 클래스를 선언하는 이유는 동기화 처리 외에 또다른 처리 기능이 추가되었을 때 유연하게 확장키 위해서 이고 각 장식자 클래스의 중복되는 코드를 묶기 위해서이다.
- WeaponAccessory추상 클래스를 상속하는 각 무기 악세서리 Generade, Scoped, Buttstock 서브 장식 클래스 구현체를 선언한다.
- 만일 새로운 악세서리를 추가할게 있다면 간단하게 WeaponAccessory를 상속한 장식자 클래스를 추가해주기만 하면 된다.
// 원본 객체와 장식된 객체 모두를 묶는 인터페이스
interface Weapon {
void aim_and_fire();
}
// 장식될 원본 객체
class BaseWeapon implements Weapon {
@Override
public void aim_and_fire() {
System.out.println("총알 발사");
}
}
// 장식자 추상 클래스
abstract class WeaponAccessory implements Weapon {
private Weapon rifle;
WeaponAccessory(Weapon rifle) { this.rifle = rifle; }
@Override
public void aim_and_fire() {
rifle.aim_and_fire(); // 위임
}
}
// 장식자 클래스 (유탄발사기)
class Generade extends WeaponAccessory {
Generade(Weapon rifle) { super(rifle); }
@Override
public void aim_and_fire() {
super.aim_and_fire(); // 부모 메서드를 호출함으로써 자신을 감싸고 있는 장식자의 메서드를 호출
generade_fire();
}
public void generade_fire() {
System.out.println("유탄 발사");
}
}
// 장식자 클래스 (조준경)
class Scoped extends WeaponAccessory {
Scoped(Weapon rifle) { super(rifle); }
@Override
public void aim_and_fire() {
aiming();
super.aim_and_fire(); // 부모 메서드를 호출함으로써 자신을 감싸고 있는 장식자의 메서드를 호출
}
public void aiming() {
System.out.println("조준 중..");
}
}
// 장식자 클래스 (개머리판)
class Buttstock extends WeaponAccessory {
Buttstock(Weapon rifle) { super(rifle); }
@Override
public void aim_and_fire() {
holding();
super.aim_and_fire(); // 부모 메서드를 호출함으로써 자신을 감싸고 있는 장식자의 메서드를 호출
}
public void holding() {
System.out.println("견착 완료");
}
}
public class Client {
public static void main(String[] args) {
// 1. 유탄발사기가 달린 총
Weapon generade_rifle = new Generade(new BaseWeapon());
generade_rifle.aim_and_fire();
// 2. 개머리판을 장착하고 스코프를 달은 총
Weapon buttstock_scoped_rifle = new Buttstock(new Scoped(new BaseWeapon()));
buttstock_scoped_rifle.aim_and_fire();
// 3. 유탄발사기 + 개머리판 + 스코프가 달린 총
Weapon buttstock_scoped_generade_rifle = new Buttstock(new Scoped(new Generade(new BaseWeapon())));
buttstock_scoped_generade_rifle.aim_and_fire();
}
}
데코레이터 순서는 원본 대상 객체 생성자를 장식자 생성자가 래핑(wrapping) 하는 형태로 간다고 보면 된다.
ex)new 장식자( new 원본() )
데코레이터 원리 파악하기 🤔
데코레이터의 특징은 아래 코드와 같이 객체의 생성자의 생성자의 생성자 형식이다.
Weapon buttstock_scoped_generade_rifle = new Buttstock(new Scoped(new Generade(new BaseWeapon())));
buttstock_scoped_generade_rifle.aim_and_fire();
이렇게 구성을 하면 각 장식자 클래스의 부모메서드 호출 부분 super.aim_and_fire() 메서드가 각 상위 장식자의 메서드로 교체되어 위의 결과와 같이 결과값이 변하게 되는 것이다. 위의 buttstock_scoped_generade_rifle 객체를 클래스 코드로 표현하자면 마치 여러개의 클래스가 합쳐져 다음과 같이 구성되는 것과 비슷하다고 보면 된다.
class 장식된최종클래스 extends WeaponAccessory {
@Override
public void aim_and_fire() {
holding(); // Buttstock 클래스로부터 장식됨
aiming(); // Scoped 클래스로부터 장식됨
System.out.println("총알 발사"); // BaseWeapon 원본 클래스 동작
generade_fire(); // Generade 클래스로부터 장식됨
}
public void holding() {
System.out.println("견착 완료");
}
public void aiming() {
System.out.println("조준 중..");
}
public void generade_fire() {
System.out.println("유탄 발사");
}
}
데코레이터 순서 주의사항 ⚠️
데코레이터는 간단한 래핑(wrapping) 원리 패턴인것 같지만, 어느 장식자를 먼저 감싸느냐에 따라 그에 대한 행동 패턴이 완전히 달라지게 된다. 예를들어 라이플 예제에서 개머리판을 먼저 부착하고 스코프를 부착하느냐, 아니면 스코프를 부착하고 개머리판을 부착하느냐 장식 순서에 따라 총을 발사하는 사람의 행동이 다음과 같이 달라진다.
public static void main(String[] args) {
// 1. 개머리판을 장착하고 스코프를 달은 총
Weapon buttstock_scoped_rifle = new Buttstock(new Scoped(new BaseWeapon()));
buttstock_scoped_rifle.aim_and_fire();
// 2. 개머리판을 장착하고 스코프를 달은 총
Weapon scoped_buttstock_rifle = new Scoped(new Buttstock(new BaseWeapon()));
scoped_buttstock_rifle.aim_and_fire();
}
이 부분은 은근히 햇깔리는 로직이므로 처음 데코레이터를 배우는 입장에선 굉장히 난해할수 있다. 이에 대한 심화 과정은바로 다음 데코레이터 구현 예제에서 다룬다.
실전 자바 메서드의 동기화 처리
이번에는 좀 더 실무에 적합한 예제로 데코레이터 패턴을 연습해보자. 만일 다음과 같은 Data 클래스를 멀티쓰레드 환경에서도 사용할 수 있도록 동기화(syncronized) 처리를 해주고 싶다고 가정해보자.
class MyData {
private int data;
public void setData(int data) {
this.data = data;
}
public int getData() {
return data;
}
}
클린하지 않은 문제의 코드 ❌
가장 심플한 솔루션은 MyData 클래스를 상속하여 메서드를 오버라이딩하여 syncronized 처리하는 로직으로 재정의 하는 것이다.
class SynchronizedData extends MyData {
private int data;
public void setData(int data) {
// synchronized (대상 객체) {} 블럭
synchronized (this) {
this.data = data;
}
}
public int getData() {
synchronized (this) {
return data;
}
}
}
public class Client {
public static void main(String[] args) {
SynchronizedData data = new SynchronizedData();
data.setData(1);
System.out.println(data.getData());
}
}
코드 동작 자체는 문제는 없다. 그러나 위에서도 경험했듯이, 만일 동기화 처리 외에 다른 부가 처리 기능도 계속 요구사항이 들어온다면 서브 클래스 폭발이 일어나게 된다. 더군다나 원본 MyData의 data 필드 변수가 private 로 설계되어있기 때문에 상속으로 재사용이 불가능하여 서브 클래스에 또 변수를 선언하여 사용하였다. 결국 뭔가 아다리가 맞지 않은채 구성이 뒤죽박죽 된 느낌이 든다.
데코레이터 패턴을 적용한 코드 ✔️
이처럼 데코레이터 패턴은 기능 확장이 필요할 때 서브 클래싱 대신 쓸 수 있는 유연한 대안으로서 적용할 수 있다.
- 먼저 동기화 처리가 안된 data 클래스와 동기화 처리가 된 data 클래스 모두를 묶어두는 IData 인터페이스를 선언한다.
- 데코레이터를 추상화한 MyDataDecorator를 선언한다. 굳이 추상 클래스를 선언하는 이유는 동기화 처리 외에 또다른 처리 기능이 추가되었을 때 유연하게 확장키 위해서 이고 각 장식자 클래스의 중복되는 코드를 묶기 위해서이다.
- MyDataDecorator 추상 클래스를 상속하는 서브 장식 클래스 구현체 SynchronizedDecorator를 선언한다.
- 또다른 부가 기능을 장식할게 있다면 간단하게 MyDataDecorator를 상속한 장식자 클래스를 추가해주기만 하면 된다.
// 원본 객체와 장식된 객체 모두를 묶는 인터페이스
interface IData{
void setData(int data);
int getData();
}
// 장식될 원본 객체
class MyData implements IData{
private int data;
public void setData(int data) {
this.data = data;
}
public int getData() {
return data;
}
}
// 장식자 추상 클래스
abstract class MyDataDecorator implements IData {
private IData mydataObj; // 최상위 인터페이스 타입으로 장식할 원본 객체 선언
MyDataDecorator(IData mydataObj) {
this.mydataObj = mydataObj;
}
public void setData(int data) {
this.mydataObj.setData(data);
}
public int getData() {
return mydataObj.getData();
}
}
// 장식자 클래스
class SynchronizedDecorator extends MyDataDecorator {
SynchronizedDecorator(IData mydataObj) {
super(mydataObj);
}
public void setData(int data) {
synchronized (this) {
System.out.println("동기화된 data 처리를 시작합니다.");
super.setData(data); // 부모 메서드를 호출함으로써 자신을 감싸고 있는 장식자의 메서드를 호출
System.out.println("동기화된 data를 처리를 완료하였 습니다.");
}
}
public int getData() {
synchronized (this) {
System.out.println("동기화된 data 처리를 시작합니다.");
int result = super.getData();
System.out.println("동기화된 data를 처리를 완료하였 습니다.");
return result;
}
}
}
// 나중에 기능 추가 요구사항이 와도 코드 수정없이 유연하게 클래스를 정의만 해주면 된다.
class AnotherSkillDecorator extends MyDataDecorator {
private IData mydataObj;
AnotherSkillDecorator(IData mydataObj) {
super(mydataObj);
}
// ...
}
public class Client {
public static void main(String[] args) {
// 동시성이 필요없을 때
IData data = new MyData();
// 동시성이 필요할 때
IData dataSync = new SynchronizedDecorator(data);
dataSync.setData(1);
System.out.println(dataSync.getData()); // 1
}
}
이러한 자바 코드의 동기화 처리는 실제 컬렉션 프레임워크 에서도 지원하는데, 대표적으로 Collections.synchronizedList() 등이 있다. new Decorator(new Data) 와 같은 new 생성자를 감싸는 형태는 아니지만 객체를 인자로 받아 기능을 장식시킨 강화된 객체를 만들어 반환한다는 점에서 결은 같다고 볼 수 있다.
List<String> list = new ArrayList<String>();
List<String> syncList = Collections.synchronizedList(list);
추가 데코레이터 구현하기 ✔️
하나만 하고 넘어가기에는 섭하니, 실제 추가 장식 기능을 구현해 보겠다. 추가할 장식 기능은 원본 객체의 메서드 로직 실행 시간을 측정하는 기능을 구현해볼 예정이다. 우선 AnotherSKillDecorator 클래스명을 timerMeasureDecorator로 변경한다. 그리고 장식자 클래스를 아래와 같이 구성한다.
class timerMeasureDecorator extends MyDataDecorator {
timerMeasureDecorator(IData mydataObj) {
super(mydataObj);
}
public void setData(int data) {
long startTime = System.nanoTime(); // 코드 시작 시간
super.setData(data);
long endTime = System.nanoTime(); // 코드 끝난 시간
long durationTimeSec = endTime - startTime;
System.out.println(durationTimeSec + "n/s"); // 나노세컨드 출력
}
public int getData() {
long startTime = System.nanoTime(); // 코드 시작 시간
int result = super.getData();
long endTime = System.nanoTime(); // 코드 끝난 시간
long durationTimeSec = endTime - startTime;
System.out.println(durationTimeSec + "n/s"); // 나노세컨드 출력
return result;
}
}
public class Client {
public static void main(String[] args) {
IData data = new MyData();
// 시간 측정 하고 싶을 때
IData data1 = new timerMeasureDecorator(data);
data1.setData(1);
// 동시성이 적용된 로직 안의 코드를 시간 측정 하고 싶을 때
IData data2 = new SynchronizedDecorator(new timerMeasureDecorator(data));
data2.setData(1);
// 동시성이 적용된 코드를 시간 측정 하고 싶을 떄
IData data3 = new timerMeasureDecorator(new SynchronizedDecorator(data));
data3.setData(1);
}
}
데코레이터 순서 파악하기 🤔
위의 실행 코드를 보면 알듯이, 어느걸 먼저 장식하느냐에 따라 이행되는 효과가 완전히 달라지게 된다. '동시성이 적용된 로직 안의 코드를 시간 측정' 과 '동시성이 적용된 코드를 시간 측정' 은 어찌 보면 말이야 방구야 할수도 있겠지만 중첩으로 장식된 코드 로직 부분들을 풀어내면, 어떤 순서대로 데코레이팅 하느냐에 따라 완전히 달라지게 된다. 따라서 장식자를 감쌀때 올바른 로직인지 개발자는 끊임 없이 검토를 수행하여야 한다.
// 동시성이 적용된 로직 안의 코드를 시간 측정 하고 싶을 때
IData data2 = new SynchronizedDecorator(new timerMeasureDecorator(data));
data2.setData(1);
// 동시성이 적용된 코드를 시간 측정 하고 싶을 떄
IData data3 = new timerMeasureDecorator(new SynchronizedDecorator(data));
data3.setData(1);
실무에서 찾아보는 Decorator 패턴
Java
- InputStream, OutputStream, Reader, Writer의 생성자를 활용한 랩퍼
- java.util.Collections가 제공하는 메소드들 활용한 랩퍼
- javax.servlet.http.HttpServletRequest / ResponseWrapper
- java.io.InputStream, OutputStream, Reader, Writer의 모든 하위 클래스에 동일한 유형의 인스턴스를 사용하는 생성자
- java.util.Collections의 checkedXXX(), synchronizedXXX(), unmodifiableXXX() 메서드들
- javax.servlet.http.HttpServletRequestWrapper 그리고 HttpServletResponseWrapper
- javax.swing.JScrollPane
자바 I/O 메서드
- InputStream, OutputStream, Reader, Writer의 생성자를 활용한 파일 I/O 랩퍼 부분은 데코레이터 패턴의 대표적인 예이다.
- 자바 코드에서 파일을 읽어 들일 때 다음과 같이 객체 생성자를 중첩하여 사용해왔다.
- File → FileReader → BufferedReader 순으로 갈수록 점점 부가 기능이 가미되었다고 보면 된다.
- 이처럼 유연하게 기능을 확장하여 사용할 수 있겠지만 대신 각각 장식자 클래스들이 어떠한 기능을 수행하는지 알고 있어야하고 역시나 자잘한 클래스들이 많이 생기는 단점이 존재한다.
/* read file example */
BufferedReader br = new BufferedReader(new FileReader(new File("test.txt")));
이 BufferedReader 예제는 어댑터 패턴(Adaptor Pattern) 포스팅에서도 다룬적이 있는데, 패턴의 목적에 따라 어댑터 패턴이라 볼 수도 있고 데코레이터 패턴이라 볼 수도 있기 때문이다. 결국은 호환 작업도 일종의 부가 기능 추가를 통한 효과라 비스무리하다고 볼 수 있다. 이처럼 디자인 패턴은 서로서로 비슷한 부분이 많이 존재한다.
java.util.Collections가 제공하는 메소드
- checkedXXX() : 기존 컬렉션 인스턴스를 부가적인 기능을 추가해서 다른 타입으로 변환해주는 메소드
- synchronizedXXX() : 컬렉션의 메서드 로직을 동기화 처리하는 기능을 가미하는 메소드
- unmodifiableXXX() : 컬렉션을 불변 객체로 취급하게 함 (읽기 전용)
class Book {
}
class Item {
}
public class Client {
public static void main(String[] args) {
// 어떠한 타입도 들어갈 수 있는 컬렉션
List list = new ArrayList<>();
list.add(new Book());
list.add(new Hashtable());
list.add(new Double(1));
// list 컬렉션에서 Book 클래스에 해당하는 타입만 선별해서 books에 담도록 재설정
list = Collections.checkedList(list, Book.class);
list.add(new Book()); // 가능
list.add(new Item()); // ! ClassCastException (타입 Cast 불가)
}
}
public class Client {
public static void main(String[] args) {
/* Collections가 제공하는 데코레이터 메소드 */
// 어떠한 타입도 들어갈 수 있는 컬렉션
List list = new ArrayList<>();
list.add(new Book());
list.add(new Hashtable());
list.add(new Double(1));
List unmodifiableList = Collections.unmodifiableList(list);
unmodifiableList.add(new Book()); // ! 넣기 불가능 (불변객체)
}
}
Spring Framework
HttpServletRequestWrapper / HttpServletResponseWrapper
- 서블릿에서 제공해주는 Wrapper로 이 역시 일종의 데코레이터 패턴이라고 볼 수 있다.
- HttpServletRequest를 확장해서 HttpServletRequestWrapper가 제공하는 기능을 오버라이딩해서 부가적인 기능을 추가할 수 있다. 예를들어 HTTP 요청 메시지 본문을 다르게 처리해서 본다. 캐싱한다. 로깅을 남긴다. 의심스러운 요청 확인 등등의 작업을 해야할 때, 이런 wrapper를 만들어서 사용할 수 있다.
- wrapper을 만들어서 HttpServletRequest를 담고, filter를 거치도록 하면, 항상 이 wrapper을 거쳐서 요청이 처리된다.
public class RequestWrapper extends HttpServletRequestWrapper {
private ObjectMapper objectMapper;
private byte[] httpRequestBodyByteArray;
private ByteArrayInputStream bis;
public RequestWrapper(HttpServletRequest request) {
super(request);
this.objectMapper = new ObjectMapper();
try {
this.httpRequestBodyByteArray = StreamUtils.copyToByteArray(request.getInputStream());
this.bis = new ByteArrayInputStream(httpRequestBodyByteArray);
} catch (IOException e) {
throw new PreamtreeException();
}
}
@Override
public ServletInputStream getInputStream() {
return new ServletInputStream() {
@Override
public boolean isFinished() {
return bis.available() == 0;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener readListener) {
return;
}
@Override
public int read() {
return bis.read();
}
};
}
public Object convertToObject() throws IOException {
if(httpRequestBodyByteArray.length == 0) return null; // body가 비어있더라도 잘 처리하도록..
return objectMapper.readValue(httpRequestBodyByteArray, Object.class);
}
}
BeanDefinitionDecorator
- 빈(Bean) 설정 데코레이터로 스프링의 인프라로 등록되어있다. (직접 사용할 일은 없다)
ServerHttpRequestDecorator / ServerHttpResponseDecorator
- Webflux HTTP 요청/응답 데코레이터
- ServerHttpRequest 와 ServerHttpResponse를 커스터마이징 하는 인터페이스
- 이 데코레이터를 상속받는 클래스를 만들어서 WebFilter를 거쳐가는 모든 요청이 이 데코레이터의 하위 클래스를 거쳐가게 된다.
public class CachingServerHttpRequestDecorator extends ServerHttpRequestDecorator {
@Getter
private final OffsetDateTime timestamp = OffsetDateTime.now();
private final StringBuilder cachedBody = new StringBuilder();
CachingServerHttpRequestDecorator(ServerHttpRequest delegate) {
super(delegate);
}
@Override
public Flux<DataBuffer> getBody() {
return super.getBody().doOnNext(this::cache);
}
@SneakyThrows
private void cache(DataBuffer buffer) {
cachedBody.append(UTF_8.decode(buffer.asByteBuffer())
.toString());
}
public String getCachedBody() {
return cachedBody.toString();
}
# 참고자료
코딩으로 학습하는 GoF의 디자인 패턴 - 백기선
https://refactoring.guru/design-patterns/decorator
이 글이 좋으셨다면 구독 & 좋아요
여러분의 구독과 좋아요는
저자에게 큰 힘이 됩니다.