본문 바로가기
👩🏻‍💻Technical things/Design pattern

[Design pattern] Decorator pattern

by Bㅐ추 2020. 6. 23.
728x90
반응형

본 게시글은 Head First Design Pattern을 읽고 정리한 내용 입니다.

해당 소스코드는, https://github.com/so1gging/Design-Pattern를 참고하세요:)


0. 정의

객체에 추가적인 요건을 동적으로 첨가한다. 데코레이터는 서브클래스를 만드는 것을 통해서 기능을 유연하게 확장할 수 있는 방법을 제공한다. 즉, 어떤 기능에 추가적으로 기능을 덧붙이고 싶은 경우, 그 기능들을 Decorator로 만들어서 덧붙이는 방식이다.

 

1. 문제상황

이 글을 보고 계시는 대부분의 분들이 카페에 한 번쯤 가보셨을 것이다.

카페에 가서 주문을 하는 상황을 한 번 상상해보자.

 

"따뜻한 카페라떼 한 잔 주세요!",

옆에서 커피가 나오는 동안 기다리는데, 다른 손님이 들어와서 주문을 한다.

"카라멜 프라푸치노에, 헤이즐럽 시럽 한 번 넣고 자바칩 넣어주세요! 아 그리고 카라멜 드리즐이랑 초코 드리즐 깔아주시구요.

 

첫 번째 주문만 있을 경우에, 사실 데코레이터 패턴은 굳이 필요하지 않을 수도 있다. 하지만 현실은 그렇지가 않죠. 많은 고객들은 자신의 취향에 맞게 기존의 레시피에 추가를 하거나 빼서 주문을 시킨다.

 

두 번째 주문의 경우 카라멜 프라푸치노 + 헤이즐럿 시럽 + 자바칩 + 카라멜 드리즐 + 초코 드리즐 으로 커피 메뉴에 많은 데코레이션 재료가 추가 되었다.

 

이것을 소프트웨어 적으로 바라본다면, 카라멜 프라푸치노 라는 객체에 헤이즐럿 시럽 객체, 자바칩 객체 등이 추가 되었다고 볼 수 있을 것.

 

 

자 이제, 커피 전문점인 스타버즈 커피사에서 좀 더 효율적인 운영을 위해 음료 주문을 받는 프로그램을 만들었다고 가정 해보자.

기본적으로 이 프로그램을 만들때 공통적으로 가지고 있는 성질을 따로 빼서 '음료(Beverage)' 라는 클래스로 만들고, 이것을 상속 받아 사용하게 하였다.

Beverage는 음료를 나타내는 추상클래스로, 커피샵에서 판매되는 모든 음료는 이 클래스의 서브클래스가 된다.

description이라는 인스턴스 변수는 각 서브클래스에서 설정된다.

cost()메소드는 추상메소드로 서브클래스에서 그 메소드를 구현해서 새로 정의해야 한다.

 

단, 두번째 주문처럼 커피에는 두유,우유,모카 등을 추가하거나 휘핑크림도 추가가 가능하게 해야 한다. 

 

그러다보니..

클래스의 개수가 말 그대로 "폭발적으로" 늘어난다. 

 

상속을 통해 관리하면 안될까?

그냥 인스턴스 변수하고 수퍼클래스 상속을 써서 추가 사항을 관리하면 안되나?

 

boolean타입의 변수를 이용해 해당 재료를 첨가하면 super클래스의 cost메소드에서 들어있는 재료들의 가격을 더해준다. 

 

서브클래스들에서 각 가격을 계산하고 슈퍼클래스에서 구현한 cost()를 호출하여 추가가격을 구한다면

아마 이런 식이 될 것이다.

public int cost() {
        
        int total = 0;
    
        if(hasMilk()) total+=500;
        if(hasShot()) total+=400;
        if(hasCream()) total+=300;
        if(hasJavachip()) total += 700;
        
        return total;
        
        
    }
public class CaffeLatte extends Beverage {
 
    @Override
    public int cost() {
        // TODO Auto-generated method stub
        return 5000+super.cost();
    }
    
}

문제점이 존재한다.

 

1. 첨가물의 가격이 바뀌면 기존코드를 수정해야하고

2. 종류가 추가되거나 삭제되면 수정해야하고

3. 음료가 추가되면 휘핑크림을 안올리는 음료에도 hasWhip을 추가하게 될것이고

