[디자인] Decorator 패턴
서론
우리가 마라탕 식당에서 다양한 재료들을 넣어 주문을 한다.
예를 들어 주꾸미가 들어간 마라탕, 양고기와 수많은 야채가 들어간 마라탕 등 이렇게 주문을 한다고 생각해보자.
종업원 측에서는 메뉴판을 구성하기 위한 비용이 많이 들 수 밖에 없다. (메뉴판을 만들다가 밤 샐 수도 있다.)
그렇다고 우리는 재료를 접시에 담아 주문을 하지, 저렇게 긴 이름으로 주문을 하지 않는다.
그래서 들어 있는 재료의 무게 및 가격을 책정하여 가격을 결정한다.
이와 같이 다양한 요소들을 하나의 부속물로 치고 상속 시키는 개념이 Decorator 패턴 이다.
정의
- Component 는 데코레이터를 추가하는데 핵심이 된다.
- 이는 추상적으로 구현되어 있어 되도록이면 변수를 정의하지 말아야 한다.
- 마라탕으로 생각하면, '마라 요리' 의 개념으로 생각해볼 수 있다.
- ConcreteComponent 는 기본 기능을 구현하는 클래스이다.
- Component 의 서브 클래스로 변수 등을 여기서 관리하면 된다.
- 마라탕으로 생각하면, '마라탕', '마라샹궈' 등이 될 수 있다.
- Decorator 는 Component 의 장식물 요소들을 관리할 수 있다.
- 주로 인터페이스를 사용하여 공통 기능을 제공한다.
- 마라탕으로 생각하면 '부속 재료' 개념으로 볼 수 있다.
- ConcreteDecorator 는 장식물의 요소들을 감싸주는 역할을 한다.
- 데코레이터 객체로, 그만의 새로운 기능들을 접목시킬 수 있다.
- 합성 (Composition) 관계 형성 : ConcreteComponent 객체 참조.
- 마라탕으로 생각하면 '오징어', '주꾸미', '고수', '연근' 등 마라탕 부재료가 될 수 있다.
특징
- 객체에 동적으로 기능 추가에 대해 간단하게 진행할 수 있다.
- 상속, 합성 관계를 사용하여 진행할 수 있기에 가능하다.
- 한 객체를 여러 개의 데코레이터로 동적으로 감쌀 수 있다.
- 마치 박스 안에 박스를 넣는 것과 같은 맥락이다.
- 데코레이터의 수퍼 클래스는 Component 의 수퍼 클래스와 같다.
- 데코레이터는 자신이 장식하고 있는 객체에게 어떤 행동을 위임하는 것 이외에 원하는 추가적인 작업을 진행할 수 있다.
- 예를 들어 마라탕 재료의 재고 관리 로직을 써도 가격 결정 프로그램에 영향이 크게 없다.
- SOLID 원칙 중의 하나인 Open Closed Principle (개방-폐쇄 원칙) 을 지킬 수 있다.
- 클래스는 확장을 개방해야 하지만, 코드 변경에 대해서는 폐쇄해야 한다.
- 데코레이터 패턴에서는 Component 데이터에 대한 코드 변경을 하지 않더라도 기능 확장을 하는데 오류를 발생하지 않아 작업 부담이 줄어들게 된다.
- 지난 번에 Java API 에서 Observer 패턴을 제공하였지만, 정작 서브 클래스를 만들어 개발을 해야 했기에 Deprecated 가 되었던 이유이다.
- 하지만 Open Closed Principle 를 너무 지키려고 노력할 필요까진 없다. 오히려 추상화 클래스들만 더 많아져 복잡해지기만 할 수도 있다.
예제
// Component 구현 : 마라 요리 클래스
public abstract class MaraDish {
private String description;
public MaraDish() {
this.description = "-- 요리 재료 목록 --\n";
}
public String getDescription() {
return description;
}
public abstract int getPrice();
}
MaraDish.java
// ConcreteComponent 구현 (1) : 마라탕
public class MaraSoup extends MaraDish {
private int price;
public MaraSoup() {
this.price = 8000;
}
public String getDescription() {
return "[[마라탕 주문]]\n마라탕\t\t8000 원\n";
}
public int getPrice() {
return price;
}
}
MaraSoup.java
// ConcreteComponent 구현 (2) : 마라샹궈
public class MaraChangguo extends MaraDish {
private int price;
public MaraChangguo() {
this.price = 16000;
}
public String getDescription() {
return "[[마라샹궈 주문]]\n마라샹궈\t\t16000 원\n";
}
public int getPrice() {
return price;
}
}
MaraChangguo.java
// Decorator 구현 : 요리 재료
public abstract class Ingredient extends MaraDish {
public abstract String getDescription();
}
Ingredient.java
// Concrete Decorator (1) 구현 : 어류 재료
public class Fish extends Ingredient {
private MaraDish maraDish;
private FishType fishType; // Enumeration Type
private int exp; // N 마리
public Fish(MaraDish maraDish, FishType fishType, int exp) {
this.maraDish = maraDish;
this.fishType = fishType;
this.exp = exp;
}
public String getDescription() {
return maraDish.getDescription() + String.format("%s(%d 개)\t\t%d 원\n", this.fishType.getValue(), this.exp, this.exp * 1000);
}
public int getPrice() {
return this.exp * 1000 + this.maraDish.getPrice();
}
}
Fish.java
// Concrete Decorator (2) 구현 : 육류 재료
public class Meat extends Ingredient {
private MaraDish maraDish;
private MeatType meatType; // Enumeration Type
private int weight; // 그램 단위
public Meat(MaraDish maraDish, MeatType meatType, int weight) {
this.maraDish = maraDish;
this.meatType = meatType;
this.weight = weight;
}
public String getDescription() {
return this.maraDish.getDescription() + String.format("%s(%d g)\t\t%d 원\n", this.meatType.getValue(), this.weight, this.weight / 100 * 1000);
}
public int getPrice() {
return this.weight / 100 * 1000 + this.maraDish.getPrice();
}
}
Meat.java
// Concrete Decorator (3) 구현 : 사리 재료
public class Noodle extends Ingredient {
private MaraDish maraDish;
private NoodleType noodleType; // Enumeration Type
private int exp; // N 인분
public Noodle(MaraDish maraDish, NoodleType noodleType, int exp) {
this.maraDish = maraDish;
this.noodleType = noodleType;
this.exp = exp;
}
public String getDescription() {
if (this.noodleType.equals(NoodleType.RAMEN)) {
return this.maraDish.getDescription() + String.format("%s(%d 인분)\t\t%d 원\n", this.noodleType.getValue(), this.exp, this.exp * 1000);
} else {
return this.maraDish.getDescription() + String.format("%s(%d 인분)\t\t%d 원\n", this.noodleType.getValue(), this.exp, this.exp * 2000);
}
}
public int getPrice() {
// 라면 사리는 1인분에 1000 원으로 책정한다.
if (this.noodleType.equals(NoodleType.RAMEN)) {
return 1000 * this.exp + this.maraDish.getPrice();
} else {
return 2000 * this.exp + this.maraDish.getPrice();
}
}
}
Noodle.java
// Concrete Decorator (4) 구현 : 채소 재료
public class Vegetable extends Ingredient {
private MaraDish maraDish;
private VegetableType vegetableType; // Enumeration Type
private int weight; // 그램 단위
public Vegetable(MaraDish maraDish, VegetableType vegetableType, int weight) {
this.maraDish = maraDish;
this.vegetableType = vegetableType;
this.weight = weight;
}
public String getDescription() {
return this.maraDish.getDescription() + String.format("%s(%d g)\t\t%d 원\n", this.vegetableType.getValue(), this.weight, this.weight / 25 * 250);
}
public int getPrice() {
return this.weight / 25 * 250 + this.maraDish.getPrice();
}
}
Vegetable.java
// Enumeration 의 값을 얻기 위한 인터페이스
public interface EnumModel {
String getKey();
String getValue();
}
EnumModel.java
public enum FishType implements EnumModel {
SQUID("오징어"), OCTOPUS("주꾸미"), SHRIMP("새우"), SKEWERS("꼬치");
private String value;
FishType(String value) {
this.value = value;
}
@Override
public String getKey() {
return name();
}
@Override
public String getValue() {
return value;
}
}
FishType.java
public enum MeatType implements EnumModel {
PORK("돼지고기"), BEEF("소고기"), LAMB("양고기"), HAM("햄"), SAUSAGE("소세지");
private String value;
MeatType(String value) {
this.value = value;
}
@Override
public String getKey() {
return name();
}
@Override
public String getValue() {
return value;
}
}
MeatType.java
public enum NoodleType implements EnumModel {
RAMEN("라면사리"), NOODLE("중국소면"), SUJEBI("수제비"), RICE_CAKE("떡볶이떡");
private String value;
NoodleType(String value) {
this.value = value;
}
@Override
public String getKey() {
return name();
}
@Override
public String getValue() {
return value;
}
}
NoodleType.java
public enum VegetableType implements EnumModel {
LOTUS_ROOT("연근"), PUMPKIN("호박"), CORIANDER("고수"), SPROUTS("숙주나물"), MUSHROOM("버섯"), BOK_CHOY("청경채"), TOFU("두부");
private String value;
VegetableType(String value) {
this.value = value;
}
@Override
public String getKey() {
return name();
}
@Override
public String getValue() {
return value;
}
}
VegetableType.java
public class Main {
public static void main(String[] args) {
MaraDish maraSoup = new MaraSoup();
maraSoup = new Fish(maraSoup, FishType.OCTOPUS, 1);
maraSoup = new Meat(maraSoup, MeatType.BEEF, 100);
maraSoup = new Noodle(maraSoup, NoodleType.SUJEBI, 1);
maraSoup = new Noodle(maraSoup, NoodleType.RAMEN, 1);
maraSoup = new Vegetable(maraSoup, VegetableType.BOK_CHOY, 25);
maraSoup = new Vegetable(maraSoup, VegetableType.MUSHROOM, 25);
maraSoup = new Vegetable(maraSoup, VegetableType.PUMPKIN, 25);
maraSoup = new Vegetable(maraSoup, VegetableType.SPROUTS, 25);
System.out.println(maraSoup.getDescription());
System.out.println("마라탕 가격 : " + maraSoup.getPrice() + " 원\n");
MaraDish maraChangguo = new MaraChangguo();
maraChangguo = new Fish(maraChangguo, FishType.SQUID, 1);
maraChangguo = new Meat(maraChangguo, MeatType.LAMB, 100);
maraChangguo = new Noodle(maraChangguo, NoodleType.RICE_CAKE, 1);
maraChangguo = new Vegetable(maraChangguo, VegetableType.SPROUTS, 25);
maraChangguo = new Vegetable(maraChangguo, VegetableType.CORIANDER, 25);
maraChangguo = new Vegetable(maraChangguo, VegetableType.BOK_CHOY, 25);
System.out.println(maraChangguo.getDescription());
System.out.println("마라샹궈 가격 : " + maraChangguo.getPrice() + " 원");
}
}
Main.java
[[마라탕 주문]]
마라탕 8000 원
주꾸미(1 개) 1000 원
소고기(100 g) 1000 원
수제비(1 인분) 2000 원
라면사리(1 인분) 1000 원
청경채(25 g) 250 원
버섯(25 g) 250 원
호박(25 g) 250 원
숙주나물(25 g) 250 원
마라탕 가격 : 14000 원
[[마라샹궈 주문]]
마라샹궈 16000 원
오징어(1 개) 1000 원
양고기(100 g) 1000 원
떡볶이떡(1 인분) 2000 원
숙주나물(25 g) 250 원
고수(25 g) 250 원
청경채(25 g) 250 원
마라샹궈 가격 : 20750 원
실행 결과
부가 설명 및 문제점
- 각 재료 별 가격 책정이 달라지는 것을 각 Concrete Decorator 클래스에 구현함으로 Component 를 직접 건들 필요가 없어지게 된다.
- 어류는 N 마리, 육류 및 채소는 N 그램, 사리는 N 인분 단위 계산.
- Concrete Component 측에서는 Decorator 의 존재 여부에 대해서 정확히 판단하기 힘들 수 있다.
- 마라샹궈를 특별 할인하는 경우에 부속 재료들에 대한 계산은 어떻게 처리할 것인가?
- 데코레이터 패턴 사용 시, Concrete Decorator 클래스들만 많아질 수도 있다.
- 위에 제시되지 않은 종류의 재료가 들어가게 될 경우.
(위에서는 햄과 어묵꼬치를 육류와 어류로 각각 통일 시켰지만, 육가공품, 어가공품 등으로 나뉘어 저장할 경우를 생각해보자.)
- 위에 제시되지 않은 종류의 재료가 들어가게 될 경우.
활용
- Java 의 File I/O API 에서 주로 사용된다.
- 예를 들어 LineInputStream 으로 file.txt 를 읽어들인다.
- LineInputStream -> BufferedInputStream -> FileInputStream 순으로 가져온다.
- 여기서 Decorator 는 InputStream 이 될 수 있다.
- Python 의 Decorator 개념 (사실 차이점이 있지만 비슷한 원리라 기재했다.)
- Java 에서 Annotation 같이 쓰는 게 Python 에선 Decorator 라고 한다.
- 공통적으로 사용하는 함수의 기능을 바인딩 해서 위에
@공통함수_이름
을 쓰면 데코레이션 기능을 사용할 수 있다. - Decorator 패턴은 위에서 언급했듯이 상속과 합성 관계를 사용하여 객체의 기능을 확장하는 것이지, 여기선 함수의 기능을 확장하기 위한 목적으로 사용한다.
Author And Source
이 문제에 관하여([디자인] Decorator 패턴), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@tails5555/디자인-Decorator-패턴저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)