[Design Pattern] Decorator Pattern이란?

Junseong·2021년 5월 4일
0
post-thumbnail

목차

  1. 등장 배경
  2. 패턴 설명
  3. 패턴 정의

본 시리즈는 'Head First Design Patterns' 책을 통해 공부한 내용을 참고 및 각색하여 작성되어졌습니다. 전체 코드는 Github 에서 다운 받을 수 있습니다.


등장 배경

데코레이터 패턴은 클래스의 코드를 전혀 바꾸지 않고도 객체에 새로운 임무를 부여하기 위해 등장하였습니다.

클래스의 코드를 바꾸지 않고 객체에 새로운 임무를 부여할 수 있게 설계한다면 새로운 기능을 추가하는데 있어서 매우 유연해지기 때문에 급변하는 주변 환경에 잘 적응할 수 있게 됩니다.

여기 새로 창업한 카페에서 개발중인 음료 주문 시스템을 통해 자세히 알아보도록 하겠습니다.

카페의 모든 음료들은 Beverage 추상 클래스를 상속받습니다.

카페 음료에는 샷을 추가한다거나 휘핑 크림을 올리는 것과 같이 여러 첨가물들이 있습니다.

Beverage 추상 클래스의 desciption 변수는 음료 이름과 추가된 첨가물을 나타내는 String 변수입니다.

Beverage 추상 클래스에는 각 첨가물에 대한 bool 변수와 has, set 메서드가 있습니다.

set 메서드로 음료에 첨가물을 추가/제거 하고, has 메서드로 해당 첨가물이 음료에 추가되어있는지 확인합니다.

Beverage 추상 클래스의 cost 메서드는 음료의 총 가격을 계산하는 추상 메서드이며 서브 클래스에서 로직을 구현합니다.

이를 구현한 코드는 다음과 같습니다. (상속을 더 활용할 수 있지만 이해를 돕기 위해 그러지 않았습니다.)

public abstract class Beverage {
  private boolean milk;
  private boolean ice;
  private boolean mocha;
  private boolean whip;
	public String description;
	
	public String getDescription() {
    return description;
  }

  public void setMilk(boolean milk) {
      this.milk = milk;
  }

  public void setIce(boolean ice) {
      this.ice = ice;
  }

  public void setMocha(boolean mocha) {
      this.mocha = mocha;
  }

  public void setWhip(boolean whip) {
      this.whip = whip;
  }

  public boolean hasMilk() {
      return milk;
  }

  public boolean hasIce() {
      return ice;
  }

  public boolean hasMocha() {
      return mocha;
  }

  public boolean hasWhip() {
      return whip;
  }

  public abstract int cost();
}
public class Americano extends Beverage {

        @Override
        public int cost() {
            int cost = 2000;
            if (hasIce()) {
                description += ", 얼음";
            }
            if (hasMocha()) {
                cost += 500;
                description += ", 모카";
            }
            if (hasWhip()) {
                cost += 500;
                description += ", 휘핑";
            }
            if (hasMilk()) {
                cost += 500;
                description += ", 우유";
            }
            return cost;
        }
    }

    public class Decaf extends Beverage {

        public Decaf() {
            description = "디카페인";
        }
        
        @Override
        public int cost() {
            int cost = 3000;
            if (hasIce()) {
                description += ", 얼음";
            }
            if (hasMocha()) {
                cost += 500;
                description += ", 모카";
            }
            if (hasWhip()) {
                cost += 500;
                description += ", 휘핑";
            }
            if (hasMilk()) {
                cost += 500;
                description += ", 우유";
            }
            return cost;
        }
    }

    public class Espresso extends Beverage {

        @Override
        public int cost() {
            int cost = 4000;
            if (hasIce()) {
                description += ", 얼음";
            }
            if (hasMocha()) {
                cost += 500;
                description += ", 모카";
            }
            if (hasWhip()) {
                cost += 500;
                description += ", 휘핑";
            }
            if (hasMilk()) {
                cost += 500;
                description += ", 우유";
            }
            return cost;
        }
    }

이렇게 상속을 이용해서 구현하면 만약 새로운 음료가 추가되더라도 Beverage 클래스의 코드를 수정할 필요 없이 이를 확장한 서브클래스만 만들어 주면 됩니다. (Ice Tea 음료 추가)

public class IceTea extends Beverage {

