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

하리보·2025년 3월 20일
post-thumbnail

데코레이터 패턴(Decorator Pattern)

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

데코레이터 패턴(Decorator Pattern)은 객체에 동적으로 새로운 기능을 추가하는 디자인 패턴입니다. 상속을 사용하지 않고도 객체의 기능을 확장할 수 있으며, OCP(Open-Closed Principle, 개방-폐쇄 원칙) 을 준수하는 유용한 패턴입니다.

2. 왜 데코레이터 패턴을 사용할까요?

문제 상황

기능을 추가해야 하는 객체가 여러 종류일 때, 이를 해결하는 일반적인 방법은 상속(Inheritance) 입니다. 하지만 상속을 사용하면 클래스가 많아지고, 변경이 어려워지는 유연성 부족 문제가 발생할 수 있습니다.

해결 방법

데코레이터 패턴을 사용하면 런타임에 객체에 새로운 기능을 추가할 수 있으며, 기존 코드를 수정하지 않고도 확장이 가능합니다.


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

주요 구성 요소

  1. Component (컴포넌트, 기본 인터페이스)
    • 기본적인 동작을 정의하는 인터페이스 또는 추상 클래스입니다.
  2. ConcreteComponent (구체적인 컴포넌트)
    • Component를 구현하는 실제 클래스입니다.
  3. Decorator (데코레이터, 추상 클래스)
    • Component를 감싸는(wrapper) 역할을 하며, 동적으로 기능을 추가하는 역할을 합니다.
  4. ConcreteDecorator (구체적인 데코레이터)
    • Decorator의 구체적인 구현체로, 새로운 기능을 추가하는 역할을 합니다.

UML 다이어그램


4. Java로 구현하기

4.1 인터페이스 정의

// Component 인터페이스 (기본 객체의 동작 정의)
public interface Beverage {
    String getDescription();
    double cost();
}

4.2 ConcreteComponent 구현

// 구체적인 Component (기본 음료)
public class Espresso implements Beverage {
    @Override
    public String getDescription() {
        return "에스프레소";
    }
    
    @Override
    public double cost() {
        return 1.99;
    }
}

4.3 Decorator 추상 클래스 구현

// 데코레이터 추상 클래스
public abstract class CondimentDecorator implements Beverage {
    protected Beverage beverage;
    
    public CondimentDecorator(Beverage beverage) {
        this.beverage = beverage;
    }
    
    @Override
    public abstract String getDescription();
}

4.4 ConcreteDecorator 구현

// 구체적인 데코레이터 (우유 추가)
public class Milk extends CondimentDecorator {
    public Milk(Beverage beverage) {
        super(beverage);
    }
    
    @Override
    public String getDescription() {
        return beverage.getDescription() + ", 우유";
    }
    
    @Override
    public double cost() {
        return beverage.cost() + 0.50;
    }
}

// 구체적인 데코레이터 (모카 추가)
public class Mocha extends CondimentDecorator {
    public Mocha(Beverage beverage) {
        super(beverage);
    }
    
    @Override
    public String getDescription() {
        return beverage.getDescription() + ", 모카";
    }
    
    @Override
    public double cost() {
        return beverage.cost() + 0.70;
    }
}

4.5 실행 코드

public class CoffeeShop {
    public static void main(String[] args) {
        Beverage espresso = new Espresso();
        System.out.println(espresso.getDescription() + " $" + espresso.cost());
        
        Beverage milkEspresso = new Milk(espresso);
        System.out.println(milkEspresso.getDescription() + " $" + milkEspresso.cost());
        
        Beverage mochaMilkEspresso = new Mocha(milkEspresso);
        System.out.println(mochaMilkEspresso.getDescription() + " $" + mochaMilkEspresso.cost());
    }
}

실행 결과

에스프레소 $1.99
에스프레소, 우유 $2.49
에스프레소, 우유, 모카 $3.19

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

✅ 장점

  • OCP(개방-폐쇄 원칙) 준수: 기존 클래스를 수정하지 않고 기능을 추가할 수 있습니다.
  • 유연성 증가: 필요한 기능을 조합하여 다양한 객체를 만들 수 있습니다.
  • 클래스 수 감소: 상속을 사용하여 기능을 추가하는 방식보다 클래스 수가 줄어듭니다.

❌ 단점

  • 객체 수 증가: 데코레이터를 중첩하여 사용할 경우 객체 수가 많아질 수 있습니다.
  • 디버깅 어려움: 여러 개의 데코레이터가 감싸져 있는 경우, 특정 동작이 어디서 변경되었는지 추적하기 어렵습니다.
  • 복잡성 증가: 단순한 기능 추가에도 여러 클래스를 생성해야 하므로 코드가 복잡해질 수 있습니다.

6. 실제 활용 예시: Java I/O 스트림

Java의 java.io 패키지는 데코레이터 패턴을 활용한 대표적인 예입니다. InputStream을 기본 컴포넌트로 사용하고, BufferedInputStream, LineNumberInputStream 같은 데코레이터를 이용하여 기능을 동적으로 추가할 수 있습니다.

import java.io.*;

public class DecoratorPatternIOExample {
    public static void main(String[] args) {
        try {
            InputStream inputStream = new FileInputStream("test.txt");
            InputStream bufferedStream = new BufferedInputStream(inputStream);
            InputStream lineNumberStream = new LineNumberInputStream(bufferedStream);
            
            int data = lineNumberStream.read();
            while (data != -1) {
                System.out.print((char) data);
                data = lineNumberStream.read();
            }
            lineNumberStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

위 코드에서 FileInputStream은 기본적인 파일 입력 기능을 제공하고, BufferedInputStream은 버퍼링 기능을 추가하며, LineNumberInputStream은 줄 번호를 추적할 수 있도록 기능을 확장하는 역할을 합니다. 이렇게 데코레이터 패턴을 활용하여 기존 클래스의 기능을 변경하지 않고 동적으로 확장할 수 있습니다.

7. 정리

데코레이터 패턴은 상속을 대체하여 동적으로 객체의 기능을 확장하는 강력한 방법입니다. 특히 GUI 컴포넌트, 스트림 처리(java.io 패키지), 텍스트 편집기 등 다양한 분야에서 널리 사용됩니다.

profile
하리보의 개발 블로그

0개의 댓글