[디자인 패턴] 데코레이터 패턴(Decorator Pattern) By starseat 2023-08-13 20:26:50 알고리즘 Post Tags # 데코레이터(decorate, 장식) 패턴 - 객체에 추가 요소를 동적으로 더할 수 있다. - 데코레이터를 사용하면 서브 클래스를 만들 때보다 훨씬 유연하게 기능을 확장할 수 있다. - 데코레이터의 슈퍼클래스는 자신이 장식하고 있는 객체의 슈퍼클래스와 같다. - 한 객체를 여러 개의 데코레이터로 감쌀 수 있다. - 데코레이터는 자신이 감싸고 있는 객체아ㅘ 같은 슈퍼클래스를 가지고 있기에 원래 객체(싸여 있는 객체)가 들어갈 자리에 데코레이터 객체를 넣어도 상관 없다. - **데코레이터는 자신이 장식하고 있는 객체에게 어떤 행동을 위임하는 일 말고도 추가 작업을 수행할 수 있다.** *(key point)* - 객체는 언제든지 감쌀 수 있으므로 샐행 중에 필요한 데코레이터를 마음대로 적용할 수 있다. ## 주의할 점(단점) - 데코레이터 패턴을 사용하면 관리해야 할 객체가 늘어나 코딩할 떄 실수할 가능성이 높다. - *하지만 실제로는 팩토리나 빌더 같은 다른 패턴으로 데코레이터를 만들고 사용한다.* - 자잘한 클래스가 엄청나게 추가되는 경우가 있어 남들이 볼 때 이해하기 힘든 디자인이 만들어지곤 한다. - *단순 장식을 위한 래퍼 클래스라고 생각하면 한결 마음이 편해진다.* - 특정 형식에 의존하는 클라이언트 코드를 가지고 제대로 생각해 보지 않은 채 데코레이터 패턴을 적용하는 사람이 많다. 그래서 데코레이터를 끼워 넣어도 클라이언트는 데코레이터를 사용하고 있다는 사실을 전혀 알 수 없다. - 데코레이터를 도입하면 구성 요소를 초기화 하는데 필요한 코드가 훨씬 복잡해 진다. # 데코레이터 패턴 구조 ![image.png](/uploads/_temp/20230813/ed50ae142210e6318e0c39f0f0ca4aa2.png) # 데코레이터 패턴 적용 예제 - 카페 음료 주문 모카와 휘핑크림을 추가한 다크 로스트 커피를 주문하는 예제 이다. 이 예제에선 다음과 같이 장식할 수 있다. 1. DarkRoast 객체를 가져온다. 2. Mocha 객체로 장식한다. 3. Whip 객체로 장식한다. 4. cost() 메소드를 호출한다. 이때 첨가물의 가격을 계산하는 일은 해당 객체에게 위임한다. ## 클래스 다이어그램 ![image.png](/uploads/_temp/20230813/b5c9f5599253df76b1a116099827713d.png) - CondimentDecorator 에서 Beverage 클래스를 확장하고 있는데 이는 상속이라기 보단 데코레이터 형식이 그 데코레이터로 감싸는 객체의 혀익과 같다는 점이 중요한 부분이다. - 그래서 데코레이터 패턴에서는 상속을 사용해서 형식을 맞추는 것이다. - 행동을 상속받으려고 Beverage 의 서브클래스를 만든게 아니라 형식을 맞추려고 한 것이다. - 행동은 기본 구성 요소와는 다른 데코레이터 등을 인스턴스 변수에 저장하는 식으로 연결한다. - 객체 구성(인스턴스 변수로 다른 객체를 저장하는 방식)을 이용하고 있어 음료에 첨가물을 다양하게 추가해도 유연성을 잃지 않을 수 있다. - 데코레이터를 언제든지 구현해서 새로운 행동을 추가할 수 있다. ## 구현 ### 기초 작업 - 추상 클래스 - **Beverage** (음료) - 추상 클래스가 아닌 인터페이스 여도 되지만. 원 소스 코드가 추상 클래스 였다고 가정한다. ```java public abstract class Beverage { public enum Size { TALL, GRANDE, VENTI }; Size size = Size.TALL; String decription = "no decription."; public String getDecription() { return decription; } public void setSize(Size size) { this.size = size; } public Size getSize() { return this.size; } public abstract double cost(); } ``` - **CondimentDecorator** - 첨가물(condiment) 장식 ```java public abstract class CondimentDecorator extends Beverage { Beverage beverage; public abscract String getDecription(); public Size getSize() { return beverage.getSize(); } } ``` ### 음료 코드 구현 - **HouseBlend** ```java public class HouseBlend extends Beverage { public HouseBlend() { description = "하우스 블렌드 커피"; } public double cost() { return 0.89; } } ``` - **Espresso** ```java public class Espresso extends Beverage { public Espresso() { description = "에스프레소"; } public double cost() { return 1.99; } } ``` - **DarkRoast** ```java public class DarkRoast extends Beverage { public DarkRoast() { description = "다크 로스트 커피"; } public double cost() { return 0.99; } } ``` - **Decaf** ```java public class Decaf extends Beverage { public Decaf() { description = "디카페인 커피"; } public double cost() { return 1.05; } } ``` ### 첨가물 코드 구현 - **Milk** ```java public class Milk extends CondimentDecorator { public Milk(Beverage beverage) { this.beverage = beverage; } public String getDecription() { return beverage.getDescription() + ", 우유"; } public double cost() { double cost = beverage.cost() + 0.20; if (beverage.getSize() == Size.TALL) { cost += 0.10; } else if (beverage.getSize() == Size.GRANDE) { cost += 0.15; } else if (beverage.getSize() == Size.GRANDE) { cost += 0.20; } return cost; } } ``` - **Mocha** ```java public class Mocha extends CondimentDecorator { public Mocha(Beverage beverage) { this.beverage = beverage; } public String getDecription() { return beverage.getDescription() + ", 모카"; } public double cost() { return beverage.cost() + 0.20; if (beverage.getSize() == Size.TALL) { cost += 0.10; } else if (beverage.getSize() == Size.GRANDE) { cost += 0.15; } else if (beverage.getSize() == Size.GRANDE) { cost += 0.20; } return cost; } } ``` - **Soy** ```java public class Soy extends CondimentDecorator { public Soy(Beverage beverage) { this.beverage = beverage; } public String getDecription() { return beverage.getDescription() + ", 두류"; } public double cost() { double cost = beverage.cost() + 0.15; if (beverage.getSize() == Size.TALL) { cost += 0.10; } else if (beverage.getSize() == Size.GRANDE) { cost += 0.15; } else if (beverage.getSize() == Size.GRANDE) { cost += 0.20; } return cost; } } ``` - **Whip** ```java public class Whip extends CondimentDecorator { public Whip(Beverage beverage) { this.beverage = beverage; } public String getDecription() { return beverage.getDescription() + ", 휘핑크림"; } public double cost() { double cost = beverage.cost() + 0.12; if (beverage.getSize() == Size.TALL) { cost += 0.10; } else if (beverage.getSize() == Size.GRANDE) { cost += 0.15; } else if (beverage.getSize() == Size.GRANDE) { cost += 0.20; } return cost; } } ``` ### 테스트 ```java public class CustomCafe { 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()); } } ``` # 데코레이터 적용 예: 자바 I/O `java.io` 패키지가 대표적으로 데코레이터 패턴이 적용된 예 이다. `java.io` 패키지에는 엄청나게 많은 클래스가 있다. 자세한 내용은 [구글 검색: java.io](https://www.google.com/search?q=java.io&sca_esv=556524961&rlz=1C1YTUH_koKR1011KR1011&sxsrf=AB5stBj-AitqqlAuUesQRRWcobVQvke-lg%3A1691931328535&ei=wNLYZL6QIIqH2roP8bqF6Ac&ved=0ahUKEwi-8Miu19mAAxWKg1YBHXFdAX0Q4dUDCA8&uact=5&oq=java.io&gs_lp=Egxnd3Mtd2l6LXNlcnAiB2phdmEuaW8yBBAjGCcyChAAGIAEGBQYhwIyChAAGIAEGBQYhwIyBRAAGIAEMgUQABiABDIFEAAYgAQyBRAAGIAEMgUQABiABDIFEAAYgAQyBRAAGIAESKwCUABYAHAAeAGQAQCYAXGgAXGqAQMwLjG4AQPIAQD4AQHiAwQYACBBiAYB&sclient=gws-wiz-serp) 에서 확인해보자. ## 자바 I/O 커스텀 데코레이터 만들기 `java.io` 가 데코레이터임을 알았으니 입력 스트림(Input Stream) 에 있는 대문자를 전부 소문자로 바꿔주는 데코레이터를 만들어 보자. - **LowerCaseInputStream** ```java public class LowerCaseInputStream extends FileInputStream { public LowerCaseInputStream(InputStream in) { super(in); } public in read() throws IOException { int c = in.read(); return (c == -1 ? c : Character.toLowerCase((char)c)); } public int read(byte[] b, int offset, int len) throws IOException { int result = in.read(b, offset, len); for (int i=offset; i= 0) { System.out.print((char) c); } in.close(); } catch (IOException e) { e.printStackTrace(); } } } ``` # 디자인 원칙 ## 디자인 원칙 5 - **OCP(Open-Closed Principle)** - 기존 코드를 건드리지 않고 *확장으로 새로운 행동을 추가하는 것.* - 옵저버 패턴을 생각해보면 옵저버를 새로 추가하면 주제에 코드를 추가하지 않으면서도 얼마든지 확장할 수 있다. - 데코레이터 패턴으로 OCP 를 준수하는 방법을 이해할 수 있다. 옵저버 패턴을 생각해보면 옵저버를 새로 추가하면 주제에 코드를 추가하 # 참조 - [헤드 퍼스트 디자인 패턴](https://product.kyobobook.co.kr/detail/S000001810483) - [[Java] 자바 I/O 에 대한 이해](https://www.ccm3.net/archives/21118#gsc.tab=0) Previous Post [디자인 패턴] 옵저버 패턴(Observer Pattern) Next Post [디자인 패턴] 팩토리 패턴(Factory Pattern)