프로그래밍/알고리즘&자료구조&패턴

디자인패턴 - 데코레이터 패턴

lazy_web_devloper 2025. 8. 18. 10:51
728x90
반응형

JAVA로 구현하는 알고리즘과 디자인 패턴 공부: 데코레이터 패턴 (Decorator)

1. 데코레이터 패턴이란? ☕

개념: 주어진 객체를 수정하지 않고, 동적으로 새로운 기능을 추가할 때 사용하는 패턴입니다. 객체를 새로운 기능을 가진 다른 객체로 감싸서(Wrapping) 기능을 확장합니다.

가장 대표적인 예는 커피 주문입니다.

  1. 기본 아메리카노를 주문합니다. (이것이 원본 객체입니다)
  2. 여기에 우유를 추가합니다. (우유 데코레이터가 아메리카노를 감쌉니다)
  3. 다시 그 위에 휘핑 크림을 올립니다. (휘핑 크림 데코레이터가 우유를 감싼 아메리카노를 감쌉니다)

각각의 '장식(데코레이터)'은 커피라는 본질을 바꾸지 않으면서 가격과 설명 같은 새로운 책임(기능)을 덧붙입니다.

2. 왜 사용할까요?

  • 유연한 기능 확장: 상속을 사용하면 기능 확장이 정적으로 묶이지만, 데코레이터를 사용하면 런타임에 필요한 기능들을 원하는 대로, 원하는 순서로 조합하여 객체에 추가할 수 있습니다.
  • 개방-폐쇄 원칙 (OCP) 충족: 기존 코드를 전혀 수정하지 않고도(Closed for modification) 데코레이터 클래스를 새로 만들어 기능을 확장(Open for extension)할 수 있습니다.
  • 작은 단위의 기능 분리: 각 데코레이터는 단 하나의 기능만 책임지므로 코드가 간결하고 명확해집니다.

3. Java로 구현하는 방법

시나리오: 커피 주문 시스템을 만들어보겠습니다. 기본 커피에 우유, 시럽 등 다양한 토핑을 추가하는 기능입니다.

1단계: 핵심(Component) 인터페이스 정의

장식될 기본 객체와 장식(데코레이터)이 모두 구현해야 할 공통 인터페이스입니다.

Java
 
// 모든 커피 객체가 공유하는 인터페이스
public interface Coffee {
    double getCost();       // 가격을 반환
    String getDescription(); // 설명을 반환
}

2단계: 구체적인 기본 객체(Concrete Component) 구현

장식의 기본이 될 원본 객체입니다.

Java
 
// 기본 커피 (아메리카노)
public class SimpleCoffee implements Coffee {
    @Override
    public double getCost() {
        return 4000; // 기본 가격 4000원
    }

    @Override
    public String getDescription() {
        return "아메리카노";
    }
}

3단계: 추상 데코레이터(Decorator) 클래스 구현

모든 구체적인 데코레이터들이 상속받을 추상 클래스입니다. 내부적으로 자신이 감쌀 Coffee 객체를 멤버 변수로 가집니다.

Java
 
// 모든 데코레이터의 부모 클래스
public abstract class CoffeeDecorator implements Coffee {
    // 자신이 감쌀(Decoration) 커피 객체
    protected final Coffee decoratedCoffee;

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

    // 기본적으로 자신이 감싸고 있는 객체에 요청을 위임한다.
    public double getCost() {
        return decoratedCoffee.getCost();
    }

    public String getDescription() {
        return decoratedCoffee.getDescription();
    }
}

4단계: 구체적인 데코레이터(Concrete Decorator) 구현

실제 '장식' 역할을 하는 클래스들입니다. 부모인 CoffeeDecorator를 상속받아 기능을 확장합니다.

Java
 
// 우유 추가 데코레이터
public class MilkDecorator extends CoffeeDecorator {
    public MilkDecorator(Coffee coffee) {
        super(coffee);
    }

    @Override
    public double getCost() {
        // 원래 가격에 우유 가격(500원)을 추가
        return super.getCost() + 500;
    }

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

// 휘핑 크림 추가 데코레이터
public class WhipDecorator extends CoffeeDecorator {
    public WhipDecorator(Coffee coffee) {
        super(coffee);
    }

