본문 바로가기

ACC/디자인 패턴 스터디

[디자인 패턴] 3. 데코레이터 패턴

변경되는 부분에 대해 데코레이터를 이용해 재귀적으로 구성 요소 객체를 Decorate한다면, 기존 코드의 변경 없이 동적으로 확장 할 수 있다.

 


커피 전문점의 주문을 돕는 프로그램을 작성한다고 생각해보자. 만약 음료를 나타내는 수퍼 클래스 하나를 상속받는 여러 음료들의 서브 클래스를 만들어서 주문을 처리하는 것이 과연 적합할까? 직관적으로 생각해봐도 문제가 있다. 음료들은 시럽, 펄 추가 혹은 무지방 우유, 두유로 변경하는 등의 가공을 거칠 수 있다. 상속을 이용한다면 일반적인 카페 라떼를 하나 구현하기 위해 카페 라떼, 무지방 라떼, 두유 라떼, 시럽 추가 라떼, 오트 라떼 등등 굉장히 많은 서브 클래스를 필요로 하는 것을 알 수 있다.

또한 이전까지 배운 객체 지향 원칙에 어긋나는 방식임을 알 수 있다. 구성이 아닌 상속을 이용하여 행동을 처리하였기 때문에 컴파일 시점에 모든 코드가 결정되게 되고, 캡슐화를 하지 않아서 바뀌는 부분에 대해 유연한 대처를 할 수 없게 되었다. 수많은 서브 클래스(음료 + 옵션)를 컴파일 시점에 만들어서 처리하는 것이 상속의 문제이고, 우유의 가격이 인상되면 옵션만 다른 모든 라떼들의 서브 클래스의 코드를 전부 수정해야 하는 경우가 캡슐화의 문제가 될 수 있다.

 

그렇다면 수퍼 클래스가 옵션들도 관리한다면 어떨까. 직관적으로 보면 문제가 없어 보인다. 음료 한개당 하나의 클래스만 있어도 되기 때문이다. 하지만 여전히 객체 지향 원칙에 어긋나는 방식이다. 만약 휘핑 크림에 대한 옵션을 수퍼 클래스에 만들었는데, 캐모마일 티에 휘핑 크림을 추가하게 될 수도 있는 것이 상속의 문제이고, 옵션의 가격이 인상되면 모든 음료들(서브 클래스)의 코드를 전부 수정해야하는 경우가 캡슐화의 문제가 될 수 있다.

 

이를 해결하는 하나의 방법이 OCP(Open-Closed Principle) 디자인 원칙이다. OCP 원칙이란 클래스는 확장에는 열려 있어야 하지만 변경에는 닫혀 있어야 한다는 원칙이다.

그리고 OCP 디자인 원칙을 이용한 패턴이 데코레이터 패턴이다. 데코레이터 패턴을 적용하여 코드의 변경 없이 동적으로 확장할 수가 있게 된다. 어떻게 데코레이터 패턴이 OCP를 따를 수 있는지 아래의 설명과 코드로 자세히 살펴보자.

 

동일한 수퍼 클래스를 상속받는 구성 요소데코레이터를 이용하여, 구성 요소에 데코레이터를 감싸는 과정을 반복하는 것이 데코레이터 패턴이다. 마르료시카라고 생각하면 이해가 편할 것이다. 이 방식을 사용하면 데코레이터로 감싸기만 해도 확장이 가능하지만, 코드를 변경하여 확장하지 않았다. 비록 상속을 이용하지만 변경될 수 있는 행동에 대한 상속이 아닌, 데코레이터가 재귀적으로 감쌀 수 있도록 타입을 맞추기 위해 상속을 이용한 것이므로 문제가 되지 않는다.

 

예시를 들어보자면, 카페 모카는 구성 요소, 시럽 추가 및 휘핑 추가 는 데코레이터라고 생각할 수 있다.

코드로 간단하게 옮겨보자면

 

Element latte = new Latte();
latte = new Whip(latte);
latte = new Syrup(latte);

 

처럼 생각할 수 있다.

가장 외부의 데코레이터부터 재귀적으로 구성 요소에 접근하여 정보(가격 등)를 뽑아올 수 있을 것이라는 생각이 들 것이다.

 


[다이어그램]

 

데코레이터 패턴 (출처: 헤드 퍼스트 디자인 패턴)

 


[데코레이터 패턴의 단점]

 

  • 잡다한 클래스가 많이 생길 수 있다.
  • 특정 형식에 의존하는 코드에는 적용할 수가 없다.
  • 굳이 넣어서 코드를 복잡하게 만들 수 있다.

당연히 상황에 따라 적용하지 않은 경우에 생길 수 있는 단점들이다.

 


[데코레이터 적용 예시]

 

Java는 I/O에 데코레이터 패턴을 적용하였다.

 

BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

 

빠른 입출력을 위해 외워서 쓰던 이 코드도 데코레이터 패턴으로 만들어져서 저런 모습을 띄고 있는 것이었다.

 


[실습 코드]

 

abstract class Beverage {
    String description = "제목 없음";

    public String getDescription() {
        return description;
    }

    public abstract double cost();
}

////////////////////////////////////////////////////////

abstract class CondimentDecorator extends Beverage {
    Beverage beverage;
    public abstract String getDescription();
}

/////////////////////////////////////////////////////

class Espresso extends Beverage {
    public Espresso() {
        description = "에스프레소";
    }

    public double cost() {
        return 1.99;
    }
}


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

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

///////////////////////////////////////////////

class Mocha extends CondimentDecorator {
    public Mocha(Beverage beverage) {
        this.beverage = beverage;
    }

    public String getDescription() {
        return beverage.getDescription() + ", 모카";
    }

    public double cost() {
        return beverage.cost() + .20;
    }
}

class Soy extends CondimentDecorator {
    public Soy(Beverage beverage) {
        this.beverage = beverage;
    }

    public String getDescription() {
        return beverage.getDescription() + ", 두유";
    }

    public double cost() {
        return beverage.cost() + .10;
    }
}

class Whip extends CondimentDecorator {
    public Whip(Beverage beverage) {
        this.beverage = beverage;
    }

    public String getDescription() {
        return beverage.getDescription() + ", 휘핑";
    }

    public double cost() {
        return beverage.cost() + .15;
    }
}

///////////////////////////////////////////////

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

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