데코레이터 패턴이란?
기존 클래스의 기능을 유연하게 확장할 수 있도록 해준 디자인 패턴이다.
새로운 기능을 추가한 래퍼클래스로 감싸 기존의 코드를 수정하지 않고 기능을 추가할 수 있도록 한다.
예를 들어 커피숍에서 커피메뉴를 주문할 때 설탕, 우유, 휘핑크림 같은 것을 추가할 수 있는데 패턴을 사용하지 않으면 각 음료마다 이 많은 옵션들을 추가해주어야 한다. 관리도 힘들고 메뉴를 추가하기도 힘들게 된다.
하지만 데코레이터 패턴을 이용하면 커피메뉴와 추가옵션들을 분리하여 손쉽게 관리할 수 있게 된다.
왜 사용해야 하는가?
- 새 자식 클래스를 만들지 않고 기능을 확장할 수 있다.
- 객체를 여러 데코레이터로 래핑 하여 추가 기능들을 합성할 수 있다.
- 단일 책임의 원칙을 지킨다. (여러 개의 작은 클래스로 나뉨)
어떤 경우에 사용해야 하는가?
- 새로운 기능을 추가하고자 할 때, 동적으로 기능을 추가해야 하는 경우
- 기존 클래스를 수정하지 않고 새로운 기능을 추가해야 하는 경우
문제점
- 데코레이터를 많이 사용하면 클래스의 수가 증가하여 복잡성이 증가한다.
- 데코레이터의 순서에 따라 결과가 달라질 수 있다.
구현 예시
커피숍에서 에스프레소를 시킨다고 가정해보자.
package com.example.designpattern.stuctural.decorator.nodecorator;
public interface Coffee {
int cost();
}
package com.example.designpattern.stuctural.decorator.nodecorator;
public class Espresso implements Coffee{
@Override
public int cost() {
return 1500;
}
}
만약 데코레이터 패턴을 사용하지 않고 설탕추가된 에스프레소, 우유추가된 에스프레소, 휘핑크림이 추가된 에스프레소를 추가하게 된다면 각각 클래스를 만들게 될 것이다.
package com.example.designpattern.stuctural.decorator.nodecorator;
public class SugarEspresso implements Coffee{
@Override
public int cost() {
return 1500 + 500;
}
}
#---------------------------------------------------------------------------
package com.example.designpattern.stuctural.decorator.nodecorator;
public class MilkEspresso implements Coffee{
@Override
public int cost() {
return 1500 + 700;
}
}
#---------------------------------------------------------------------------
package com.example.designpattern.stuctural.decorator.nodecorator;
public class WhippedCreamDecorator implements Coffee{
@Override
public int cost() {
return 1500 + 1000;
}
}
여기까진 뭐 직접 만든다고 해도.. 만약 우유랑 설탕이 추가된 에스프레소와 같이 옵션이 두 개 이상 포함되기 시작하면 골치 아파지기 시작한다.
거기다 커피 메뉴를 추가할 때마다 이 옵션들을 직접 넣을 생각을 하면 굉장히 보기도 싫은 악취가 나는 코드가 될 것 같다..
이제 데코레이터 패턴을 적용해서 이 문제를 간단하게 해결해 보자
똑같이 에스프레소 메뉴를 만들어보자
package com.example.designpattern.stuctural.decorator.coffee;
public interface Coffee {
int cost();
}
package com.example.designpattern.stuctural.decorator.coffee;
public class Espresso implements Coffee {
@Override
public int cost() {
return 1500;
}
}
여기까진 기존의 방법과 똑같다.
여기서 이제 옵션을 추가해 줄 것인데 기존의 코드를 건들지 않기 위해서 Decorator 클래스를 만들어준다.
package com.example.designpattern.stuctural.decorator.decorator;
import com.example.designpattern.stuctural.decorator.coffee.Coffee;
public class Decorator implements Coffee {
protected Coffee coffee;
public Decorator(Coffee coffee) {
this.coffee = coffee;
}
@Override
public int cost() {
return coffee.cost();
}
}
기본 데코레이터 클래스는 단순히 인터페이스와 연결하기 위해서 만들어준 것이다.
이제 이를 이용해서 옵션들을 추가해 보자.
package com.example.designpattern.stuctural.decorator.decorator;
import com.example.designpattern.stuctural.decorator.coffee.Coffee;
public class SugarDecorator extends Decorator {
public SugarDecorator(Coffee coffee) {
super(coffee);
}
@Override
public int cost() {
return super.cost() + 500;
}
}
package com.example.designpattern.stuctural.decorator.decorator;
import com.example.designpattern.stuctural.decorator.coffee.Coffee;
public class MilkDecorator extends Decorator{
public MilkDecorator(Coffee coffee) {
super(coffee);
}
@Override
public int cost() {
return super.cost() + 700;
}
}
package com.example.designpattern.stuctural.decorator.decorator;
import com.example.designpattern.stuctural.decorator.coffee.Coffee;
public class WhippedCreamDecorator extends Decorator{
public WhippedCreamDecorator(Coffee coffee) {
super(coffee);
}
@Override
public int cost() {
return super.cost() + 1000;
}
}
이제 옵션을 여러 개 선택하는 것에 대한 추가적인 클래스를 만들지 않아도 되고
클라이언트 입장에서는 원하는 대로 옵션을 부여할 수 있게 되었다.
package com.example.designpattern.stuctural.decorator;
import com.example.designpattern.stuctural.decorator.coffee.Coffee;
import com.example.designpattern.stuctural.decorator.coffee.Espresso;
import com.example.designpattern.stuctural.decorator.decorator.MilkDecorator;
import com.example.designpattern.stuctural.decorator.decorator.SugarDecorator;
public class DecoratorClient {
public static void main(String[] args) {
Espresso espresso = new Espresso();
System.out.println("에스프레소의 가격은 " + espresso.cost());
Coffee sugarEspresso = new SugarDecorator(espresso);
System.out.println("설탕이 추가된 에스프레소의 가격은 " + sugarEspresso.cost());
Coffee milkSugarEspresso = new MilkDecorator(sugarEspresso);
System.out.println("우유와 설탕이 추가된 에스프레소의 가격은 " + milkSugarEspresso.cost());
}
}
결과
에스프레소의 가격은 1500
설탕이 추가된 에스프레소의 가격은 2000
우유와 설탕이 추가된 에스프레소의 가격은 2700
'Design Pattern > 구조 패턴(Structural patterns)' 카테고리의 다른 글
Flyweight Pattern, 플라이웨이트 패턴 (0) | 2024.02.14 |
---|---|
Facade Pattern, 퍼사드 패턴 (0) | 2024.02.07 |
Composite Pattern, 복합체 패턴 (0) | 2024.02.05 |
Bridge Pattern, 브릿지 패턴 (0) | 2024.02.05 |
Adapter Pattern, 어댑터 패턴 (0) | 2024.02.01 |