디자인 패턴 - Decorator Pattern

목표

  • 데코레이터 패턴 이해 및 구현

1. 데코레이터 패턴

정의

  • 데코레이터 패턴은 객체에 추가 책임을 동적으로 추가하는 패턴입니다.
  • 기존 코드를 변경하지 않고 새로운 기능을 추가할 수 있습니다.
  • 서브클래스 구현보다 유연하게 기능을 확장할 수 있습니다.

사용 시점

  • 기존 코드를 변경하지 않고 새로운 기능을 추가할 때
  • 기존 코드를 재사용하면서 새로운 기능을 추가할 때
  • 런타임에 객체의 기능을 조합하고 싶을 때

클래스 다이어그램

classDiagram
    class Beverage {
        <<abstract>>
        #description: String
        +cost(): double
        +getDescription(): String
    }
    
    class CondimentDecorator {
        <<abstract>>
        #beverage: Beverage
        +cost(): double
        +getDescription(): String
    }
    
    class Espresso {
        +cost(): double
        +getDescription(): String
    }
    
    class HouseBlend {
        +cost(): double
        +getDescription(): String
    }
    
    class DarkRoast {
        +cost(): double
        +getDescription(): String
    }
    
    class Decaf {
        +cost(): double
        +getDescription(): String
    }
    
    class Mocha {
        -beverage: Beverage
        +Mocha(beverage: Beverage)
        +cost(): double
        +getDescription(): String
    }
    
    class Soy {
        -beverage: Beverage
        +Soy(beverage: Beverage)
        +cost(): double
        +getDescription(): String
    }
    
    class Whip {
        -beverage: Beverage
        +Whip(beverage: Beverage)
        +cost(): double
        +getDescription(): String
    }
    
    class SteamedMilk {
        -beverage: Beverage
        +SteamedMilk(beverage: Beverage)
        +cost(): double
        +getDescription(): String
    }
    
    %% 상속 관계
    Beverage <|-- Espresso : extends
    Beverage <|-- HouseBlend : extends
    Beverage <|-- DarkRoast : extends
    Beverage <|-- Decaf : extends
    Beverage <|-- CondimentDecorator : extends
    CondimentDecorator <|-- Mocha : extends
    CondimentDecorator <|-- Soy : extends
    CondimentDecorator <|-- Whip : extends
    CondimentDecorator <|-- SteamedMilk : extends
    
    %% 구성 관계 (Composition)
    CondimentDecorator *-- Beverage : has

2. 예시 코드

❌ 데코레이터 미적용 예시 코드 (문제가 있는 설계)

⚠️ 경고: 아래 코드는 데코레이터 패턴을 적용하지 않은 잘못된 설계입니다. 실제 프로젝트에서는 사용하지 마세요!

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
// 문제가 있는 설계 - 모든 조합을 위한 클래스가 필요
// 클래스 폭발 문제 발생!
public abstract class Beverage {
protected String description = "Unknown Beverage";
protected boolean milk = false;
protected boolean soy = false;
protected boolean mocha = false;
protected boolean whip = false;

public String getDescription() {
return description;
}

// 복잡하고 유지보수하기 어려운 cost() 메서드
public double cost() {
double condimentCost = 0.0;
if (hasMilk()) {
condimentCost += 0.10;
}
if (hasSoy()) {
condimentCost += 0.15;
}
if (hasMocha()) {
condimentCost += 0.20;
}
if (hasWhip()) {
condimentCost += 0.10;
}
return condimentCost;
}

// 모든 토핑에 대한 getter/setter - 확장성 부족
public boolean hasMilk() { return milk; }
public void setMilk(boolean milk) { this.milk = milk; }
public boolean hasSoy() { return soy; }
public void setSoy(boolean soy) { this.soy = soy; }
public boolean hasMocha() { return mocha; }
public void setMocha(boolean mocha) { this.mocha = mocha; }
public boolean hasWhip() { return whip; }
public void setWhip(boolean whip) { this.whip = whip; }
}

