Post

상속의 문제점과 데코레이터 패턴

데코레이터 패턴

객체에 추가 요소를 동적으로 더할 수 있는 패턴으로 서브 클래스를 사용할 때보다 훨씬 유연하게 기능을 확장할 수 있다.

기존 클래스의 문제점

초대형 커피 전문점, 베어카페를 운영하고 있다. 현재 주문 시스템 클래스는 다음과 같이 구성되어 있다.

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
public abstract class Beverage {
  String description;
  public String getDescription() {
    return description;
  }
  public abstract double cost();
}

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

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

고객은 휘핑 크림, 우유, 시럽 등을 추가하기 원한다. 이를 위해 다음과 같은 클래스를 추가한다. HouseBlendWithMilk, HouseBlendWithSoyMilk,DarkRoastWithChoco … 이와 같은 방식의 문제는 무엇일까?

  1. 클래스 개수 * 토핑 개수 만큼 클래스를 생성해야 한다.
  2. 우유 가격이 인상되면 모든 우유가 첨가된 커피의 가격을 수정해야 한다.
  3. 캐러멜 시럽을 추가하면 캐러멜을 추가할 수 있는 클래스 수 만큼 클래스를 추가해야 한다.

이러한 문제는 구성인 아닌 상속을 사용했기 때문에 발생한다.

상속보다는 구성을 활용하자.

  • 서브클래스를 만드는 방식으로 행동을 상속받으면 그 행동은 컴파일할 때 결정되며, 모든 서브 클래스에서 똑같은 행동을 상속받아야 한다.
  • 구성으로 객체의 행동을 확장하면 실행 중에 동적으로 행동을 설정할 수 있다. 객체를 동적으로 구성하면 결과적으로 기존 코드를 고치지 않고 새로운 코드를 만들어서 기능을 추가할 수 있다.

데코레이터 패턴

구성을 사용한 데코레이터 패턴을 적용해 이 문제를 해결해보자. 데코레이터 패턴은 말 그래도 객체를 감싸서(데코레이트) 새로운 행동을 추가하는 것이다. Whip(Milk(DarkRoast)) 이런식으로 감싸서 행동을 추가할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public abstract class Beverage {

    String description = "Unknown Beverage";

    public String getDescription() {
        return description;
    }

    public abstract double cost();
}

public abstract class CondimentDecorator extends Beverage {

    Beverage beverage; // 각 데코레이터가 감쌀 음료를 나타내는 Beverage 객체
    public abstract String getDescription();
    
}

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
public class HouseBlend extends Beverage {

    public HouseBlend() {
        description = "House Blend Coffee";
    }

    public double cost() {
        return .89;
    }
}

public class Mocha extends CondimentDecorator {

    public Mocha(Beverage beverage) {
        this.beverage = beverage;         // 감싸고자 하는 음료를 저장하는 인스턴스 변수
    }

    @Override
    public double cost() {
        return this.beverage.cost() + .20; // 감싼 객체의 cost() 메서드를 호출하여 그 결과에 추가적인 가격을 더한다.
    }

    @Override
    public String getDescription() {
        return beverage.getDescription() + ". Mocha";
    }
}

public class Main {

    public static void main(String[] args) {
        Beverage beverage = new HouseBlend();
        System.out.println(beverage.getDescription() + " $" + beverage.cost());

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

        Beverage beverage3 = new HouseBlend();
        beverage3 = new Mocha(beverage3);
        beverage3 = new Mocha(beverage3);
        System.out.println(beverage3.getDescription() + " $" + beverage3.cost());
    }
}

상속을 사용해서 문제가 생겼다고 했는데 여전히 상속은 하고 있다. 그러나 이번에는 상속을 사용해서 행동을 상속받는 것이 아니라, 형식을 맞추기 위한 것임을 알 수 있다.

데코레이터 패턴의 단점

  • 구상 구성 요소로 어떤 작업을 처리하는 코드에 데코레이터 패턴을 적용하면 코드가 제대로 작동하지 않는다.
    • 특별 할인 같은 작업을 처리하는 경우
  • 데코레이터 패턴을 빼먹는 실수를 할 수 있다.
    • 팩토리나 빌더 같은 다른 패턴으로 데코레이터를 만들고 사용한다.
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      
      class BeverageFactory {
       public static Coffee getCoffee(String type) {
         Beverage beverage = new HouseBlend();
              
         if (type.contains("Milk")) {
             beverage = new MilkDecorator(coffee);
         }
         if (type.contains("Sugar")) {
             beverage = new SugarDecorator(coffee);
         }
              
         return beverage;
       }
      }
      
This post is licensed under CC BY 4.0 by the author.

© bear-su. Some rights reserved.