    @Override
    public double getCost() {
        // 원래 가격에 휘핑 크림 가격(700원)을 추가
        return super.getCost() + 700;
    }

    @Override
    public String getDescription() {
        // 원래 설명에 '휘핑 크림'을 추가
        return super.getDescription() + ", 휘핑 크림 추가";
    }
}

5단계: 클라이언트 코드에서 사용하기

클라이언트는 이제 원하는 대로 객체들을 조립(장식)하여 사용할 수 있습니다.

Java
 
public class CoffeeShop {
    public static void main(String[] args) {
        // 1. 기본 아메리카노
        Coffee simpleCoffee = new SimpleCoffee();
        System.out.println("주문: " + simpleCoffee.getDescription() + " / 가격: " + simpleCoffee.getCost());

        // 2. 아메리카노에 우유 추가 (라떼)
        Coffee latte = new MilkDecorator(simpleCoffee);
        System.out.println("주문: " + latte.getDescription() + " / 가격: " + latte.getCost());
        
        // 3. 라떼에 휘핑 크림 추가
        Coffee latteWithWhip = new WhipDecorator(latte);
        System.out.println("주문: " + latteWithWhip.getDescription() + " / 가격: " + latteWithWhip.getCost());

        // 4. 아메리카노에 휘핑 크림만 추가한 후 우유 추가 (순서 변경 가능)
        Coffee coffeeWithWhipAndMilk = new MilkDecorator(new WhipDecorator(new SimpleCoffee()));
        System.out.println("주문: " + coffeeWithWhipAndMilk.getDescription() + " / 가격: " + coffeeWithWhipAndMilk.getCost());
    }
}

4. 데코레이터 패턴과 실제 사례

  • Java I/O 클래스: 자바의 입출력 클래스는 데코레이터 패턴의 교과서적인 예입니다.
    • new BufferedReader(new InputStreamReader(new FileInputStream("file.txt")))
    • FileInputStream(기본 객체)을 InputStreamReader(문자 스트림으로 변환하는 데코레이터)로 감싸고, 다시 이것을 BufferedReader(버퍼 기능 추가 데코레이터)로 감싸서 기능을 점층적으로 확장합니다.
  • 스프링 프레임워크: 스프링에서는 AOP(관점 지향 프로그래밍)를 구현하거나 트랜잭션, 보안 등을 적용할 때 내부적으로 프록시(Proxy) 객체를 사용하는데, 이 프록시가 데코레이터 패턴과 매우 유사한 방식으로 동작하여 원래의 빈(Bean) 객체에 부가 기능을 추가합니다.

5. 문제 제시 및 답변 💡

문제: 간단한 텍스트를 표시하는 TextView 컴포넌트가 있습니다. 이 TextView에 스크롤바(Scrollbar)를 추가하거나, 테두리(Border)를 추가하는 기능을 넣고 싶습니다. 상속을 사용하지 않고 데코레이터 패턴을 사용하여 이 기능을 구현하려면 어떻게 설계해야 할까요?

답변:

커피 예제와 동일한 구조로 설계할 수 있습니다.

  1. Component 인터페이스: draw() 메서드를 가진 공통 인터페이스를 만듭니다.
  2. TextView 클래스: Component 인터페이스를 구현하는 기본 텍스트 컴포넌트를 만듭니다. draw() 메서드는 단순히 텍스트를 출력합니다.
  3. Decorator 추상 클래스: Component를 구현하고, 내부에 Component 객체를 멤버로 가집니다.
  4. ScrollbarDecorator 클래스: Decorator를 상속받습니다. draw() 메서드에서 먼저 스크롤바를 그린 후, 감싸고 있는 Component 객체의 draw()를 호출합니다.
  5. BorderDecorator 클래스: Decorator를 상속받습니다. draw() 메서드에서 먼저 테두리를 그린 후, 마찬가지로 내부 Component의 draw()를 호출합니다.

사용 예시: new BorderDecorator(new ScrollbarDecorator(new TextView())) 와 같이 호출하면, 텍스트 뷰에 스크롤바가 추가되고 그 전체에 다시 테두리가 그려지는 객체를 동적으로 생성할 수 있습니다.

728x90
반응형