// 모든 조합을 위한 클래스들 - 클래스 폭발 문제!
// 토핑이 4개면 2^4 = 16개의 클래스가 필요!
public class DarkRoastWithMilk extends Beverage {
public DarkRoastWithMilk() {
description = "다크 로스트 커피, 스팀 밀크";
setMilk(true);
}

@Override
public double cost() {
return 0.99 + super.cost();
}
}

public class DarkRoastWithMocha extends Beverage {
public DarkRoastWithMocha() {
description = "다크 로스트 커피, 모카";
setMocha(true);
}

@Override
public double cost() {
return 0.99 + super.cost();
}
}

public class DarkRoastWithMilkAndMocha extends Beverage {
public DarkRoastWithMilkAndMocha() {
description = "다크 로스트 커피, 스팀 밀크, 모카";
setMilk(true);
setMocha(true);
}

@Override
public double cost() {
return 0.99 + super.cost();
}
}

// 더 많은 조합들... 계속해서 클래스가 늘어남
public class DarkRoastWithMilkAndMochaAndWhip extends Beverage {
public DarkRoastWithMilkAndMochaAndWhip() {
description = "다크 로스트 커피, 스팀 밀크, 모카, 휘핑 크림";
setMilk(true);
setMocha(true);
setWhip(true);
}

@Override
public double cost() {
return 0.99 + super.cost();
}
}

// 테스트 코드 - 새로운 토핑 추가 시 모든 조합 클래스 필요
public class StarbuzzCoffeeWithoutDecorator {
public static void main(String[] args) {
// 새로운 토핑이 추가되면 모든 조합의 클래스를 만들어야 함
// 토핑이 4개면 2^4 = 16개의 클래스가 필요!

Beverage beverage1 = new DarkRoastWithMilk();
System.out.println(beverage1.getDescription() + " $" + beverage1.cost());

Beverage beverage2 = new DarkRoastWithMilkAndMocha();
System.out.println(beverage2.getDescription() + " $" + beverage2.cost());

// 새로운 토핑(바닐라)이 추가되면?
// DarkRoastWithVanilla, DarkRoastWithMilkAndVanilla,
// DarkRoastWithMochaAndVanilla, DarkRoastWithMilkAndMochaAndVanilla...
// 계속해서 새로운 클래스들이 필요!
}
}

문제점 분석

  • 클래스 폭발: 토핑이 n개면 2^n개의 클래스가 필요
  • 확장성 부족: 새로운 토핑 추가 시 모든 조합의 클래스를 생성해야 함
  • 유지보수 어려움: 각 클래스마다 중복된 코드가 많음
  • 런타임 유연성 부족: 주문 시점에 토핑을 동적으로 조합할 수 없음

✅ 데코레이터 적용 예시 코드

1. 컴포넌트 인터페이스

1
2
3
4
5
6
7
8
9
public abstract class Beverage {
protected String description = "Unknown Beverage";

public String getDescription() {
return description;
}

public abstract double cost();
}

2. 구체적인 음료 클래스들

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
public class Espresso extends Beverage {
public Espresso() {
description = "에스프레소";
}

@Override
public double cost() {
return 1.99;
}
}

public class HouseBlend extends Beverage {
public HouseBlend() {
description = "하우스 블렌드 커피";
}

@Override
public double cost() {
return 0.89;
}
}

public class DarkRoast extends Beverage {
public DarkRoast() {
description = "다크 로스트 커피";
}

@Override
public double cost() {
return 0.99;
}
}

public class Decaf extends Beverage {
public Decaf() {
description = "디카페인 커피";
}

@Override
public double cost() {
return 1.05;
}
}

3. 데코레이터 추상 클래스

1
2
3
4
5
6
7
8
9
10
public abstract class CondimentDecorator extends Beverage {
protected Beverage beverage;

public CondimentDecorator(Beverage beverage) {
this.beverage = beverage;
}

@Override
public abstract String getDescription();
}

4. 구체적인 데코레이터 클래스들

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
public class Mocha extends CondimentDecorator {
public Mocha(Beverage beverage) {
super(beverage);
}

@Override
public String getDescription() {
return beverage.getDescription() + ", 모카";
}

@Override
public double cost() {
return 0.20 + beverage.cost();
}
}

