본문 바로가기
Design Pattern/구조 패턴(Structural patterns)

Decorator Pattern, 데코레이터 패턴

by codeyaki 2024. 2. 6.
반응형

데코레이터 패턴이란?

기존 클래스의 기능을 유연하게 확장할 수 있도록 해준 디자인 패턴이다.

새로운 기능을 추가한 래퍼클래스로 감싸 기존의 코드를 수정하지 않고 기능을 추가할 수 있도록 한다.

예를 들어 커피숍에서 커피메뉴를 주문할 때 설탕, 우유, 휘핑크림 같은 것을 추가할 수 있는데 패턴을 사용하지 않으면 각 음료마다 이 많은 옵션들을 추가해주어야 한다. 관리도 힘들고 메뉴를 추가하기도 힘들게 된다.

하지만 데코레이터 패턴을 이용하면 커피메뉴와 추가옵션들을 분리하여 손쉽게 관리할 수 있게 된다.

 

왜 사용해야 하는가?

  • 새 자식 클래스를 만들지 않고 기능을 확장할 수 있다.
  • 객체를 여러 데코레이터로 래핑 하여 추가 기능들을 합성할 수 있다.
  • 단일 책임의 원칙을 지킨다. (여러 개의 작은 클래스로 나뉨)

어떤 경우에 사용해야 하는가?

  • 새로운 기능을 추가하고자 할 때, 동적으로 기능을 추가해야 하는 경우
  • 기존 클래스를 수정하지 않고 새로운 기능을 추가해야 하는 경우

문제점

  • 데코레이터를 많이 사용하면 클래스의 수가 증가하여 복잡성이 증가한다.
  • 데코레이터의 순서에 따라 결과가 달라질 수 있다.

구현 예시

 

커피숍에서 에스프레소를 시킨다고 가정해보자.

package com.example.designpattern.stuctural.decorator.nodecorator;

public interface Coffee {
    int cost();
}
package com.example.designpattern.stuctural.decorator.nodecorator;

public class Espresso implements Coffee{
    @Override
    public int cost() {
        return 1500;
    }
}

 

만약 데코레이터 패턴을 사용하지 않고 설탕추가된 에스프레소, 우유추가된 에스프레소, 휘핑크림이 추가된 에스프레소를 추가하게 된다면 각각 클래스를 만들게 될 것이다.

package com.example.designpattern.stuctural.decorator.nodecorator;

public class SugarEspresso implements Coffee{

    @Override
    public int cost() {
        return 1500 + 500;
    }
}


#---------------------------------------------------------------------------

package com.example.designpattern.stuctural.decorator.nodecorator;

public class MilkEspresso implements Coffee{
    @Override
    public int cost() {
        return 1500 + 700;
    }
}

#---------------------------------------------------------------------------

package com.example.designpattern.stuctural.decorator.nodecorator;

public class WhippedCreamDecorator implements Coffee{
    @Override
    public int cost() {
        return 1500 + 1000;
    }
}

 

여기까진 뭐 직접 만든다고 해도.. 만약 우유랑 설탕이 추가된 에스프레소와 같이 옵션이 두 개 이상 포함되기 시작하면 골치 아파지기 시작한다.

거기다 커피 메뉴를 추가할 때마다 이 옵션들을 직접 넣을 생각을 하면 굉장히 보기도 싫은 악취가 나는 코드가 될 것 같다.. 

 

이제 데코레이터 패턴을 적용해서 이 문제를 간단하게 해결해 보자

 


똑같이 에스프레소 메뉴를 만들어보자

package com.example.designpattern.stuctural.decorator.coffee;

public interface Coffee {
    int cost();
}
package com.example.designpattern.stuctural.decorator.coffee;

public class Espresso implements Coffee {
    @Override
    public int cost() {
        return 1500;
    }
}

 

여기까진 기존의 방법과 똑같다.

여기서 이제 옵션을 추가해 줄 것인데 기존의 코드를 건들지 않기 위해서 Decorator 클래스를 만들어준다.

package com.example.designpattern.stuctural.decorator.decorator;

import com.example.designpattern.stuctural.decorator.coffee.Coffee;

public class Decorator implements Coffee {
    protected Coffee coffee;

    public Decorator(Coffee coffee) {
        this.coffee = coffee;
    }

    @Override
    public int cost() {
        return coffee.cost();
    }

}

 

기본 데코레이터 클래스는 단순히 인터페이스와 연결하기 위해서 만들어준 것이다.

이제 이를 이용해서 옵션들을 추가해 보자.

 

package com.example.designpattern.stuctural.decorator.decorator;

import com.example.designpattern.stuctural.decorator.coffee.Coffee;

public class SugarDecorator extends Decorator {
    public SugarDecorator(Coffee coffee) {
        super(coffee);
    }

    @Override
    public int cost() {
        return super.cost() + 500;
    }
}
package com.example.designpattern.stuctural.decorator.decorator;

import com.example.designpattern.stuctural.decorator.coffee.Coffee;

public class MilkDecorator extends Decorator{

    public MilkDecorator(Coffee coffee) {
        super(coffee);
    }

    @Override
    public int cost() {
        return super.cost() + 700;
    }
}
package com.example.designpattern.stuctural.decorator.decorator;

import com.example.designpattern.stuctural.decorator.coffee.Coffee;

public class WhippedCreamDecorator extends Decorator{
    public WhippedCreamDecorator(Coffee coffee) {
        super(coffee);
    }

    @Override
    public int cost() {
        return super.cost() + 1000;
    }
}

이제 옵션을 여러 개 선택하는 것에 대한 추가적인 클래스를 만들지 않아도 되고 

클라이언트 입장에서는 원하는 대로 옵션을 부여할 수 있게 되었다.

package com.example.designpattern.stuctural.decorator;

import com.example.designpattern.stuctural.decorator.coffee.Coffee;
import com.example.designpattern.stuctural.decorator.coffee.Espresso;
import com.example.designpattern.stuctural.decorator.decorator.MilkDecorator;
import com.example.designpattern.stuctural.decorator.decorator.SugarDecorator;

public class DecoratorClient {
    public static void main(String[] args) {

        Espresso espresso = new Espresso();
        System.out.println("에스프레소의 가격은 " + espresso.cost());

        Coffee sugarEspresso = new SugarDecorator(espresso);
        System.out.println("설탕이 추가된 에스프레소의 가격은 " + sugarEspresso.cost());

        Coffee milkSugarEspresso = new MilkDecorator(sugarEspresso);
        System.out.println("우유와 설탕이 추가된 에스프레소의 가격은 " + milkSugarEspresso.cost());

    }
}

결과

에스프레소의 가격은 1500
설탕이 추가된 에스프레소의 가격은 2000
우유와 설탕이 추가된 에스프레소의 가격은 2700

 

반응형