컴퓨터과학/0 + 소프트웨어 아키텍처(디자인 패턴)

[소프트웨어 아키텍처] 14. 데코레이터 패턴(Decorator Pattern -java)

힘들면힘을내는쿼카 2022. 12. 7. 22:44
728x90
반응형

데코레이터 패턴(Decorator Pattern)

 

기존 클래스 코드를 변경하지 않고 객체에 새로운 작업을 추가할 수 있는 패턴
데코레이터 패턴을 사용하면 객체에 추가 요소를 동적으로 더할 수 있습니다.
또한, 서브클래스를 만들 때보다 훨씬 유연하게 기능을 확장할 수 있습니다.

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

상속이 좋은 방법이긴 하지만, 서브클래스를 계속 만들게 되면, 모든 서브클래스에서 똑같은 행동을 상속받아야 되고, 컴파일시에 결정되기 때문에 실행시에 동적으로 이를 정의할 수가 없습니다.
🔑키 포인트는, 기존 코드를 건드리지 않고 기능을 새로 추가할 수 있다는 점입니다.

데코레이터 패턴 구조

  • Component
    • 구성요소(장식의 대상)
  • ConcreteComponent
    • 새로운 행동을 동적으로 추가
  • Decorator
    • 자신이 장식할 구성 요소와 같은 인터페이스 또는 추상 클래스를 구현
  • ConcreteDecoratorA, ConcreteDecoratorB
    • 데코레이터가 새로운 메소드를 추가할 수도 있습니다.
    • 하지만 일반적으로 새로운 메소드를 추가하는 대신 Component에 원래 있던 메소드를 별도의 작업으로 처리해서 새로운 기능을 추가합니다

OCP(Open Closed Principle)

클래스의 확장에는 열려 있어야 하지만 변경에는 닫혀 있어야 한다.!
모순처럼 보이지만, 직접 코드를 수정하지 않고도 코드를 확장할 수 있게 해주는 기법입니다.!

기존에 별다방 음료 애플리케이션은 다음과 같았습니다.
우와! 클래스가 폭발 했습니다.!! 🌋

이러한 상황에서 우유 가격이 상승한다면? 새로운 모카가 추가된가면??!

반응형

왜 클래스가 많이 필요하나요?
인스턴스 변수와 슈퍼클래스 상속을 이용해서 첨가물을 관리해볼까요?

첨가물에 대해서는 불리언 변수로 만들고, cost()는 추상메소드로 정의하지 않고 구현했습니다.
하지만, 이런식으로 설계한다면 다음과 같은 문제가 발생할 수 있습니다.

  1. 첨가물 가격이 바뀔때 마다 기존 코드를 수정해야합니다.
  2. 첨가물의 종류가 많아지면 새로운 메소드를 추가해야합니다.
  3. 새로운 음료가 출시 되었습니다.! 그런데 그 음료는 첨가물이 없는 음료라면…? 필요없는 메소드를 상속 받게 됩니다…!
  4. 만약 동일한 첨가물을 계속 넣는다면?

데코레이터 패턴으로 ⭐️ 별다방 음료 메뉴 리팩토링

특정 음료에서 시작해서 첨가물로 그 음료를 장식하는 방식으로 구현해 봅시다.!
키포인트는 상속을 이용해서 클래스를 확장하고, 구성을 이용해 새로운 행동을 유연하게 추가합니다.

StarBeverage

public abstract class StarBeverage {
    protected String description = "설명 없음";

    public String getDescription() {
        return description;
    }

    public abstract double cost();
}

Americano

public class Americano extends StarBeverage {

    public Americano() {
        description = "아메리카노";
    }

    @Override
    public double cost() {
        return 2500;
    }
}

Espresso

public class Espresso extends StarBeverage {

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

    @Override
    public double cost() {
        return 2000;
    }
}

StarCondimentDecorator

/**
 * extends StarBeverage 클래스를 확장 합니다.
 */
public abstract class StarCondimentDecorator extends StarBeverage {
    /**
     * StarBeverage 클래스 구성을 이용합니다.
     */
    protected StarBeverage startBeverage;
    public abstract String getDescription();
}

Ice

public class Ice extends StarCondimentDecorator {

    public Ice(StarBeverage startBeverage) {
        this.startBeverage = startBeverage;
    }

    @Override
    public double cost() {
        return this.startBeverage.cost() + 500;
    }

    @Override
    public String getDescription() {
        return this.startBeverage.getDescription() + ", 얼음";
    }
}

Whip

public class Whip extends StarCondimentDecorator {

    public Whip(StarBeverage starBeverage) {
        this.startBeverage = starBeverage;
    }

    @Override
    public double cost() {
        return this.startBeverage.cost() + 300;
    }

    @Override
    public String getDescription() {
        return this.startBeverage.getDescription() + ", 휘핑 크림";
    }
}

Client

public class Client {
    public static void main(String[] args) {
        StarBeverage beverage1 = new Espresso();
        System.out.println("첫번째 주문 = "+beverage1.getDescription()+" :: 가격 = "+beverage1.cost());

        StarBeverage beverage2 = new Americano();
        beverage2 = new Ice(beverage2);
        System.out.println("두번째 주문 = " + beverage2.getDescription()+" :: 가격 = "+beverage2.cost());

        beverage2 = new Whip(beverage2);
        System.out.println("세번째 주문 = " + beverage2.getDescription()+" :: 가격 = "+beverage2.cost());
    }
}

결과

첫번째 주문 = 에스프레소 :: 가격 = 2000.0
두번째 주문 = 아메리카노, 얼음 :: 가격 = 3000.0
세번째 주문 = 아메리카노, 얼음, 휘핑 크림 :: 가격 = 3300.0

정리

  • 디자인의 유연성 면에서 바라보면 상속으로 확장하는 일은 좋은 선택은 아님
  • 구성과 위임으로 실행 중에 새로운 행동을 추가할 수 있음
  • 상속 대신 데코레이터 패턴으로 행동을 확장할 수 있음
  • 데코레이터 패턴은 구상 구성 요소를 감싸 주는 데코레이터를 사용
  • 데코레이터 클래스의 형식은 그 클래스가 감싸는 클래스 형식을 반영
    • 상속이나 인터페이스 구현으로 자신이 감쌀 클래스와 같은 타입
  • 데코레이터는 자신이 감싸고 있는 구성 요소의 메소드를 호출한 결과에 새로운 기능을 더함으로써 행동을 확장
  • 구성 요소를 감싸는 데코레이터의 갯수에는 제한이 없음
  • 구성 요소의 클라이언트는 데코레이터의 존재를 알 수 없다.
    • 다만, 클라이언트가 구성 요소의 구체적인 형식에 의존하는 경우는 예외
  • 데코레이터 패턴을 사용하면 서브 클래스가 매우 많이 추가될 수 있음
  • 데코레이터를 너무 많이 사용하면 코드가 필요 이상으로 복잡해짐

 

728x90
반응형