[디자인] 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 패턴은 위에서 언급했듯이 상속과 합성 관계를 사용하여 객체의 기능을 확장하는 것이지, 여기선 함수의 기능을 확장하기 위한 목적으로 사용한다.

좋은 웹페이지 즐겨찾기