이 글은 아래 두 글과 함께보면 더 좋습니다.
한 번에 글을 보고 싶으신 분은 아래를 참고해주세요.
데코레이터 패턴
데코레이터 패턴은 기존 객체를 변경하지 않고 기능을 동적으로 추가하거나 확장할 수 있게 해주는 디자인 패턴이다.
데코레이터의 말 뜻대로 객체를 꾸며주는 역할을 한다고 생각하면 이해하기 쉽다.
구조
아래 그림에 따라 각 요소에 대한 설명과 예제를 통해 설명해보겠다.
Component
Component
는 기능을 정의하는 인터페이스이다.
구현 객체인 ConcreteComponent
와 장식자인 Decorator
가 이를 상속받아 구현하게 된다.
public interface Food {
String name();
int price();
void order();
}
ConcreteComponent
ConcreteComponent
는 인터페이스를 상속받아 실제로 구현한 구현 객체이다.
데코레이팅 될 실제 객체라고 생각하면 된다.
public class Hamburger implements Food {
@Override
public String name() {
return "햄버거";
}
@Override
public int price() {
return 7500;
}
@Override
public void order() {
System.out.println("주문 했습니다");
}
}
Decorator
Decorator
는 추상 클래스 혹은 인터페이스로 정의되어 새로운 기능이나 기존 기능에 기능을 추가하는 메서드를 선언하는 역할을 한다.
상세 기능 추가를 진행하기 위해 ConcreteDecorator
는 ****이를 상속받아 상세 내용을 구현하게 된다.
public abstract class FoodDecorator implements Food { // 추상 클래스로 구현한 경우
private Food food;
public FoodDecorator(Food food) {
this.food = food;
}
@Override
public String name() {
return food.name(); // 기능 위임
}
@Override
public int price() {
return food.price(); // 기능 위임
}
@Override
public void order() {
food.order(); // 기능 위임
}
}
interface FoodDecorator extends Food { // 인터페이스로 구현한 경우
void extraOrder();
}
ConcreteDecorator
ConcreteDecorator
는 추상 클래스 혹은 인터페이스를 상속받아 구체화 한 데코레이터이다.
public class BaconDecorator extends FoodDecorator { // 추상 클래스를 상속받아 구체화 한 데코레이터
public BaconDecorator (Food food) {
super(food);
}
@Override
public String name() {
return "베이컨" + super.name();
}
@Override
public int price() {
return super.price() + 2000;
}
@Override
public void order() {
super.order(); // 원본 객체의 operation() 실행
extraOrder(); // 추가적인 동작
}
public void extraOrder() {
System.out.println("+ 베이컨");
}
}
public class CheeseDecorator extends FoodDecorator { // 추상 클래스를 상속받아 구체화 한 데코레이터
public CheeseDecorator (Food food) {
super(food);
}
@Override
public String name() {
return "치즈" + super.name();
}
@Override
public int price() {
return super.price() + 1000;
}
@Override
public void order() {
super.order(); // 원본 객체의 operation() 실행
extraOrder(); // 추가적인 동작
}
public void extraOrder() {
System.out.println("+ 치즈");
}
}
class BaconDecorator implements FoodDecorator { // 인터페이스를 상속받아 구체화 한 데코레이터
Food food; // 원본 객체를 composition
BaconDecorator(Food food) {
this.food = food;
}
public String name() {
return "베이컨" + food.name();
}
public int price() {
return food.price() + 2000;
}
public void order() {
food.order(); // 원본 객체의 operation() 실행
extraOrder(); // 추가적인 동작
}
public void extraOrder() {
System.out.println("+ 베이컨");
}
}
class CheeseDecorator implements FoodDecorator { // 인터페이스를 상속받아 구체화 한 데코레이터
Food food; // 원본 객체를 composition
CheeseDecorator(Food food) {
this.food = food;
}
public String name() {
return "치즈" + food.name();
}
public int price() {
return food.price() + 1000;
}
public void order() {
food.order(); // 원본 객체의 operation() 실행
extraOrder(); // 추가적인 동작
}
public void extraOrder() {
System.out.println("+ 치즈");
}
}
이를 실제로 실행했을 때, 아래와 같이 동작하게 된다.
public class Main {
public static void main(String[] args) {
Food food = new Hamburger();
FoodDecorator baconBurger = new BaconDecorator(food);
FoodDecorator cheeseBurger = new CheeseDecorator(food);
FoodDecorator baconCheeseBurger = new BaconDecorator(new CheeseDecorator(food));
System.out.println("기존 객체");
System.out.println(food.name() + ", " + food.price() + "원");
food.order();
System.out.println();
System.out.println("베이컨 데코레이터");
System.out.println(baconBurger.name() + ", " + baconBurger.price() + "원");
baconBurger.order();
System.out.println();
System.out.println("치즈 데코레이터");
System.out.println(cheeseBurger.name() + ", " + cheeseBurger.price() + "원");
cheeseBurger.order();
System.out.println();
System.out.println("베이컨 치즈 데코레이터");
System.out.println(baconCheeseBurger.name() + ", " + baconCheeseBurger.price() + "원");
baconCheeseBurger.order();
}
}
기존 객체
햄버거, 7500원
주문 했습니다
베이컨 데코레이터
베이컨햄버거, 9500원
주문 했습니다
+ 베이컨
치즈 데코레이터
치즈햄버거, 8500원
주문 했습니다
+ 치즈
베이컨 치즈 데코레이터
베이컨치즈햄버거, 10500원
주문 했습니다
+ 치즈
+ 베이컨
위와 데코레이터 패턴을 적용했을 경우에는 같이 하나의 객체만 만들어 둔 후 데코레이터를 통해서 기존 객체에 기능을 추가할 수 있다.
그렇기에 핫도그나 샌드위치 같은 새로운 음식이 추가되었을 경우에도 이미 구현된 데코레이터를 통해서 베이컨과 치즈를 추가할 수 있게 되었다.
만약 데코레이터 패턴을 쓰지 않았을 경우에는 추가 기능을 위해 BaconHamburger
와 CheeseHamburger
를 새로운 구현 객체로 만들어서 사용했어야 되었을 것이다.
public class BaconHamburger implements Food {
private Food hamburger;
public BaconHamburger(Food hamburger) {
this.hamburger = hamburger;
}
@Override
public String name() {
return "베이컨" + hamburger.name();
}
@Override
public int price() {
return hamburger.price() + 2000;
}
@Override
public void order() {
hamburger.order();
System.out.println("+ 베이컨");
}
}
public class CheeseHamburger implements Food {
private Food hamburger;
public BaconHamburger(Food hamburger) {
this.hamburger = hamburger;
}
@Override
public String name() {
return "치즈" + hamburger.name();
}
@Override
public int price() {
return hamburger.price() + 1000;
}
@Override
public void order() {
hamburger.order();
System.out.println("+ 치즈");
}
}
이렇게 음식이 구현 객체로 추가되는 방식은 향후 새로운 음식이 만들었을 때도 BaconHotdog
, CheeseHotdog
, BaconSandwitch
, CheeseSandwitch
처럼 구현 객체를 모두 만들어야 하므로 데코레이터를 사용하여 장식하는 것 처럼 유연하게 대처할 수 없다는 것을 알 수 있다.
특징
장점
- 데코레이터를 통한 유연한 기능 확장이 가능하다.
- 컴파일 시점이 아닌 런타임시 동적으로 기능을 변경할 수 있다.
- 여러 데코레이터로 구현 객체를 래핑하여 여러 기능을 결합시킬 수 있다.
- 장식자 클래스마다 고유한 책임을 가지기 때문에 단일 책임 원칙(SRP)을 준수 할 수 있다.
- 직접적인 객체의 코드 수정 없이 기능 확장이 필요할 시 장식자 클래스를 추가해 사용하면 되기에 개방 폐쇄 원칙(OCP)를 준수 할 수 있다.
- 구현 객체가 아닌 인터페이스를 상속받아 구현하므로 의존 역전 원칙(DIP)를 준수 할 수 있다.
단점
- 이미 적용된 데코레이터의 경우 다시 제거하기가 어렵다.
- 런타임 시 기능을 추가하기 때문에 객체를 수정하는 과정에서 성능 저하가 발생할 수 있다.
- 여러 데코레이터를 적용하는 경우 초기화 코드가 복잡해 보일 수 있다.
- ex)
new BaconDecorator(new CheeseDecorator(food));
- ex)
참고
https://inpa.tistory.com/entry/GOF-💠-데코레이터Decorator-패턴-제대로-배워보자
https://ittrue.tistory.com/558
데코레이터 패턴 (refactoring.guru)
'오늘의 공부 > 디자인 패턴' 카테고리의 다른 글
데코레이터 패턴과 프록시 패턴의 차이 (2) | 2024.04.12 |
---|---|
프록시 패턴 (2) | 2024.04.12 |