오늘은 Decorator Pattern에 대해 알아보는 시간을 가지겠다. Decorator Pattern은 Head First Design Pattern 책의 117p ~ 145p에 설명되어 있으며 책의 내용과 구글링 정보를 종합해서 정리할 예정이다.
데코레이터 패턴을 설명하기 전에, OCP에 대해 먼저 알아보자.
디자인 원칙 중 하나인 OCP는 클래스는 확장에 대해서는 열려 있어야 하지만 코드 변경에 있어서는 닫혀 있어야 한다.
Ex) 기본 도로 표시 기능에 차선 표시, 교통량 표시, 교차로 표시, 단속 카메라 표시의 4가지 추가 기능이 있을 때 추가 기능의 모든 조합은 15가지가 된다.
-> 데코레이터 패턴을 이용하여 필요 추가 기능의 조합을 동적으로 생성할 수 있다.
데코레이터 패턴은 OCP에 충실한 디자인 패턴이다. 해당 패턴을 이용할 경우 다음과 같은 장단점을 얻을 수 있다.
장점
단점
데코레이터 패턴이 적용된 대표적인 사례로는 자바 I/O가 있다. Java I/O의 구조를 볼 경우 다음과 같이 구성되고 있다.
InputStream
OutputStream
Java I/O는 다음과 같이 객체 속에 객체를 결합하여 InputStream을 구현하였다.
카페를 연 개발자 박씨는 카페 시스템을 개발해 운영하려 한다. 원만하게 운영하던 그 때 어떤 손님이 찾아와 다음과 같이 말했다.
여기 민트초코에 휘핑크림, 샷, 초콜릿 추가해주시고 텀블러에 담아주세요~
저는 아이스 아메리카노 에스프레소 샷 추가인데 로스팅 된 원두로 부탁드려요~
뭔 민트 초코를 이렇게 먹나..? 이 손님은 커잘알인가... 주문의 디테일이 엄청나다. 막상 주문을 접수하려 하니 현재 시스템으로는 주문이 불가능한 요구사항이었다.
개발자 박씨는 어떻게 구현해야만 해당 주문을 성공적으로 시스템에 등록할 수 있을까?
현재 시스템 구조는 다음과 같다.
음.. 해당 구조에서 어떻게 해야 될까?
만약.. 그냥 단순히 모든 토핑을 추가한다고 하면 다음과 같은 클래스 다이어그램이 구성될 것 같다.
오우 쉣... 딱 봐도 너무나도 더럽기 때문에 단순한 방법으로는 어려울 것 같다.
가장 먼저 생각해야 될 부분은 새로운 메뉴를 주문하는 것이 아닌 기존 메뉴에 구성 요소를 추가한다는 것이다. 현재 음료인 Beverage 클래스에 토핑들을 추가하는 방법이니 단순히 객체를 만들어서 구현하기에는 다소 어려움이 존재한다.
또한, 앞으로 신 메뉴가 나오거나 새로운 토핑이 생길 경우 기존 시스템에 영향을 주지 않고 구현이 가능해야 된다.
필수 고려사항
앞서 배웠던 데코레이터 패턴은 어느 상황에서 사용하는지 상기해보자.
객체의 결합 을 통해 기능을 동적으로 유연하게 확장 할 수 있게 해주는 패턴
따라서 객체의 결합에 대해 생각하며 진행해보자. 데코레이터 패턴을 적용할 경우 기존 클래스는 음료일 것이고 데코레이터 패턴에 들어갈 내용은 토핑일 것이다.
객체의 결합을 중점적으로 볼 때 유동적으로 추가되는 토핑은 객체의 결합의 주체일 것이기에 토핑이 데코레이터 클래스에 추가될 것이다.
다음을 그림으로 표현해보자.
데코레이터 클래스 내부에는 다음과 같이 토핑이 들어갈 예정이며, 추가될 경우에는 계속해서 원 내부로 새로운 토핑 클래스가 등록될 것이다.
그럼 박씨의 개인카페의 시스템 클래스 다이어그램을 다시 한 번 생각해보자.
Decorator 클래스를 하나 생성한 이후 각 토핑들이 해당 데코레이터 클래스를 상속받도록 설계하였다.
메인 클래스인 음료
같은 경우 Beverage 클래스를 상속받으며, Decorator 클래스를 추가로 포함시켜 토핑을 추가할 수 있게 되었다.
구조만 봐서는 이해가 안될수도 있으니 코드를 작성해보자.
public abstract class Beverage {
private String description;
public String getDescription() {
description = "Beverage : ";
return description;
}
public Integer cost() {
return 0;
}
}
public class Decorator extends Beverage{
@Override
public String getDescription(){
return "Add Topping: ";
}
}
public class Americano extends Beverage {
@Override
public Integer cost() {
return super.cost() + 2500;
}
@Override
public String getDescription(){
return super.getDescription() + "Americano ";
}
}
public class Latte extends Beverage {
@Override
public Integer cost() {
return super.cost() + 3000;
}
@Override
public String getDescription(){
return super.getDescription() + "Latte ";
}
}
public class MintChoco extends Beverage {
@Override
public Integer cost() {
return super.cost() + 3500;
}
@Override
public String getDescription(){
return super.getDescription() + "Mint Choco ";
}
}
public class Smoothie extends Beverage {
@Override
public Integer cost() {
return super.cost() + 4000;
}
@Override
public String getDescription(){
return super.getDescription() + "Smoothie ";
}
}
public class Choco extends Decorator {
private Beverage beverage;
public Choco(Beverage beverage) {
this.beverage = beverage;
}
@Override
public Integer cost() {
return beverage.cost() + 500;
}
@Override
public String getDescription(){
return beverage.getDescription() + super.getDescription() + "Choco ";
}
}
public class Milk extends Decorator {
private Beverage beverage;
public Milk(Beverage beverage) {
this.beverage = beverage;
}
@Override
public Integer cost() {
return beverage.cost() + 500;
}
@Override
public String getDescription(){
return beverage.getDescription() + super.getDescription() + "Milk ";
}
}
public class Shot extends Decorator {
private Beverage beverage;
public Shot(Beverage beverage) {
this.beverage = beverage;
}
@Override
public Integer cost() {
return beverage.cost() + 500;
}
@Override
public String getDescription(){
return beverage.getDescription() + super.getDescription() + "Shot ";
}
}
public class main {
public static void main(String[] args){
Beverage americano = new Americano();
System.out.println(americano .getDescription() + "cost is : " + americano.cost());
americano = new Shot(americano);
System.out.println(americano .getDescription() + "cost is : " + americano.cost() + "\n");
Beverage mintChoco = new MintChoco();
System.out.println(mintChoco .getDescription() + "cost is : " + mintChoco.cost());
mintChoco = new Choco(mintChoco);
System.out.println(mintChoco .getDescription() + "cost is : " + mintChoco.cost());
mintChoco = new Milk(mintChoco);
System.out.println(mintChoco .getDescription() + "cost is : " + mintChoco.cost()+ "\n");
Beverage latte = new Latte();
System.out.println(latte.getDescription() + "cost is : " + latte.cost());
latte = new Milk(latte);
System.out.println(latte.getDescription() + "cost is : " + latte.cost());
}
}
getDescription()
메소드를 한 번 자세히 들여다 보길 바란다. 앞서 클래스 다이어그램을 다시 봐보자. 만약 Decorator Class에서 getDescription();
메소드에 super.getDescription()을 추가했다면 어떤 답이 나올까?
public class Decorator extends Beverage{
@Override
public String getDescription(){
return super.getDescription() + "Add Topping: ";
}
}
한 번 고민해보기를 바란다. 힌트는 다음과 같다.
모든 놈들이 결국 Beverage를 상속받고 있으니 흠... 한 번 머릿 속에서 출력을 그려보는 것도 좋을 것 같다.
오늘은 데코레이터 패턴을 배워보았다. 막상 main 코드를 구현하려고 하니 무엇을 호출해야 되는지 잠시 애좀 먹었고, 출력이 생각대로 되지 않아서 잠깐 헤메었지만 확실히 어떤 기능을 추가할 경우에 기존 코드를 수정하지 않아도 될 것 같다는 생각이 드는 구조였다.
단점은 너무 많은 클래스를 생성해야 되다 보니 관리 차원에서 생각보다 까다로울수도 있을 것 같다는 생각이 들었고, 한층 더 복잡해지면 어떤 구조로 되어있는지 이해하기가 굉장히 어려울 것 같다.
하지만 확장성을 고려해야되는 상황이면 데코레이터 패턴은 항상 고려해볼만한 패턴이라 생각한다.