1. 데코레이터 패턴(Decorator Pattern)
데코레이션 패턴은 구조 패턴 중 하나로, 객체의 동작을 확장할 수 있도록 해주는 패턴이다. 기본 기능을 구현한 후 추가할 수 있는 기능의 종류가 많은 경우, Decorator 클래스로 정의 한 뒤 Decorator 객체를 조합함으로써 추가 기능을 덧붙인다.
- 데코레이터 패턴을 왜 사용하는가?
데코레이터 패턴을 사용하면 기존 코드를 변경하지 않고 새로운 기능을 추가할 수 있다. 상속보다는 구성(Composition)을 활용하여 객체의 유연성과 재사용성을 높인다.
구성(Composition)이란? 객체의 재사용과 확장을 위해 사용하는 설계 기법 중 하나로 클래스간의 "포함(has a ~)" 관계를 표현한다. 객체를 상속하는게 아닌 다른 객체를 멤버 변수로 포함하여 기능을 확장하는 방식이다.
그럼 왜 상속보다 합성을 더 자주 사용하는 이유가 무엇일까? 물론 중복 코드를 제거하고 클래스를 묶는 다형성을 이용할 수 있어서 좋은 기술처럼 보인다. 하지만 상속은 정말 개념적으로 포함되어 있을때만 제한적으로 사용한다.
- 상속을 하게 되면 부모 클래스와 자식 클래스의 관계가 컴파일 시점에 관계가 결정되기 때문에 결합도가 높아진다.
- 부모 클래스에 메소드를 추가했을 때, 자식 클래스에는 적합하지 않은 메소드를 상속할 수 있다.
- 부모 클래스에 오류가 있다면 자식 클래스에도 그대로 넘어오게 된다.
- 자식 클래스가 부모 클래스의 메소드를 오버라이딩 할 때 접근 제한자가 변경될 수 있어서 캡슐화를 위반할 수 있다.
- 다양한 조합이 필요한 상황이 오면 java는 클래스 다중 상속을 허용하지 않기 때문에 조합의 수 만큼 단일 상속을 통해 새로운 클래스를 추가해야 한다.
데코레이터 패턴을 구현하기 위한 예제를 생각해보자
기본 햄버거에 여러가지 토핑을 추가하여 다양한 햄버거 객체를 만들어볼까 한다.
interface Burger {
void make();
}
class HamBurger implements Burger {
@Override
public void make() {
System.out.println("햄버거를 만들었다!");
}
}
class TomatoCheeseBurger() {
@Override
public void make() {
System.out.println("햄버거를 만들었다!");
}
@Override
public void make() {
System.out.println("햄버거를 만들었다!");
addTomato();
addCheese();
}
public void addTomato() {
System.out.println("토마토 추가");
}
public void addCheese() {
System.out.println("치즈 추가");
}
}
class CheeseBurger() {
@Override
public void make() {
System.out.println("햄버거를 만들었다!");
}
@Override
public void add() {
System.out.println("햄버거를 만들었다!");
addCheese();
}
public void addCheese() {
System.out.println("치즈 추가");
}
}
데코레이터 패턴없이 구현하게 된다면 여러가지 토핑을 추가할 때마다 클래스를 계속 추가하게 된다.
- 데코레이터 패턴 구현
따라서 토핑을 추가한 상태의 버거를 일일이 구현하는것이 아닌 각 토핑을 미리 정의해주고, new BurgerTopping(new Tomato(new Cheese())) 이런식으로 생성자를 감싸듯이 구성(Composition)하여 자유롭게 동적으로 토핑을 추가할 수 있다.
// 원본 객체와 장식된 객체 모두를 묶는 인터페이스
interface Burger {
void make();
}
// 장식될 원본 객체
class HamBurger implements Burger {
@Override
public void make() {
System.out.print("햄버거를 만들었다!");
}
}
// 장식자 추상 클래스
abstract class BurgerTopping implements Burger {
private Burger burger;
BurgerTopping(Burger burger) {this.burger = burger;}
@Override
public void make() {
burger.make();
}
}
// 장식자 클래스 (토마토 토핑 추가 버거)
class Tomato extends BurgerTopping {
Tomato(Burger burger) {super(burger);}
@Override
public void make() {
addTomato();
super.make();
}
public void addTomato() {
System.out.println("토마토 추가");
}
}
// 장식자 클래스 (치즈 토핑 추가 버거)
class Cheese extends BurgerTopping {
Cheese(Burger burger) {super(burger);}
@Override
public void make() {
addCheese();
super.make();
}
public void addCheese() {
System.out.println("치즈 추가");
}
}
public class Customer {
public static void main(String[] args) {
// 토마토 버거
Burger tomatoBurger = new Tomato(new HamBurger());
tomatoBurger.make();
// 토마토 치즈 버거
Burger tomatoCheeseBurger = new Tomato(new Cheese(new HamBurger()));
tomatoCheeseBurger.make();
}
}
토마토 추가
햄버거를 만들었다!
토마토 추가
치즈 추가
햄버거를 만들었다!
이렇게 장식자 클래스를 여러개 만들어 준 후 자유롭게 원하는 햄버거를 만들때마다 구성(Composition)을 통해 추가해 줄 수 있다. 또한, 데코레이션의 순서는 가장 밖에 wrapping 되어있는 순서로 진행된다.
- 데코레이터 패턴 원리
Burger tomatoCheeseBurger = new Tomato(new Cheese(new HamBurger()));
tomatoCheeseBurger.make();
토마토 치즈 버거를 만들 때 이렇게 구성(Composition)을 하게 되면 super.make() 메소드가 각 상위 장식자의 메소드로 교체되어 결과값이 변하게 된다. 결국 tomatoCheesBurger 객체를 클래스로 나타낸다면 아래와 같이 나타낼 수 있다.
class 토마토치즈버거 extends BurgerTopping {
@Override
public void make() {
addTomato(); // Tomato 클래스로 장식
addCheese(); // Cheese 클래스로 장식
System.out.println("햄버거를 만들었다!"); // Burger 클래스(원본 클래스)에서 make()
}
public void addTomato() {
System.out.println("토마토 추가");
}
public void addCheese() {
System.out.println("치즈 추가");
}
}
- 데코레이터 패턴 순서
데코레이터 패턴은 구성(Composition)하는 순서를 주의해야 한다. 예를 들어 토마토와 치즈토핑을 추가한 햄버거를 만들 때 토마토를 먼저 추가하느냐, 치즈를 먼저 추가하느냐는 다른 방식으로 wrapping 해야한다.
public class CustomerEx {
public static void main(String[] args) {
// 치즈 토마토 버거
Burger cheeseTomatoBurger = new Cheese(new Tomato(new HamBurger()));
cheeseTomatoBurger.make();
// 토마토 치즈 버거
Burger tomatoCheeseBurger = new Tomato(new Cheese(new HamBurger()));
tomatoCheeseBurger.make();
}
}
치즈 추가
토마토 추가
햄버거를 만들었다!
토마토 추가
치즈 추가
햄버거를 만들었다!
이렇게 데코레이터 패턴은 여러가지 장식자로 기본 객체에 많은 조합을 할 수 있는 상황일 때 사용한다.
'JAVA' 카테고리의 다른 글
[JAVA] Heap 메모리 구조와 Garbage Collection (2) | 2024.12.08 |
---|---|
[JAVA] 중첩클래스, 중첩인터페이스 (1) | 2024.12.03 |
[JAVA] 디자인패턴 - 전략 패턴(Strategy Pattern)을 알아보자 (0) | 2024.12.02 |
[JAVA] 오버라이딩 & 오버로딩 (Overriding & Overloading) (0) | 2024.12.01 |
[JAVA] 추상클래스와 인터페이스의 차이점? (0) | 2024.12.01 |