3일차, 데코레이터 패턴을 공부했다!
데코레이터 패턴은 OCP를 보여주기 가장 좋은 디자인패턴 같다. 추가나 변경사항이 있을때, 코드를 고치지 않고 추가하는 것만으로 쉽게 구현할 수 있도록 해주는 패턴이다.
어떤 Beverage라는 슈퍼클래스가 있다. 이 클래스는 설명을 출력하는 getDescription() 메서드와 가격을 계산해주는 cost() 메서드가 있다. 음료 서브클래스의 종류는 두개, 그 위에 얹을 수 있는 추가옵션이 두개 있다. 여러 조합의 음료가 있을 것인데, 가격과 설명을 출력하도록 어떻게 구현하는 것이 좋을까?
가장 쉽게 떠오르는 방법은 상속을 이용하는 것이다. 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 3000;
}
}
문제는 Beverage를 상속받는 각 음료 서브클래스들은 언제든지 추가될 수 있다는 것이다. 여름에 탕후루슬러시가 나오면 우리는 새로운 클래스를 추가해야 하고 cost()와 getDescription() 메서드를 구현해주어야 할 것이다. 근데 여기서 탕후루슬러시에 휘핑크림을 얹고 싶은 사람이 나오면 어떻게 해야할까? 탕후루슬러시+휘핑크림 클래스를 또 만들어주어야 하는걸까? 이렇게 단순 서브클래스들 만으로는 추가가 용이하지 않다. 더욱 더 나아가서, 만약에 그렇게 수많은 클래스를 만들었는데, 휘핑크림의 가격이 바뀌면 어떻게 해야할까? 맞다. 수많은 클래스의 cost() 메서드를 수정해주어야 한다. 이것은 전혀 좋지 않은 방법이다. 다른 방법을 몰색해보자.
public class 탕후루슬러시에휘핑크림 extends Beverage {
public 탕후루슬러시에휘핑크림() {
description = "탕후루슬러시에휘핑크림추가";
}
@Override
public int cost() {
return 8000;
}
}
여기서 데코레이터 패턴이 등장한다. 데코레이터 패턴은 꾸밀 객체들의 슈퍼클래스(우리는 Beverage 슈퍼클래스)를 상속받으며 구성하는 추상클래스 Decorator 슈퍼클래스로부터 시작한다.
public abstract class BeverageDecorator extends Beverage {
Beverage beverage;
public abstract String getDescription();
}
그 후에 이 슈퍼클래스를 상속받는 데코레이터를 만들어보자.
public class Whip extends BeverageDecorator {
public Whip(Beverage beverage) {
this.beverage = beverage;
}
@Override
public String getDescription() {
return beverage.getDescription() + ", 휘핑";
}
@Override
public int cost() {
return beverage.cost() + 300;
}
}
이런식으로 데코레이터 패턴을 사용해볼 수 있었다. 이렇게 만들면 데코레이터 구상클래스들은 Beverage 구상클래스들을 감쌀 수 있고, 또한 다른 데코레이터가 감쌀 수도 있게 된다.
가장 밖의 Whip 클래스가 Whip 클래스를 감싸고, 그 Whip클래스가 Americano 클래스를 감싸는 방식이다. 이렇게 하면 가장 밖의 Whip 클래스의 메서드를 호출하면 가장 안쪽의 메서드까지 호출이 이어지고, 다시 타고 올라오면서 가격을 계산하거나 설명을 덧붙일 수 있다.
@Slf4j
public class Main {
public static void main(String[] args) {
Beverage beverage = new Espresso();
log.info("에스프레소 주문이요! (현재 상태 : {})", beverage.getDescription());
beverage = new Mocha(beverage);
log.info("모카 한바퀴랑요. (현재 상태 : {})", beverage.getDescription());
beverage = new Mocha(beverage);
log.info("아, 모카 한바퀴 더요. (현재 상태 : {})", beverage.getDescription());
beverage = new Whip(beverage);
log.info("휘핑크림 올려주세요. (현재 상태 : {})", beverage.getDescription());
log.info("네, 가격은 {}원 입니다.", beverage.cost());
}
}
어떤가. 동적으로 객체의 수정이 가능하며, 추가에 용이한 것이 눈에 확 들어오지 않는가?
에스프레소 주문이요! (현재 상태 : 에스프레소)
모카 한바퀴랑요. (현재 상태 : 에스프레소, 모카)
아, 모카 한바퀴 더요. (현재 상태 : 에스프레소, 모카, 모카)
휘핑크림 올려주세요. (현재 상태 : 에스프레소, 모카, 모카, 휘핑)
네, 가격은 3800원 입니다.
기존 단순 상속 방식의 문제를 요약하면 이렇다.
이 문제점들이 데코레이터 패턴을 통해서 어떻게 해결되는지 볼 수 있었다. 하지만 책에는 이런 말도 있었다. '모든 부분에 OCP를 적용할 필요는 없다.' 즉, 모든 부분을 확장에는 열려있고 변경에는 닫혀있도록 설계할 필요가 없다는 것이다. 오히려 이렇게 설계하면 불필요한 클래스가 과도하게 많아져, 전체적인 흐름을 잡기가 어려울 수도 있다는 이야기이다. 디자인패턴들은 내 전체 비즈니스 코드에서 중요한 부분에 알맞게 사용하는 것이 좋을 것 같다.
위 글은 다음 책을 바탕으로 작성하였습니다.
헤드퍼스트 디자인패턴 : https://www.yes24.com/Product/Goods/108192370
소스 코드 : https://github.com/Dompoo/DesignPatternStudy/tree/master/src/main/java/dompoo/study/decoratorPattern