4. 더블모카를 주문하게되면?

 

=> 데코레이터 패턴을 통해 해결이 가능하다.

OCP (Open-Closed Principle) : 클래스는 확장에 대해서는 열려 있어여 하지만 코드 변경에 대해서는 닫혀 있어야 한다.
 
즉, 기존 코드는 건드리지 말고 확장을 통해서 새로운 행동을 간단하게 추가 할 수 있도록 한다!

2. 소스코드

데코레이터 패턴의 기본 UML 다이어그램
데코레이터 패턴으로 설계한 커피숍 UML 다이어그램

 

첨가물들은 CordimentDecorator 라는 클래스를 상속 받는다.  이 클래스는 Beverage 클래스를 확장한 것으로, 첨가물들에게 getDescription 메소드를 새롭게 정의하도록 만든다. 왜 이렇게 하냐면, Beverage를 상속받는 커피의 이름에 첨가물의 이름을 더 하기 위해 이렇게 한다.

 

// Beverage 는 추상클래스이며 getDescription()과 cost()라는 두 개의 메소드가 있다.
// 음료의 공통적인 성질만 따로 뺀 것으로 카페에서 판매하는 모든 음료는 이 클래스를 상속받아야 한다.
public abstract class Beverage {
	String description = "Unknown Beverage"; // 음료 이름
  
	public String getDescription() {
		return description;
	}
 
	public abstract double cost();
	// cost()는 서브클래스에서 구현해야 한다.
}

 

public class Espresso extends Beverage {
  
	public Espresso() {
		description = "Espresso"; // 클래스 생성자 부분에서 descrition변수의 값을 설정한다.
		// description 변수는 Beverage로부터 상속받은 것이다.
	}
  
	public double cost() {
		return 1.99; 
	}
}

// 모든 첨가물이 상속받아야 하는 클래스
// Berverage 객체가 들어갈 자리에 들어갈 수 있어야 하므로 Beverage 클래스를 확장해야 한다.
public abstract class CondimentDecorator extends Beverage {
	Beverage beverage;
	public abstract String getDescription(); // 모든 첨가물 데코레이터에서 getDescription()메소드를 새로 구현하도록 만들 계획이다.
}
// 생성자에서 넘겨받은 Beverage의 인스턴스를 가지고, 현재 인스턴스에 구현되어 있는 객체의 필드와 메소드에 접근
public class Mocha extends CondimentDecorator {
	public Mocha(Beverage beverage) {
		this.beverage = beverage;
	}
 
	public String getDescription() {
		return beverage.getDescription() + ", Mocha";
	}
 
	public double cost() {
		return .20 + beverage.cost();
	}
}
public class mainTest {
	public static void main(String[] args) {
		 Beverage beverage = new Espresso();
		 System.out.println(beverage.getDescription()+", "+beverage.cost());
		 
	     beverage = new Mocha(beverage); 
	     beverage = new Mocha(beverage); 
	     beverage = new Whip(beverage); 
	        
	     System.out.println("메뉴 : " + beverage.getDescription());
	     System.out.println("가격 : " + beverage.cost());
	}
}

main에서 같이 초기화 시켰을 때,Whip- Mocha-Mocha-Espresso 이렇게 감싸지는 형태로 객체가 생성 되게 된다. 첨가물들의 beverage 필드를 통해 저런 형태의 체인이 만들어 지게 되는데,

 

이때 제일 외부의 Whip객체의 getDescription 메소드를 실행 시키면, beverage에 저장 되어 있는 Mocha 객체의 getDescription으로, 그리고 이어서 Espresso 객체에 있는 getDescription 메소드가 차례로 호출 되게 됩니다.

 

그리고 리턴은 역순으로 일어나게 되므로,  Espresso, Mocha, Mocha, Whip 이 리턴되는 것이다.

 

cost 메소드 또한 Espresso 까지 호출되어 가서, 리턴 되며 가격이 더해져서 최종적으로 더해진 가격이 나온다.

 

 

3. 다시 정의

객체에 추가적인 요건을 동적으로 첨가한다. 데코레이터는 서브클래스를 만드는 것을 통해서 기능을 유연하게 확장할 수 있는 방법을 제공한다. 즉, 어떤 기능에 추가적으로 기능을 덧붙이고 싶은 경우, 그 기능들을 Decorator로 만들어서 덧붙이는 방식이다.

 

728x90
반응형