ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [디자인 패턴] 데코레이터 패턴 (Decorator Pattern)
    Design Patterns 2025. 1. 31. 16:37

    데코레이터 패턴이란?

    • 객체의 기능을 동적으로 확장할 수 있는 구조적 디자인 패턴이다.
    • 기존 코드 수정 없이 객체의 행동을 변경하거나 새로운 기능을 추가할 때 유용하다.
    • 상속 대신 구성(Composition)과 위임(Delegation)을 활용하여 기능을 확장하는 것이 핵심이다.

    핵심 개념

    1️⃣ Component

    • Component: 핵심 인터페이스로서, 기능의 기본 구조를 정의한다.
    • Concrete Component: Component 인터페이스를 구현하는 구체 클래스이다.

    2️⃣ Decorator

    • Decorator: Compoent 인터페이스를 구현하는 추상 클래스이다. 실제 기능을 수행하지 않고 기존 Component를 감싼다.
    • Concrete Decorator: Decorator 클래스를 상속하여 특정 기능을 추가하는 역할을 한다. 로깅, 성능 측정, 보안, 데이터 변환 등의 기능을 구현할 수 있다.

    3️⃣ 전체 구조

    프록시 패턴과의 차이

    • 데코레이터 패턴은 프록시 패턴과 코드 구조가 매우 비슷하지만, 의도(Intent)가 다르기 때문에 별개의 패턴으로 구분된다.

    장단점

    장점

    • 상속보다 더 유연하게 기능을 추가할 수 있다.
    • OCP(Open-Closed Principle) 준수: 기존 코드 변경 없이 새로운 기능 추가할 수 있음
    • SRP(Single Responsibility Principle) 준수: 기능별로 클래스를 분리하여 유지보수성을 높일 수 있음

    단점

    • 데코레이터 객체가 늘어날수록 복잡성이 증가하여 디버깅이 어려울 수 있다.

    예제 코드: 햄버거 주문 🍔

    햄버거 주문 시스템에 데코레이터 패턴을 적용하여 추가 옵션(사이드 메뉴, 토핑 등)을 동적으로 추가하는 방식을 만들어보자.

     

    👨🏻‍🍳 요구사항

    • Food 인터페이스를 기반으로 주문 가능한 음식(BasicFood)을 정의
    • 특정 음식에 추가 옵션(치즈 추가, 감자튀김 추가, 음료 추가 등)을 동적으로 적용할 수 있어야 함
    • 기존 BasicFood 클래스를 수정하지 않고 기능을 확장해야 함
    • 클라이언트인 Customer 클래스의 음식을 소비하는 로직이 수정되지 않아야 함

    1️⃣ Component

    // 음식 (Component)
    public interface Food {
        String getDescription(); // 음식의 설명을 반환
        double getCost(); // 음식의 가격을 반환
    }

     

     

    2️⃣ Concrete Component

    // 기본 음식(Concrete Component)
    public class BasicFood implements Food {
    
        @Override
        public String getDescription() {
            return "🍔 기본 버거";
        }
    
        @Override
        public double getCost() {
            return 5000;
        }
    }
    

     

     

    3️⃣ Decorator

    // 데코레이터 부모 클래스 (Decorator)
    public abstract class FoodDecorator implements Food {
    
        protected Food decoratedFood;
    
        public FoodDecorator(Food food) {
            this.decoratedFood = food;
        }
    
        @Override
        public String getDescription() {
            return decoratedFood.getDescription();
        }
    
        @Override
        public double getCost() {
            return decoratedFood.getCost();
        }
    }
    

     

     

    4️⃣ Concrete Decorators

    • 각각 치즈, 감자튀김, 음료를 추가하는 기능을 가진 Decorator들을 만들었다.
    // 치즈 추가 (Concrete Decorator)
    public class CheeseDecorator extends FoodDecorator {
    
        public CheeseDecorator(Food food) {
            super(food);
        }
    
        @Override
        public String getDescription() {
            return super.getDescription() + " 🧀 + 치즈 추가";
        }
    
        @Override
        public double getCost() {
            return super.getCost() + 1000;
        }
    }
    // 감자튀김 추가 (Concrete Decorator)
    public class FriesDecorator extends FoodDecorator {
    
        public FriesDecorator(Food food) {
            super(food);
        }
    
        @Override
        public String getDescription() {
            return super.getDescription() + " 🍟 + 감자튀김 추가";
        }
    
        @Override
        public double getCost() {
            return super.getCost() + 2000;
        }
    }
    // 음료 추가 (Concrete Decorator)
    public class DrinkDecorator extends FoodDecorator {
    
        public DrinkDecorator(Food food) {
            super(food);
        }
    
        @Override
        public String getDescription() {
            return super.getDescription() + " 🥤 + 음료 추가";
        }
    
        @Override
        public double getCost() {
            return super.getCost() + 1500;
        }
    }

     

     

    5️⃣ Client

    // 음식을 소비하는 client
    public class Customer {
    
        private final Food food;
    
        public Customer(Food food) {
            this.food = food;
        }
    
        public void eat() {
            String result = food.getDescription();
            double cost = food.getCost();
            System.out.println("주문한 음식: " + result);
            System.out.println("총 가격: " + cost);
        }
    }

     

     

    6️⃣ 실행 예제 및 결과

    public class AppMain {
        public static void main(String[] args) {
            System.out.println("🙋🏻‍♂️ [1] 기본 버거 주문");
            basicBurgerCustomer().eat();
    
            System.out.println("\\n🙋🏻‍♂️ [2] 치즈 버거 주문");
            cheeseBurgerCustomer().eat();
    
            System.out.println("\\n🙋🏻‍♂️ [3] 풀세트 주문");
            fullSetCustomer().eat();
        }
    
        public static Customer basicBurgerCustomer() {
            Food basicBurger = new BasicFood();
            return new Customer(basicBurger);
        }
    
        public static Customer cheeseBurgerCustomer() {
            Food cheeseBurger = new CheeseDecorator(new BasicFood());
            return new Customer(cheeseBurger);
        }
    
        public static Customer fullSetCustomer() {
            Food fullSet = new DrinkDecorator(new FriesDecorator(new CheeseDecorator(new BasicFood())));
            return new Customer(fullSet);
        }
    }
    🙋🏻‍♂️ [1] 기본 버거 주문
    주문한 음식: 🍔 기본 버거
    총 가격: 5000.0
    
    🙋🏻‍♂️ [2] 치즈 버거 주문
    주문한 음식: 🍔 기본 버거 🧀 + 치즈 추가
    총 가격: 6000.0
    
    🙋🏻‍♂️ [3] 풀세트 주문
    주문한 음식: 🍔 기본 버거 🧀 + 치즈 추가 🍟 + 감자튀김 추가 🥤 + 음료 추가
    총 가격: 9500.0
    

    ✅ 정리

    다음 코드의 실행 흐름을 호출 스택으로 나타내보고 마무리하자.

    public class AppMain {
        public static void main(String[] args) {
            System.out.println("\\n🙋🏻‍♂️ [3] 풀세트 주문");
            fullSetCustomer().eat();
        }
    
        public static Customer fullSetCustomer() {
            Food fullSet = new DrinkDecorator(new FriesDecorator(new CheeseDecorator(new BasicFood())));
            return new Customer(fullSet);
        }
    }
    🙋🏻‍♂️ [3] 풀세트 주문
    주문한 음식: 🍔 기본 버거 🧀 + 치즈 추가 🍟 + 감자튀김 추가 🥤 + 음료 추가
    총 가격: 9500.0
    

     

     

    1️⃣ 호출 스택 생성

    • Customer가 가지고 있는 Food 객체는 DrinkDecorator이다.
    • Customer::eat 메서드 내부에서 DrinkDecorator::getDescription을 호출한다.
    • 이어서 Decorator들은 자신의 상위 클래스의 getDescription()을 먼저 호출하기 때문에 위 그림처럼 BasicFood까지 올라가게 된다.

     

    2️⃣ 호출 스택 반환

    • BasicFood에서 반환한 값은 자신을 호출했던 곳으로 반환되게 되어, 결국 위 그림처럼 최종적인 결과를 Customer가 받게 된다.
    • getCost()의 경우도 위 구조와 동일하다.