관리 메뉴

SW

[개발서적] 헤드퍼스트 디자인 패턴 Ch3. 데코레이터(Decorator) 패턴 본문

개발서적/헤드퍼스트 디자인패턴

[개발서적] 헤드퍼스트 디자인 패턴 Ch3. 데코레이터(Decorator) 패턴

SWKo 2024. 7. 28. 14:47

1. 초대형 커피 전문점, 스타버즈

현재는 매우 많은 스타버즈 매장이 존재합니다.

하지만 워낙 빠르게 성장하다보니 다양한 음료를 모두 포괄하는 주문 시스템을 갖추지 못했습니다.

초기 주문 시스템 클래스는 아래와 같았습니다.

 

 

Beverage는 음료를 나타내는 추상 클래스이며, 모든 음료는 Beverage 클래스의 서브클래스가 됩니다.

모든 서브클래스는 가격을 리턴하는 cost() 메소드를 구현해야 합니다.

 

// Beverage.java
public abstract class Beverage {
    String description = "Unknown Beverage";

    public String getDescription() {
        return description;
    }

    public abstract double cost();
}

// HouseBlend.java
public class HouseBlend extends Beverage {
    public HouseBlend() {
        description = "House Blend Coffee";
    }

    public double cost() {
        return .89;
    }
}

// DarkRoast.java
public class DarkRoast extends Beverage {
    public DarkRoast() {
        description = "Dark Roast Coffee";
    }

    public double cost() {
        return .99;
    }
}

 

이렇게 되면, 커피에 첨가물이 추가될 때마다 새로운 Class가 필요합니다.

 

 

만약 우유 가격이 인상되면 어떨까요? 클래스를 모두 수정해줘야 합니다.

그러면, Beverage 클래스에 각 첨가물 첨가 여부를 보여주는 인스턴스 변수를 추가해보겠습니다.

 

 

// 기본 Beverage 클래스
class Beverage {
    private String description = "Unknown Beverage";
    private double baseCost = 0.0;
    private boolean hasMilk = false;
    private boolean hasSoy = false;

    // 기본 생성자
    public Beverage(String description, double baseCost) {
        this.description = description;
        this.baseCost = baseCost;
    }

    // 설명 반환
    public String getDescription() {
        return description;
    }

    // Milk 추가 여부 설정
    public void setMilk(boolean hasMilk) {
        this.hasMilk = hasMilk;
    }

    // Soy 추가 여부 설정
    public void setSoy(boolean hasSoy) {
        this.hasSoy = hasSoy;
    }

    // 총 비용 계산
    public double cost() {
        double totalCost = baseCost;
        if (hasMilk) {
            totalCost += 0.50; // Milk 추가 비용
        }
        if (hasSoy) {
            totalCost += 0.30; // Soy 추가 비용
        }
        return totalCost;
    }
}

// Espresso 클래스
class Espresso extends Beverage {
    public Espresso() {
        super("Espresso", 1.99);
    }
}

// HouseBlend 클래스
class HouseBlend extends Beverage {
    public HouseBlend() {
        super("House Blend", 0.89);
    }
}

// 메인 클래스
public class CoffeeShop {
    public static void main(String[] args) {
        // 에스프레소 주문
        Beverage beverage1 = new Espresso();
        beverage1.setMilk(true); // 밀크 추가
        System.out.println(beverage1.getDescription() + " $" + beverage1.cost());

        // 하우스 블렌드 주문
        Beverage beverage2 = new HouseBlend();
        beverage2.setSoy(true); // 소이 추가
        beverage2.setMilk(true); // 밀크 추가
        System.out.println(beverage2.getDescription() + " $" + beverage2.cost());
    }
}

 

클래스 수는 줄었지만 문제점이 몇가지 있습니다.

  • 첨가물 가격이 바뀔 때마다 기존 코드를 수정해야 합니다.
  • 첨가물의 종류가 많아지면 새로운 메소드를 추가해야 하고, 슈퍼클래스의 cost() 메소드도 수정해야 합니다.
  • 특정 첨가물이 들어가면 안되는 음료가 출시될 수도 있습니다. 새로 출시될 아이스티 같은 경우 hasWhip() 메소드가 필요없지만 여전히 상속되는 문제가 있습니다.

 

2. OCP (Open-Closed Principle)

디자인 원칙
클래스는 확장에 열려 있어야 하지만, 변경에는 닫혀 있어야 한다.

 

https://inpa.tistory.com/entry/OOP-%F0%9F%92%A0-%EC%95%84%EC%A3%BC-%EC%89%BD%EA%B2%8C-%EC%9D%B4%ED%95%B4%ED%95%98%EB%8A%94-OCP-%EA%B0%9C%EB%B0%A9-%ED%8F%90%EC%87%84-%EC%9B%90%EC%B9%99

 

💠 완벽하게 이해하는 OCP (개방 폐쇄 원칙)