public class Soy extends CondimentDecorator {
public Soy(Beverage beverage) {
super(beverage);
}

@Override
public String getDescription() {
return beverage.getDescription() + ", 두유";
}

@Override
public double cost() {
return 0.15 + beverage.cost();
}
}

public class Whip extends CondimentDecorator {
public Whip(Beverage beverage) {
super(beverage);
}

@Override
public String getDescription() {
return beverage.getDescription() + ", 휘핑 크림";
}

@Override
public double cost() {
return 0.10 + beverage.cost();
}
}

public class SteamedMilk extends CondimentDecorator {
public SteamedMilk(Beverage beverage) {
super(beverage);
}

@Override
public String getDescription() {
return beverage.getDescription() + ", 스팀 밀크";
}

@Override
public double cost() {
return 0.10 + beverage.cost();
}
}

5. 테스트 코드

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
public class StarbuzzCoffee {
public static void main(String[] args) {
// 에스프레소 주문
Beverage beverage = new Espresso();
System.out.println(beverage.getDescription() + " $" + beverage.cost());

// 다크 로스트 + 모카 + 휘핑 크림 주문
Beverage beverage2 = new DarkRoast();
beverage2 = new Mocha(beverage2);
beverage2 = new Mocha(beverage2);
beverage2 = new Whip(beverage2);
System.out.println(beverage2.getDescription() + " $" + beverage2.cost());

// 하우스 블렌드 + 두유 + 모카 + 휘핑 크림 주문
Beverage beverage3 = new HouseBlend();
beverage3 = new Soy(beverage3);
beverage3 = new Mocha(beverage3);
beverage3 = new Whip(beverage3);
System.out.println(beverage3.getDescription() + " $" + beverage3.cost());

// 디카페인 + 스팀 밀크 + 모카 주문
Beverage beverage4 = new Decaf();
beverage4 = new SteamedMilk(beverage4);
beverage4 = new Mocha(beverage4);
System.out.println(beverage4.getDescription() + " $" + beverage4.cost());
}
}

실행 결과

1
2
3
4
에스프레소 $1.99
다크 로스트 커피, 모카, 모카, 휘핑 크림 $1.39
하우스 블렌드 커피, 두유, 모카, 휘핑 크림 $1.34
디카페인 커피, 스팀 밀크, 모카 $1.35

데코레이터 패턴 장점

  1. 개방-폐쇄 원칙: 기존 코드를 수정하지 않고 새로운 기능을 추가할 수 있습니다.
  2. 단일 책임 원칙: 각 데코레이터는 하나의 책임만 가집니다.
  3. 유연성: 런타임에 객체의 기능을 조합할 수 있습니다.
  4. 확장성: 새로운 데코레이터를 쉽게 추가할 수 있습니다.
  5. 재사용성: 데코레이터를 다양한 컴포넌트와 조합하여 사용할 수 있습니다.

데코레이터 패턴 단점

  1. 복잡성: 많은 데코레이터가 조합되면 코드가 복잡해질 수 있습니다.
  2. 디버깅 어려움: 데코레이터 체인이 길어지면 디버깅이 어려울 수 있습니다.
  3. 메모리 사용량: 각 데코레이터가 추가 객체를 생성하므로 메모리 사용량이 증가할 수 있습니다.
  4. 구성 초기화 문제: 구성 요소가 많아지면 데코레이터 클래스 구성 요소 초기화가 복잡해질 수 있습니다.

실제 사용 예시

  • Java I/O: InputStream, OutputStream의 데코레이터들 (BufferedInputStream, DataInputStream 등)
  • GUI 컴포넌트: 스크롤바, 테두리, 배경색 등을 동적으로 추가
  • 로깅 시스템: 다양한 로그 레벨과 포맷을 조합
  • 캐싱 시스템: 다양한 캐싱 전략을 조합

정리

  • 데코레이터 패턴은 객체에 추가 책임을 동적으로 추가할 때 사용합니다.
  • 기존 코드를 변경하지 않고 새로운 기능을 추가할 수 있습니다.
  • 런타임에 객체의 기능을 조합하고 싶을 때 사용합니다.
  • 개방-폐쇄 원칙을 잘 지키는 패턴입니다.

ref

  • 헤드퍼스트 디자인 패턴