SW공학/디자인패턴

[SW공학 | 디자인패턴] 5. 데코레이터 패턴(Decorator Pattern)

revolutionarylife 2024. 11. 5. 14:21
반응형

🎨 [SW공학 | 디자인패턴] 5. 데코레이터 패턴(Decorator Pattern)

목차

  • 데코레이터 패턴 개요
  • 데코레이터 패턴의 필요성
  • 데코레이터 패턴의 구조
  • 데코레이터 패턴 예제
  • 데코레이터 패턴의 장점과 단점
  • 마무리

데코레이터 패턴 개요

안녕하세요! 오늘은 객체에 새로운 기능을 추가할 때 유용한 데코레이터 패턴(Decorator Pattern)에 대해 알아보겠습니다. 데코레이터 패턴은 구조 패턴(Structural Pattern) 중 하나로, 객체에 동적으로 새로운 기능을 추가할 수 있도록 해줍니다.

데코레이터 패턴은 마치 크림, 시럽, 토핑을 더해 커피의 맛을 풍부하게 하는 것처럼, 기존 객체에 여러 기능을 추가하거나 확장할 수 있게 해줍니다. 이 패턴을 사용하면 서브클래스를 만들지 않고도 객체의 기능을 확장할 수 있으며, 객체 조합을 통해 다양한 기능을 쉽게 추가할 수 있습니다.


데코레이터 패턴의 필요성

기능을 추가하려고 할 때마다 새로운 클래스를 생성하는 대신, 동적으로 객체에 기능을 추가할 수 있다면 훨씬 더 유연한 설계를 할 수 있습니다. 데코레이터 패턴은 서브클래싱을 통한 확장보다 유연하고, 필요한 기능만 선택적으로 추가할 수 있다는 장점이 있습니다.

예를 들어, 커피를 주문할 때 기본 커피에 크림, 시럽, 초코 토핑 등을 추가하고 싶다면, 커피 클래스에 각각의 옵션을 추가하지 않고 데코레이터 패턴을 사용하여 필요한 옵션만 동적으로 추가할 수 있습니다.


데코레이터 패턴의 구조

데코레이터 패턴은 Component (구성 요소), ConcreteComponent (구체적인 구성 요소), Decorator (데코레이터), ConcreteDecorator (구체적인 데코레이터) 네 가지 요소로 구성됩니다.

데코레이터 패턴 클래스 다이어그램

  • Component (인터페이스)
    • 원본 객체와 장식된 객체가 동일하게 사용할 수 있는 기본 인터페이스를 제공합니다.
    • 데코레이터와 원본 객체 모두 이 인터페이스를 구현하기 때문에, 클라이언트는 원본 객체와 데코레이터를 동일한 방식으로 사용할 수 있습니다.
  • ConcreteComponent (구체적인 구성 요소)
    • 원본 객체로, 데코레이팅할 대상입니다.
    • 이 객체는 기본 기능을 제공하며, 여기에 데코레이터들이 기능을 추가하게 됩니다.
  • Decorator (추상 데코레이터 클래스)
    • 데코레이터의 기본 구조를 정의한 추상 클래스입니다.
    • 이 클래스는 Component 인터페이스를 구현하며, 원본 객체(구성 요소)를 가리키는 wrappee 필드를 가지고 있어 데코레이터가 원본 객체와 동일한 인터페이스를 통해 접근할 수 있도록 합니다.
      • wrapper: 데코레이터 객체 자체를 감싸는 포장지 역할로 볼 수 있어 wrapper라고 합니다. 데코레이터 클래스는 원본 객체를 감싸서 추가 기능을 제공하므로 wrapper라고 함
      • wrappee: 데코레이터가 감싸고 있는 원본 객체를 wrappee라고 부름. 데코레이터가 감싼 대상이라는 의미로, 데코레이터가 원본 객체를 감싸고(wrap) 있는 객체를 나타내기 위해 사용되는 표현
      • 즉, wapper는 감싸는 데코레이터 자체를 의미하며, wrappee는 감싸진 원본 객체를 의미
    • 또한, 이 추상 클래스는 Component의 기능을 그대로 전달할 수 있게 하여, 원본 객체와 동일하게 작동합니다.
  • ConcreteDecorator (구체적인 데코레이터 클래스)
    • 구체적인 데코레이터는 실제로 기능을 추가하는 클래스입니다.
    • Decorator의 기능을 확장하여, 원본 객체의 메서드를 호출하면서 그 전후로 부가적인 로직을 추가할 수 있습니다.
    • 예를 들어, 커피에 우유나 초코를 추가하는 데코레이터들은 각자 고유의 로직을 추가하여 원본 객체의 기능을 확장합니다.