개방 폐쇄 원칙 - OCP (Open Closed Principle) 개방 폐쇄의 원칙(OCP)이란 기존의 코드를 변경하지 않으면서, 기능을 추가할 수 있도록 설계가 되어야 한다는 원칙을 말한다. 보통 OCP를 확장에 대해서는

inpa.tistory.com

 

 

3. 데코레이터 패턴

상속을 사용해서 총 가격을 산출하는 방법은 좋은 방법은 아니었습니다.

특정 음료에서 시작해서 첨가물로 그 음료를 장식(decorate) 해보는 건 어떨까요?

  1. DarkRoast 객체를 가져온다.
  2. Mocha 객체로 장식한다.
  3. Whip 객체로 장식한다.
  4. cost() 메소드를 호출한다. 
    이때 첨가물의 가격을 계산하는 일은 해당 객체에게 위임한다.

자세히 살펴봅시다.

 

1. DarkRoast 객체에서 시작합니다.

 

2. 고객이 모카를 주문했으니 Mocha 객체를 만들고 그 객체로 DarkRoast를 감쌉니다.(장식합니다.)

  • Mocha 객체는 Decorator 입니다. 
  • Mocha 에도 cost() 메소드가 있고, Mocha가 감싸고 있는 것도 Beverage 객체로 간주할 수 있습니다.(Mocha도 Beverage의 서브클래스)

 

3. 휘핑크림도 추가했으니 Whip 데코레이터를 만들어 Mocha를 감쌉니다.

 

4. 이제 가격 계산을 할 때는 Whip의 cost()를 호출하면 됩니다. 그럼 Whip은 그 객체가 장식하고 있는 객체에게 가격 계산을 위임합니다.

 

 

정리

  • 데코레이터(Whip, Mocha)의 슈퍼클래스(Beverage)는 자신이 장식하고 있는 객체(DarkRoast)의 슈퍼클래스(Beverage)와 같습니다.
  • 한 객체(DarkRoast)를 여러 개의 데코레이터(Whip, Mocha)로 감쌀 수 있습니다.
  • 데코레이터는 자신이 감싸고 있는 객체와  같은 슈퍼클래스(Beverage)를 가지고 있기에 원래 객체(싸여 있는 객체)가 들어갈 자리에 데코레이터 객체를 넣어도 상관 없습니다.
  • 데코레이터는 자신이 장식하고 있는 객체에게 어떤 행동을 위임하는 일 말고도 추가 작업을 수행할 수 있습니다. (cost() 메소드에서 로깅 등 다른 액션 가능)
  • 객체는 언제든지 감쌀 수 있으므로 실행 중에 필요한 데코레이터를 마음대로 적용할 수 있습니다.

 

4. 데코레이터 패턴의 정의

데코레이터 패턴(Decorator Pattern)으로 객체에 추가 요소를 동적으로 더할 수 있습니다.

데코레이터를 사용하면 서브클래스를 만들 때보다 훨씬 유연하게 기능을 확장할 수 있습니다.

 

 

각 데코레이터 안에는 Component 객체가 들어있습니다. (데코레이터에는 구성 요소의 Reference를 포함한 인스턴스 변수가 있습니다.)

 

5. 데코레이터 패턴

Beverage 클래스

public abstract class Beverage {
    String description = "제목 없음";

    // 이미 구현된 메서드
    public String getDescription() {
        return description;
    }

    // 서브클래스에서 구현해야 하는 추상 메서드
    public abstract double cost();
}

 

첨가물(Condiment) 클래스 = 데코레이터 클래스

public abstract class CondimentDecorator extends Beverage {
    Beverage beverage;

    // 모든 첨가물 데코레이터에 getDescription() 메서드를 새로 구현하도록 하기 위해 추상 메서드로 선언
    public abstract String getDescription();
}

 

음료 클래스

public class Espresso extends Beverage {
    public Espresso() {
        description = "에스프레소";
    }

    public double cost() {
        return 1.99;
    }
}

 

첨가물 코드 구현

public class Mocha extends CondimentDecorator {
    Beverage beverage;

    // 생성자에서 감싸고자 하는 음료 객체를 전달받아 저장
    public Mocha(Beverage beverage) {
        this.beverage = beverage;
    }

    // 음료 설명에 '모카' 추가
    public String getDescription() {
        return beverage.getDescription() + ", 모카";
    }

    // 음료 가격에 모카 가격 추가
    public double cost() {
        return beverage.cost() + 0.20;
    }
}

 

주문용 코드

