디자인 패턴 & OOP

[디자인 패턴] 데코레이터 패턴(Decorator Pattern)

cloud-grace 2024. 5. 19. 15:48

데코레이터 패턴(Decorator Pattern)이란?

데코레이터 패턴은 디자인 패턴(Design Pattern) 중 구조 패턴(Structural Pattern)이다.

구조 패턴 : 클래스나 객체를 조합해서 더 큰 구조를 만드는 패턴
예를 들어, 서로 다른 인터페이스를 지닌 객체 2개를 묶어서 단일 인터페이스를 제공하거나 객체들을 서로 묶어서 새로운 기능을 제공하는 패턴이다.

 

GoF 디자인 패턴에 의하면 데코레이터 패턴은 주어진 상황 및 용도에 따라 어떤 객체에 다른 객체를 덧붙이는 방식이다.

 

사용 목적은 클래스의 요소들을 계속 수정하면서 사용하는 구조가 필요할 때, 여러 요소들을 조합해서 사용하는 구조일 때 사용한다. 예를 들어, 캐싱, 로깅, 검증과 같은 기능에 사용된다.

데코레이터 패턴 : 주어진 상황과 용도에 따라 어떤 객체에 책임(기능)을 덧붙이는 패턴으로, 기능 확장이 필요할 때 서브클래싱 대신 쓸 수 있는 유연한 대안이 될 수 있다. 말 그대로 장식이라고 생각하면 된다. 기본 기능을 가지고 있는 클래스를 만들고 여기다가 추가할 수 있는 기능들을 추가하기 편하도록 설계하는 방식이다.

 

따라서 데코레이터 패턴은 기본 틀에 새롭게 추가될 수 있는 정보들을 따로 분리하면서, 확장하기 용이하도록 융통성을 제공한다. 기존 객체를 '행위'를 가진 특별한 래퍼 객체(데코레이터)에 넣어 객체가 그 '행위'를 할 수 있게 만들어준다.

데코레이터 패턴 구조

출처 : 위키백과

  • Component : 부모 Class로 구성 요소들을 의미하는 추상 클래스이자, 구체적인 객체와 데코레이터가 구현해야 하는 인터페이스이다. operation을 하위 클래스가 재정의하도록 한다. 실질적인 인스턴스를 컨트롤하는 역할을 한다.
  • ConcreteComponent : 기본 기능을 구현하는 구체적인 클래스이다. Component의 실질적인 인스턴스 부분으로 책임의 주체 역할을 한다.
  • Decorator : 인터페이스를 구현하며, Component 객체를 포함하는 추상 클래스이다. 부수적인 기능, 장식이 되는 기능을 추가한다. Component와 ConcreteDecorator를 동일하게 해주는 역할을 한다.
  • ConcreteDecorator : 데코레이터의 구체적인 구현 클래스이다. Decorator를 상속 받아 구현한다. 상속이 아닌 aggregation을 이용한다는 점에서 더욱 융통적이다. 즉, 이 클래스는 Component 객체에 새로운 기능을 추가한다. 실질적인 장식 인스턴스와 정의이며 추가된 책임의 주체이다.

데코레이터 패턴 예제 코드

예제 : 커피 제조

  • Component : 커피를 제조할 때 비용과 재료 설명을 필요로 하며 이 함수를 Component 인터페이스에서 구현한다.
interface Coffee {
    double cost(); // 비용
    String description(); // 재료 설명
}
  • ConcreteComponent : Component를 상속 받아 기본 커피의 비용은 5.0, 재료 설명은 Simple Coffee로 정의한다.
class SimpleCoffee implements Coffee {
    @Override
    public double cost() {
        return 5.0;
    }

    @Override
    public String description() {
        return "Simple Coffee";
    }
}
  • Decorator : 커피 비용과 재료 설명의 근간이 되는 추상 클래스이다. 재료는 이 Decorator를 상속 받아 재료를 추가한다.
abstract class CoffeeDecorator implements Coffee {
    protected Coffee decoratedCoffee;

    public CoffeeDecorator(Coffee coffee) {
        this.decoratedCoffee = coffee;
    }

    @Override
    public double cost() {
        return decoratedCoffee.cost();
    }

    @Override
    public String description() {
        return decoratedCoffee.description();
    }
}
  • ConcreteDecorator : MilkDecorator는 우유를 추가하는 것으로 cost()와 description()을 정의해준다.
class MilkDecorator extends CoffeeDecorator {
    public MilkDecorator(Coffee coffee) {
        super(coffee);
    }

    @Override
    public double cost() {
        return decoratedCoffee.cost() + 1.5;
    }

    @Override
    public String description() {
        return decoratedCoffee.description() + ", Milk";
    }
}
  • ConcreteDecorator : SugarDecorator는 설탕을 추가하는 것으로 cost()와 description()을 정의해준다.
class SugarDecorator extends CoffeeDecorator {
    public SugarDecorator(Coffee coffee) {
        super(coffee);
    }

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

    @Override
    public String description() {
        return decoratedCoffee.description() + ", Sugar";
    }
}
  • Main : 클라이언트 코드는 SimpleCoffee 객체를 생성하고 필요에 따라 데코레이터를 적용하여 커피의 기능을 확장한다.
public class Main {
    public static void main(String[] args) {
        Coffee simpleCoffee = new SimpleCoffee();
        System.out.println(simpleCoffee.description() + " : $" + simpleCoffee.cost());
        // Simple Coffee : $5.0

        Coffee milkCoffee = new MilkDecorator(simpleCoffee);
        System.out.println(milkCoffee.description() + " : $" + milkCoffee.cost());
        // Simple Coffee, Milk : $6.5

        Coffee milkSugarCoffee = new SugarDecorator(milkCoffee);
        System.out.println(milkSugarCoffee.description() + " : $" + milkSugarCoffee.cost());
        // Simple Coffee, Milk, Sugar : $7.0
    }
}
  • 데코레이터 패턴을 사용하면 자기가 감싸고 있는 구성 요소의 메소드를 호출한 결과에 새로운 기능을 더하면서 행동을 확장할 수 있다.

데코레이터 패턴의 장점

  • 데코레이터(래퍼 클래스)를 이용해 기능을 조합할 수 있다.
    • 상속은 조합이 불가능해서 유연하지 못했지만, 조합 순서에도 상관없이 조합이 가능하다.
  • 또한, 컴파일 타임에 기능 내용이 확정되는 것이 아닌, 런타임에 기능 내용을 변경할 수 있다.
    • 상속의 경우에는 이미 컴파일 타임에 관계가 확정되어 있다.

데코레이터 패턴의 단점

  • 데코레이터를 조합하는 코드가 조금 복잡해질 수 있다.
    • 작은 객체들이 많이 늘어나지만 상속보다는 많이 늘어나지는 않는다.