목표
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; } 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; } 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; } } 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) { Beverage beverage1 = new DarkRoastWithMilk (); System.out.println(beverage1.getDescription() + " $" + beverage1.cost()); Beverage beverage2 = new DarkRoastWithMilkAndMocha (); System.out.println(beverage2.getDescription() + " $" + beverage2.cost()); } }
문제점 분석
클래스 폭발 : 토핑이 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
데코레이터 패턴 장점
개방-폐쇄 원칙 : 기존 코드를 수정하지 않고 새로운 기능을 추가할 수 있습니다.
단일 책임 원칙 : 각 데코레이터는 하나의 책임만 가집니다.
유연성 : 런타임에 객체의 기능을 조합할 수 있습니다.
확장성 : 새로운 데코레이터를 쉽게 추가할 수 있습니다.
재사용성 : 데코레이터를 다양한 컴포넌트와 조합하여 사용할 수 있습니다.
데코레이터 패턴 단점
복잡성 : 많은 데코레이터가 조합되면 코드가 복잡해질 수 있습니다.
디버깅 어려움 : 데코레이터 체인이 길어지면 디버깅이 어려울 수 있습니다.
메모리 사용량 : 각 데코레이터가 추가 객체를 생성하므로 메모리 사용량이 증가할 수 있습니다.
구성 초기화 문제 : 구성 요소가 많아지면 데코레이터 클래스 구성 요소 초기화가 복잡해질 수 있습니다.
실제 사용 예시
Java I/O : InputStream
, OutputStream
의 데코레이터들 (BufferedInputStream
, DataInputStream
등)
GUI 컴포넌트 : 스크롤바, 테두리, 배경색 등을 동적으로 추가
로깅 시스템 : 다양한 로그 레벨과 포맷을 조합
캐싱 시스템 : 다양한 캐싱 전략을 조합
정리
데코레이터 패턴은 객체에 추가 책임을 동적으로 추가할 때 사용합니다.
기존 코드를 변경하지 않고 새로운 기능을 추가할 수 있습니다.
런타임에 객체의 기능을 조합하고 싶을 때 사용합니다.
개방-폐쇄 원칙을 잘 지키는 패턴입니다.
ref