데코레이터 패턴 예제

아래는 데코레이터 패턴을 활용한 커피 주문 시스템의 예제입니다. 기본 커피에 우유, 초코 토핑을 선택적으로 추가할 수 있습니다.

// 커피 객체와 데코레이터가 공통으로 사용할 메서드를 제공(Component)
interface Coffee {
    String getDescription(); // 커피 설명을 반환
    double getCost(); // 커피 가격을 반환
}

// 기본 커피 클래스 (ConcreteComponent)
class SimpleCoffee implements Coffee {
    @Override
    public String getDescription() {
        return "기본 커피"; // 기본 커피 설명
    }

    @Override
    public double getCost() {
        return 2.0; // 기본 커피 가격
    }
}

// 데코레이터 클래스 (Decorator)
abstract class CoffeeDecorator implements Coffee {
    protected Coffee decoratedCoffee; // 데코레이터가 감쌀 커피 객체

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

    @Override
    public String getDescription() {
        return decoratedCoffee.getDescription(); // 기본 커피 설명을 반환
    }

    @Override
    public double getCost() {
        return decoratedCoffee.getCost(); // 기본 커피 가격을 반환
    }
}

// 우유를 추가하는 데코레이터 (ConcreteDecorator)
class MilkDecorator extends CoffeeDecorator {
    public MilkDecorator(Coffee coffee) {
        super(coffee); // 우유를 추가할 커피 객체를 초기화
    }

    @Override
    public String getDescription() {
        return super.getDescription() + ", 우유"; // 기존 설명에 "우유" 추가
    }

    @Override
    public double getCost() {
        return super.getCost() + 0.5; // 기존 가격에 우유 가격 추가
    }
}

// 초코를 추가하는 데코레이터 (ConcreteDecorator)
class ChocoDecorator extends CoffeeDecorator {
    public ChocoDecorator(Coffee coffee) {
        super(coffee); // 초코를 추가할 커피 객체를 초기화
    }

    @Override
    public String getDescription() {
        return super.getDescription() + ", 초코"; // 기존 설명에 "초코" 추가
    }

    @Override
    public double getCost() {
        return super.getCost() + 0.7; // 기존 가격에 초코 가격 추가
    }
}

// 클라이언트 코드
public class Client {
    public static void main(String[] args) {
        Coffee coffee = new SimpleCoffee(); // 기본 커피 생성
        System.out.println("주문: " + coffee.getDescription() + " | 가격: $" + coffee.getCost());

        coffee = new MilkDecorator(coffee); // 커피에 우유 추가
        System.out.println("주문: " + coffee.getDescription() + " | 가격: $" + coffee.getCost());

        coffee = new ChocoDecorator(coffee); // 커피에 초코 추가
        System.out.println("주문: " + coffee.getDescription() + " | 가격: $" + coffee.getCost());
    }
}

위 예제에서, 기본 커피(SimpleCoffee)MilkDecoratorChocoDecorator를 사용하여 우유와 초코 토핑을 동적으로 추가할 수 있습니다.


