Mobile & App Stack/Java Software Architecture & Patterns

데코레이터 패턴 완벽 정리: 상속 없이 객체 기능 확장하기

임베디드 친구 2024. 12. 25. 10:08
728x90
반응형

새로운 기능을 추가할 때마다 매번 새로운 자식 클래스를 만들고 계신가요? 그러다 보면 클래스 개수가 걷잡을 수 없이 늘어나는 '클래스 폭발' 현상을 겪게 됩니다. 오늘은 상속을 사용하지 않고도 객체에 옷을 입히듯 동적으로 기능을 추가하는 데코레이터 패턴(Decorator Pattern)에 대해 알아보겠습니다.

Generated by Gemini AI.


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

데코레이터 패턴은 기존 객체의 코드를 수정하지 않고도, 런타임에 부가적인 기능을 '장식(Decorate)' 하듯이 추가할 수 있게 해주는 구조 패턴입니다.

핵심 아이디어: 상속보다는 조합(Composition)

  • 상속: 컴파일 타임에 기능이 결정되며, 정적이고 유연하지 못합니다.
  • 데코레이터: 객체들을 마치 선물 상자를 포장하듯 겹겹이 감싸서 기능을 확장하므로 매우 유연합니다.

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

패턴의 구조는 크게 4가지 역할로 나뉩니다.

  1. Component (인터페이스): 기본 기능을 정의하며, 원본 객체와 장식자 객체가 모두 이를 구현해야 합니다.
  2. ConcreteComponent: 장식될 실제 '기본 객체'입니다.
  3. Decorator (추상 클래스): Component를 필드로 가지며, 이를 감싸서 다시 Component 인터페이스를 제공합니다.
  4. ConcreteDecorator: 실제로 기능을 추가하는 클래스(우유 추가, 샷 추가 등)입니다.

3. Java 구현 예제: 커피 주문 시스템

커피에 다양한 토핑을 추가하는 과정을 코드로 살펴보겠습니다.

Step 1. Component와 ConcreteComponent

Java
 
// 모든 커피의 기본 인터페이스
interface Coffee {
    String getDescription();
    double getCost();
}

// 기본 커피 클래스
class SimpleCoffee implements Coffee {
    public String getDescription() { return "에스프레소"; }
    public double getCost() { return 3.0; }
}

Step 2. Decorator 구현 (기능 추가의 핵심)

Java
 
// 장식자들의 공통 추상 클래스
abstract class CoffeeDecorator implements Coffee {
    protected Coffee decoratedCoffee; // 감싸질 객체

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

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

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

    public String getDescription() {
        return decoratedCoffee.getDescription() + ", 우유 추가";
    }

    public double getCost() {
        return decoratedCoffee.getCost() + 0.5;
    }
}

Step 3. 클라이언트 코드 (런타임에 기능 조합)

Java
 
public class CoffeeShop {
    public static void main(String[] args) {
        // 1. 에스프레소 준비
        Coffee coffee = new SimpleCoffee();
        
        // 2. 우유 추가 (에스프레소를 우유로 감싸기)
        coffee = new MilkDecorator(coffee);
        
        // 3. 샷 추가 (우유 커피를 다시 샷으로 감싸기)
        // coffee = new ShotDecorator(coffee); // 다른 데코레이터도 동일한 방식으로 적용
        
        System.out.println("주문 메뉴: " + coffee.getDescription());
        System.out.println("최종 가격: $" + coffee.getCost());
    }
}

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

👍 장점

  • 유연한 확장: 상속을 사용하지 않으므로 런타임에 기능을 자유롭게 섞어서 추가할 수 있습니다.
  • SRP(단일 책임 원칙) 준수: 각 데코레이터는 자신이 맡은 특정 기능(우유, 설탕 등)에만 집중합니다.
  • 코드 재사용성: 기존 코드를 건드리지 않고 새로운 데코레이터를 만드는 것만으로 기능 확장이 가능합니다.

👎 단점

  • 객체 수 증가: 자잘한 데코레이터 객체들이 많이 생성되어 메모리 구조가 복잡해 보일 수 있습니다.
  • 초기 설정 복잡성: new Milk(new Sugar(new SimpleCoffee())) 처럼 객체를 생성하는 코드가 다소 길어질 수 있습니다.

5. 실무에서의 활용: Java I/O 스트림

자바를 공부하면서 아래와 같은 코드를 보신 적이 있을 겁니다.

Java
 
DataInputStream dis = new DataInputStream(new BufferedInputStream(new FileInputStream("data.txt")));

이것이 바로 데코레이터 패턴의 정석입니다!

  • FileInputStream: 기본 파일 읽기
  • BufferedInputStream: 버퍼링 기능 추가
  • DataInputStream: 기본 데이터 타입 읽기 기능 추가 이처럼 자바는 I/O 라이브러리 설계에 데코레이터 패턴을 적극적으로 사용하여 개발자가 필요한 기능을 선택적으로 조립하도록 만들었습니다.

결론

데코레이터 패턴은 "상속은 강력하지만 무겁고, 조합은 유연하고 가볍다"는 사실을 잘 보여줍니다. 객체에 기능을 추가해야 하지만 상속 구조가 복잡해지는 것이 걱정된다면 데코레이터 패턴 도입을 추천합니다.


포스팅이 도움이 되셨다면 공감과 구독 부탁드립니다! 디자인 패턴에 대해 더 궁금한 점이 있다면 댓글로 남겨주세요.

반응형