상속의 문제점과 데코레이터 패턴
데코레이터 패턴
객체에 추가 요소를 동적으로 더할 수 있는 패턴으로 서브 클래스를 사용할 때보다 훨씬 유연하게 기능을 확장할 수 있다.
기존 클래스의 문제점
초대형 커피 전문점, 베어카페를 운영하고 있다. 현재 주문 시스템 클래스는 다음과 같이 구성되어 있다.
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
public abstract class Beverage {
String description;
public String getDescription() {
return description;
}
public abstract double cost();
}
public class HouseBlend extends Beverage {
public HouseBlend() {
description = "하우스 블렌드 커피";
}
public double cost() {
return 0.89;
}
}
public class DarkRoast extends Beverage {
public DarkRoast() {
description = "다크 로스트 커피";
}
public double cost() {
return 0.99;
}
}
고객은 휘핑 크림, 우유, 시럽 등을 추가하기 원한다. 이를 위해 다음과 같은 클래스를 추가한다. HouseBlendWithMilk
, HouseBlendWithSoyMilk
,DarkRoastWithChoco
… 이와 같은 방식의 문제는 무엇일까?
클래스 개수 * 토핑 개수
만큼 클래스를 생성해야 한다.- 우유 가격이 인상되면 모든 우유가 첨가된 커피의 가격을 수정해야 한다.
- 캐러멜 시럽을 추가하면 캐러멜을 추가할 수 있는 클래스 수 만큼 클래스를 추가해야 한다.
이러한 문제는 구성인 아닌 상속을 사용했기 때문에 발생한다.
상속보다는 구성을 활용하자.
- 서브클래스를 만드는 방식으로 행동을 상속받으면 그 행동은 컴파일할 때 결정되며, 모든 서브 클래스에서 똑같은 행동을 상속받아야 한다.
- 구성으로 객체의 행동을 확장하면 실행 중에 동적으로 행동을 설정할 수 있다. 객체를 동적으로 구성하면 결과적으로 기존 코드를 고치지 않고 새로운 코드를 만들어서 기능을 추가할 수 있다.
데코레이터 패턴
구성을 사용한 데코레이터 패턴을 적용해 이 문제를 해결해보자. 데코레이터 패턴은 말 그래도 객체를 감싸서(데코레이트) 새로운 행동을 추가하는 것이다. Whip(Milk(DarkRoast))
이런식으로 감싸서 행동을 추가할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public abstract class Beverage {
String description = "Unknown Beverage";
public String getDescription() {
return description;
}
public abstract double cost();
}
public abstract class CondimentDecorator extends Beverage {
Beverage beverage; // 각 데코레이터가 감쌀 음료를 나타내는 Beverage 객체
public abstract String getDescription();
}
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class HouseBlend extends Beverage {
public HouseBlend() {
description = "House Blend Coffee";
}
public double cost() {
return .89;
}
}
public class Mocha extends CondimentDecorator {
public Mocha(Beverage beverage) {
this.beverage = beverage; // 감싸고자 하는 음료를 저장하는 인스턴스 변수
}
@Override
public double cost() {
return this.beverage.cost() + .20; // 감싼 객체의 cost() 메서드를 호출하여 그 결과에 추가적인 가격을 더한다.
}
@Override
public String getDescription() {
return beverage.getDescription() + ". Mocha";
}
}
public class Main {
public static void main(String[] args) {
Beverage beverage = new HouseBlend();
System.out.println(beverage.getDescription() + " $" + beverage.cost());
Beverage beverage2 = new HouseBlend();
beverage2 = new Mocha(beverage2);
System.out.println(beverage2.getDescription() + " $" + beverage2.cost());
Beverage beverage3 = new HouseBlend();
beverage3 = new Mocha(beverage3);
beverage3 = new Mocha(beverage3);
System.out.println(beverage3.getDescription() + " $" + beverage3.cost());
}
}
상속을 사용해서 문제가 생겼다고 했는데 여전히 상속은 하고 있다. 그러나 이번에는 상속을 사용해서 행동을 상속받는 것이 아니라, 형식을 맞추기 위한 것임을 알 수 있다.
데코레이터 패턴의 단점
- 구상 구성 요소로 어떤 작업을 처리하는 코드에 데코레이터 패턴을 적용하면 코드가 제대로 작동하지 않는다.
- 특별 할인 같은 작업을 처리하는 경우
- 데코레이터 패턴을 빼먹는 실수를 할 수 있다.
- 팩토리나 빌더 같은 다른 패턴으로 데코레이터를 만들고 사용한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14
class BeverageFactory { public static Coffee getCoffee(String type) { Beverage beverage = new HouseBlend(); if (type.contains("Milk")) { beverage = new MilkDecorator(coffee); } if (type.contains("Sugar")) { beverage = new SugarDecorator(coffee); } return beverage; } }
- 팩토리나 빌더 같은 다른 패턴으로 데코레이터를 만들고 사용한다.
This post is licensed under CC BY 4.0 by the author.