public class StarbuzzCoffee {
    public static void main(String[] args) {
        // 아무것도 넣지 않은 에스프레소를 주문하고 그 음료 설명과 가격을 출력합니다
        Beverage beverage1 = new Espresso();
        System.out.println(beverage1.getDescription() + " $" + beverage1.cost());

        // 다크 로스트 커피에 모카 두 번과 휘핑크림을 추가하여 주문하고, 설명과 가격을 출력합니다
        Beverage beverage2 = new DarkRoast();
        beverage2 = new Mocha(beverage2); // 모카샷 하나 추가
        beverage2 = new Mocha(beverage2); // 모카샷 하나 더 추가
        beverage2 = new Whip(beverage2);  // 휘핑크림 추가
        System.out.println(beverage2.getDescription() + " $" + beverage2.cost());

        // 하우스 블렌드 커피에 두유, 모카, 휘핑크림을 추가하여 주문하고, 설명과 가격을 출력합니다
        Beverage beverage3 = new HouseBlend();
        beverage3 = new Soy(beverage3);   // 두유 추가
        beverage3 = new Mocha(beverage3); // 모카 추가
        beverage3 = new Whip(beverage3);  // 휘핑크림 추가
        System.out.println(beverage3.getDescription() + " $" + beverage3.cost());
    }
}

 

전체 코드

// Beverage 클래스
public abstract class Beverage {
    String description = "제목 없음";

    // 이미 구현된 메서드
    public String getDescription() {
        return description;
    }

    // 서브클래스에서 구현해야 하는 추상 메서드
    public abstract double cost();
}

// CondimentDecorator 클래스
public abstract class CondimentDecorator extends Beverage {
    Beverage beverage;

    // 모든 첨가물 데코레이터에 getDescription() 메서드를 새로 구현하도록 하기 위해 추상 메서드로 선언
    public abstract String getDescription();
}

// Mocha 클래스
public class Mocha extends CondimentDecorator {
    Beverage beverage;

    public Mocha(Beverage beverage) {
        this.beverage = beverage;
    }

    public String getDescription() {
        return beverage.getDescription() + ", 모카";
    }

    public double cost() {
        return beverage.cost() + 0.20;
    }
}

// Whip 클래스
public class Whip extends CondimentDecorator {
    Beverage beverage;

    public Whip(Beverage beverage) {
        this.beverage = beverage;
    }

    public String getDescription() {
        return beverage.getDescription() + ", 휘핑크림";
    }

    public double cost() {
        return beverage.cost() + 0.30;
    }
}

// Soy 클래스
public class Soy extends CondimentDecorator {
    Beverage beverage;

    public Soy(Beverage beverage) {
        this.beverage = beverage;
    }

    public String getDescription() {
        return beverage.getDescription() + ", 두유";
    }

    public double cost() {
        return beverage.cost() + 0.15;
    }
}

// DarkRoast 클래스
public class DarkRoast extends Beverage {
    public DarkRoast() {
        description = "최고의 다크 로스트 커피";
    }

    public double cost() {
        return 1.99;
    }
}

// Espresso 클래스
public class Espresso extends Beverage {
    public Espresso() {
        description = "에스프레소";
    }

    public double cost() {
        return 1.99;
    }
}

// HouseBlend 클래스
public class HouseBlend extends Beverage {
    public HouseBlend() {
        description = "하우스 블렌드 커피";
    }

    public double cost() {
        return 0.89;
    }
}

// StarbuzzCoffee 클래스
public class StarbuzzCoffee {
    public static void main(String[] args) {
        // 아무것도 넣지 않은 에스프레소를 주문하고 그 음료 설명과 가격을 출력합니다
        Beverage beverage1 = new Espresso();
        System.out.println(beverage1.getDescription() + " $" + beverage1.cost());

        // 다크 로스트 커피에 모카 두 번과 휘핑크림을 추가하여 주문하고, 설명과 가격을 출력합니다
        Beverage beverage2 = new DarkRoast();
        beverage2 = new Mocha(beverage2); // 모카샷 하나 추가
        beverage2 = new Mocha(beverage2); // 모카샷 하나 더 추가
        beverage2 = new Whip(beverage2);  // 휘핑크림 추가
        System.out.println(beverage2.getDescription() + " $" + beverage2.cost());

        // 하우스 블렌드 커피에 두유, 모카, 휘핑크림을 추가하여 주문하고, 설명과 가격을 출력합니다
        Beverage beverage3 = new HouseBlend();
        beverage3 = new Soy(beverage3);   // 두유 추가
        beverage3 = new Mocha(beverage3); // 모카 추가
        beverage3 = new Whip(beverage3);  // 휘핑크림 추가
        System.out.println(beverage3.getDescription() + " $" + beverage3.cost());
    }
}

 

데코레이터 패턴은 객체의 기능을 쉽게 추가하고 변경할 수 있는 방법을 제공합니다.

이를 통해 객체 간 결합도를 낮추고 코드의 재사용성과 유지보수성을 높일 수 있는 강력한 디자인 패턴입니다.

 


https://medium.com/@mr.kashif.samman/flexible-and-maintainable-react-native-applications-with-the-decorator-pattern-32d84de576f6

 

Flexible and Maintainable React / React Native Applications with the Decorator Pattern

In the world of software development, there are many design patterns that developers can use to build better and more maintainable…

medium.com

 

 

https://www.youtube.com/watch?v=PU9sr-q8bys

 

  • 고차 함수
    • 예시 : debounce
  • 고차 컴포넌트(HOC)
    • 예시 : 인증 여부에 따라 조건부 렌더링을 하는 컴포넌트
Comments