준비하는 대학생

[Design Pattern] 데코레이터 패턴(Decorator Pattern) 본문

Programming/Design pattern

[Design Pattern] 데코레이터 패턴(Decorator Pattern)

Bangii 2023. 4. 10. 14:31

데코레이터 패턴(Decorator Pattern)은 객체지향 프로그래밍에서 객체의 기능을 동적으로 추가하거나 변경할 수 있는 디자인 패턴입니다. 데코레이터 패턴은 런타임 시에 객체에 새로운 기능을 추가하거나 기존 기능을 변경하는 데 사용되며, 코드의 수정 없이도 객체의 행위를 변경할 수 있습니다.

1. 데코레이터 패턴의 구조

데코레이터 패턴은 다음과 같은 구성 요소로 이루어져 있습니다:

  1. Component: 인터페이스 또는 추상 클래스로, 모든 객체가 구현해야 하는 공통의 기능을 정의합니다.
  2. ConcreteComponent: Component를 구현하는 실제 객체로, 기본 기능을 제공합니다.
  3. Decorator: Component를 구현하는 추상 클래스로, ConcreteComponent에 추가 기능을 제공하는 역할을 합니다.
  4. ConcreteDecorator: Decorator를 구현하는 실제 객체로, ConcreteComponent에 추가되는 구체적인 기능을 정의합니다.

2. 데코레이터 패턴 예제:  JAVA로 음료 가격 계산하기

이 예제에서는 데코레이터 패턴을 사용하여 음료의 가격을 계산하는 간단한 프로그램을 작성해 보겠습니다. 음료에는 여러 가지 토핑을 추가할 수 있으며, 토핑에 따라 가격이 달라집니다.

2.1. Component 정의하기

먼저, 음료와 토핑을 추상화한 Beverage 인터페이스를 정의합니다.

public interface Beverage {
    double cost();
    String getDescription();
}

2.2. ConcreteComponent 정의하기

Beverage 인터페이스를 구현하는 Coffee와 Tea 클래스를 정의합니다.

public class Coffee implements Beverage {
    @Override
    public double cost() {
        return 2.0;
    }

    @Override
    public String getDescription() {
        return "커피";
    }
}

public class Tea implements Beverage {
    @Override
    public double cost() {
        return 1.5;
    }

    @Override
    public String getDescription() {
        return "차";
    }
}

2.3. Decorator 정의하기

Beverage 인터페이스를 구현하는 추상 클래스 ToppingDecorator를 정의합니다.

public abstract class ToppingDecorator implements Beverage {
    protected Beverage beverage;

    public ToppingDecorator(Beverage beverage) {
        this.beverage = beverage;
    }
}

2.4. ConcreteDecorator 정의하기

ToppingDecorator를 구현하는 MilkCaramel 클래스를 정의합니다. 각각의 클래스에서는 추가되는 토핑에 따른 가격과 설명을 구현합니다.

public class Milk extends ToppingDecorator {
    public Milk(Beverage beverage) {
        super(beverage);
    }

    @Override
    public double cost() {
        return beverage.cost() + 0.5;
    }

    @Override
    public String getDescription() {
        return beverage.getDescription() + ", 우유";
    }
}

public class Caramel extends ToppingDecorator {
    public Caramel(Beverage beverage) {
        super(beverage);
    }

    @Override
    public double cost() {
        return beverage.cost() + 0.7;
    }

    @Override
    public String getDescription() {
        return beverage.getDescription() + ", 캐러멜";
    }
}

2.5. 데코레이터 패턴 사용하기

이제 데코레이터 패턴을 사용하여 음료에 토핑을 추가하고, 가격을 계산하는 코드를 작성합니다.

public class Main {
    public static void main(String[] args) {
        Beverage coffee = new Coffee();
        coffee = new Milk(coffee);
        coffee = new Caramel(coffee);

        System.out.println(coffee.getDescription() + " 가격: $" + coffee.cost());

        Beverage tea = new Tea();
        tea = new Milk(tea);

        System.out.println(tea.getDescription() + " 가격: $" + tea.cost());
    }
}

위의 예제에서는 커피에 우유와 캐러멜 토핑을 추가하고, 차에는 우유만 추가한 후 각 음료의 가격을 계산합니다.

3. 이점