        @Override
        public int cost() {
            int cost = 2000;
            if (hasIce()) {
                description += ", 얼음";
            }
            if (hasMocha()) {
                cost += 500;
                description += ", 모카";
            }
            if (hasWhip()) {
                cost += 500;
                description += ", 휘핑";
            }
            if (hasMilk()) {
                cost += 500;
                description += ", 우유";
            }
            return cost;
        }

그러나 만약에 새로운 첨가물을 추가해야한다면 어떨까요? 어쩔 수 없이 Beverage 클래스의 코드를 수정해야 할 것입니다.

또한 아이스티에는 휘핑 크림 첨가물이 필요가 없음에도 관련 변수와 메서드들을 상속받게 될 것입니다. 이러면 손님이 휘핑 크림이 추가된 아이스티를 받는 불상사가 일어날 수도...


패턴 설명

상속을 이용해서 만든 음료 주문 시스템은 기존 코드의 수정이 불가피하였으며 문제도 있었습니다.

그렇다면 본격적으로 데코레이터 패턴을 적용해서 앞의 문제를 해결해 보도록 하겠습니다.

데코레이터 패턴을 적용한 음료 주문 시스템은 다음과 같이 동작합니다. (어떤 손님이 모카하고 얼음을 추가한 아메리카노를 주문한 예제입니다)

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

이를 그림으로 표현하면 다음과 같습니다.

음료를 감싼 객체중 가장 바깥쪽에 있는 객체(Mocha)의 cost()가 호출되면 그 다음 안쪽의 cost()를 호출합니다.

가장 안쪽 객체(Americano)의 cost()에 도달하면 자신을 감싼 객체(Ice)에게 자신의 가격(2000원)을 리턴합니다.

가격을 리턴받은 객체(Ice)는 리턴받은 가격과 자신의 가격(0원)을 합쳐 또 자신을 감싼 객체(Mocha)에게 리턴합니다.

이 과정을 가장 바깥쪽의 있는 객체까지 반복 실행시키면 주문받은 객체의 최종 가격을 알 수 있게 됩니다.

데코레이터 패턴은 이처럼 어떤 객체에 새로운 요소가 생길 때마다 그 요소로 덮어씌우는 모습이 마치 가만히 있는 객체를 각종 요소들로 꾸미는 행위처럼 보이기 때문에 데코레이터 패턴이라는 이름이 붙게 되었습니다.

데코레이터 패턴에서는 객체를 꾸미는 요소들을 데코레이터라고 부르며 꾸밈을 받는 객체를 컴포넌트라고 합니다. 음료 주문 시스템에서는 Milk, Mocha, Ice, Whip 이 데코레이터에 해당되며 Americano, Decaf, Espresso, IceTea 음료들이 컴포넌트에 해당됩니다.

데코레이터 패턴을 사용하면 카페에 새로운 첨가물이 생기더라도 기존 객체의 코드 수정 없이 데코레이터 객체만 만들어 주면 되기 때문에 유연한 설계방식이라고 할 수 있겠습니다.

이제 패턴이 어떻게 구현되는지 알았으니 직접 음료 주문 시스템을 데코레이터 패턴으로 구현해 보도록 하겠습니다.

모든 음료들은 Beverage 추상 객체(혹은 인터페이스)를 구현하고 있습니다.

모든 첨가물들은 CondimentDecorator 추상 객체(혹은 인터페이스)를 구현하고 있으며 이 추상 객체 또한 Beverage 추상 객체를 구현하고 있습니다. 이 덕분에 데코레이터 객체가 다른 객체를 감싸는 행동을 할 수 있습니다.

모든 첨가물 객체는 생성될 때 자신이 꾸밀 객체를 파라미터로 입력받습니다. 이것이 데코레이터 객체가 다른 객체를 감싸는 행동입니다.

마지막으로 구현 코드와 테스트를 보고 최종적으로 정리해 보도록 하겠습니다.

public abstract class Beverage {
    public String description;

    public String getDescription() {
        return description;
    }

    public abstract int cost();
}

public class Americano extends Beverage {

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

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

public class Espresso extends Beverage {

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

    @Override
    public int cost() {
        return 4000;
    }
}
public abstract class CondimentDecorator extends Beverage {
    public abstract String getDescription();
}

public class Ice extends CondimentDecorator {
    Beverage beverage;

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

    @Override
    public int cost() {
        return beverage.cost() + 0;
    }

    @Override
    public String getDescription() {
        return beverage.getDescription() + ", 아이스";
    }
}

public class Mocha extends CondimentDecorator {
    Beverage beverage;

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

    @Override
    public int cost() {
        return beverage.cost() + 500;
    }

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

public class Whip extends CondimentDecorator {
    Beverage beverage;

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

    @Override
    public int cost() {
        return beverage.cost() + 500;
    }

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

테스트

Beverage beverage = new Espresso();

// "에스프레소 $ 4000"
System.out.println(beverage.getDescription() + " $ " + beverage.cost());

Beverage beverage2 = new Americano();
beverage2 = new Mocha(beverage2);
beverage2 = new Ice(beverage2);
beverage2 = new Whip(beverage2);

// "아메리카노, 모카, 얼음, 휘핑 $ 3000" 
System.out.println(beverage2.getDescription() + " $ " + beverage2.cost());

Beverage beverage3 = new Whip(new Ice(new Mocha(new Americano())));

// "아메리카노, 모카, 얼음, 휘핑 $ 3000"
System.out.println(beverage3.getDescription() + " $ " + beverage3.cost());

만약 이 음료 주문 시스템에 새로운 첨가물을 추가해야 한다면 어떻게 해야할까요?

그냥 CondimentDecorator 추상 객체를 구현하는 데코레이터 객체만 만들어 주면 될 것입니다.

새로운 음료가 추가되어도 Beverage 추상 객체를 구현하는 객체만 만들어 주면 됩니다.

이제는 데코레이터 패턴이 어떻게 기존 코드 수정없이 새로운 기능을 추가할 수가 있으며 왜 급변하는 주변 환경에 잘 적응을 할 수 있는 것인지 알 수 있을것입니다.


패턴 정의

데코레이터 패턴에서는 객체에 추가적인 요건을 동적으로 첨가한다. 데코레이터는 서브클래스를 만드는 것을 통해서 기능을 유연하게 확장할 수 있는 방법을 제공한다.

profile
#취준생 #Back-end

0개의 댓글