코드 설명

  1. Coffee 인터페이스(Component)
    • Coffee는 기본적인 getDescription()getCost() 메서드를 정의하는 인터페이스입니다. 이 인터페이스는 커피 객체와 데코레이터가 공통으로 사용할 메서드를 제공합니다.
  2. SimpleCoffee 클래스(ConcreteComponent)
    • SimpleCoffee는 기본 커피 클래스이며, Coffee 인터페이스를 구현합니다.
    • getDescription() 메서드는 "기본 커피"라는 설명을 반환하고, getCost()는 기본 커피의 가격인 $2.0을 반환합니다.
  3. CoffeeDecorator 추상 클래스(Decorator)
    • CoffeeDecoratorCoffee 인터페이스를 구현하고, 데코레이터 클래스의 기본 구조를 제공합니다.
    • CoffeeDecorator는 생성자를 통해 Coffee 객체를 감싸며, getDescription()과 getCost() 메서드를 호출할 때 감싸고 있는 커피 객체의 메서드를 호출합니다.
  4. MilkDecorator 클래스(ConcreteDecorator)
    • MilkDecoratorCoffeeDecorator를 상속받아 기본 커피에 우유를 추가하는 역할을 합니다.
    • getDescription() 메서드는 기본 커피의 설명에 ", 우유"를 추가하고, getCost()는 커피의 가격에 우유 추가 가격 $0.5를 더합니다.
  5. ChocoDecorator 클래스(ConcreteDecorator)
    • ChocoDecoratorCoffeeDecorator를 상속받아 기본 커피에 초코를 추가하는 역할을 합니다.
    • getDescription() 메서드는 기본 커피의 설명에 ", 초코"를 추가하고, getCost()는 커피의 가격에 초코 추가 가격 $0.7을 더합니다.
  6. 클라이언트 코드
    • 클라이언트 코드는 main() 메서드와 같은 프로그램의 진입점에서 실행되는 코드로, 보통 애플리케이션이 시작되거나 특정 작업이 호출될 때 사용자나 시스템이 실제로 사용하는 코드를 말합니다.
    • 디자인 패턴의 관점에서 클라이언트 코드는 패턴을 사용하여 객체를 생성하고 조합하거나 호출하는 부분을 의미합니다.
    • 위 예제에서 클라이언트SimpleCoffee 객체를 생성하고, 데코레이터를 통해 커피에 우유와 초코를 차례로 추가합니다.
    • 결과적으로, 커피의 설명은 "기본 커피, 우유, 초코"가 되고, 총 가격은 $3.2가 됩니다.

출력 결과

주문: 기본 커피 | 가격: $2.0 
주문: 기본 커피, 우유 | 가격: $2.5 
주문: 기본 커피, 우유, 초코 | 가격: $3.2

데코레이터 패턴의 장점과 단점

장점

  • 단일 책임 원칙(SRP) 준수: 각 데코레이터 클래스는 고유의 책임을 가지므로, 특정 기능을 독립적으로 구현할 수 있습니다.
  • 개방-폐쇄 원칙(OCP) 준수: 클라이언트 코드를 수정하지 않고도 새로운 데코레이터 클래스를 추가하여 기능을 확장할 수 있어, 개방-폐쇄 원칙을 따릅니다.
  • 의존 역전 원칙(DIP) 준수: 구현체가 아닌 인터페이스를 사용함으로써 의존 역전 원칙을 준수하고, 유연한 설계를 가능하게 합니다.
  • 유연한 기능 확장: 서브클래스를 만드는 것보다 훨씬 유연하게 기능을 확장할 수 있습니다. 필요한 기능을 동적으로 추가하거나 제거할 수 있습니다.
  • 동적 조합: 객체를 여러 데코레이터로 감싸 다양한 동작을 결합할 수 있습니다. 예를 들어, 커피에 우유와 초코를 함께 추가할 수 있습니다.
  • 런타임에서 기능 변경: 컴파일 타임이 아닌 런타임에 동적으로 기능을 추가할 수 있어, 실행 중에 객체의 행동을 유연하게 조절할 수 있습니다.

단점

  • 순서 의존성: 데코레이터의 순서에 따라 최종 결과가 달라질 수 있습니다. 예를 들어, 커피에 먼저 초코를 추가하고 우유를 추가한 것과, 그 반대 순서로 추가한 경우가 다르게 처리될 수 있습니다.
  • 데코레이터 제거의 어려움: 특정 데코레이터를 제거하려면, 데코레이터 스택에서 해당 데코레이터만 제거하는 것은 쉽지 않습니다. 모든 데코레이터를 다시 설정해야 할 수 있습니다.
  • 복잡한 생성 코드: 데코레이터를 중첩하여 사용하는 경우 new MilkDecorator(new ChocoDecorator(new SimpleCoffee()))와 같은 코드가 생성되어, 코드 가독성이 떨어질 수 있습니다.

마무리

데코레이터 패턴은 서브클래스를 만들지 않고도 객체에 새로운 기능을 동적으로 추가할 수 있는 강력한 디자인 패턴입니다. 이를 통해 객체의 유연성을 높이고, 필요에 따라 기능을 쉽게 확장할 수 있습니다. 데코레이터 패턴을 활용하여 다양한 기능을 조합하고 유연한 설계를 구현해보세요! 🎨

반응형