3.1 유연성 (OCP 준수)

 데코레이터 패턴을 사용하면 객체의 기능을 동적으로 추가하거나 변경할 수 있습니다. 예제에서 보았듯이, Coffee와 Tea 객체에 Milk와 Caramel 토핑을 자유롭게 추가할 수 있었습니다. 이를 통해 다양한 조합의 음료를 쉽게 생성할 수 있으며, 새로운 토핑이 추가되더라도 기존의 코드를 수정할 필요가 없습니다.

Beverage coffee = new Coffee();
coffee = new Milk(coffee);
coffee = new Caramel(coffee);

3.2 확장성 (OCP 준수)

 데코레이터 패턴은 새로운 기능을 추가하기 용이하게 합니다. 예를 들어, 새로운 토핑 Chocolate를 추가하려면 기존의 코드를 수정할 필요 없이 새로운 ConcreteDecorator 클래스만 생성하면 됩니다. 이렇게 하면 기존의 코드에 영향을 주지 않으면서도 새로운 기능을 쉽게 확장할 수 있습니다.

public class Chocolate extends ToppingDecorator {
    public Chocolate(Beverage beverage) {
        super(beverage);
    }

    @Override
    public double cost() {
        return beverage.cost() + 0.6;
    }

    @Override
    public String getDescription() {
        return beverage.getDescription() + ", 초콜릿";
    }
}

3.3 Single Responsibility Principle (SRP) 준수

 데코레이터 패턴을 사용하면 각 클래스는 자신의 책임에 충실합니다. 이를 통해 코드의 유지보수가 용이해집니다. 예를 들어, Coffee와 Tea 클래스는 각각 음료의 기본 가격과 설명에만 집중할 수 있으며, 토핑에 대한 가격과 설명은 각각의 데코레이터 클래스에서 처리하게 됩니다. 이렇게 함으로써 각 클래스는 한 가지 역할에만 집중하게 되어 코드의 가독성과 유지보수성이 향상됩니다.

3.4 코드 재사용성

 데코레이터 패턴은 기능을 조합하여 다양한 객체를 생성할 수 있습니다. 이를 통해 중복 코드를 줄이고 코드의 재사용성을 높일 수 있습니다. 예를 들어, 각각의 토핑 클래스는 해당 토핑에 대한 기능만 구현하면 되므로, 다양한 음료에 동일한 토핑을 적용할 수 있습니다. 이렇게 하면 코드의 중복을 피하고 각 클래스의 기능을 재사용할 수 있습니다.

Beverage coffeeWithMilk = new Milk(new Coffee());
Beverage teaWithMilk = new Milk(new Tea());

3.5 결합도 감소 

 데코레이터 패턴을 사용하면 객체 간의 결합도를 낮출 수 있습니다. 예제에서 Coffee와 Tea 클래스는 각각의 토핑 클래스와 직접적인 관계가 없으며, 모든 토핑 클래스는 공통 인터페이스인 Beverage를 구현하게 됩니다. 이렇게 함으로써 클래스 간의 결합도를 낮추고 코드의 유연성을 높일 수 있습니다.

이와 같은 이점들 덕분에 데코레이터 패턴은 객체의 기능을 동적으로 확장하거나 변경하는 데 매우 유용한 디자인 패턴입니다. 프로젝트의 요구 사항에 따라 데코레이터 패턴을 사용하여 객체의 기능을 조합하고 확장함으로써, 유연하고 확장 가능한 코드를 작성할 수 있습니다.

4. 결론

데코레이터 패턴은 객체지향 프로그래밍에서 객체의 기능을 동적으로 확장하거나 변경할 수 있는 강력한 디자인 패턴입니다. 이 패턴을 사용하면 코드의 수정 없이도 객체의 행위를 변경할 수 있으며, 런타임 시에 객체에 새로운 기능을 추가하거나 기존 기능을 변경할 수 있습니다. 이 포스트에서는 자바를 사용하여 데코레이터 패턴을 구현하는 예제를 살펴보았습니다. 이를 통해 데코레이터 패턴의 이해가 더욱 깊어졌기를 바랍니다. 이 패턴을 사용하면 객체의 기능을 동적으로 확장하거나 변경할 수 있어 프로그램의 유연성과 확장성이 높아집니다. 다양한 프로젝트에서 데코레이터 패턴을 활용하여 효과적인 코드를 작성해 보세요.

Comments