ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [디자인 패턴] 10. 데커레이터 패턴 (Decorator Design Pattern)
    Computer Science/Design Pattern 2022. 11. 11. 01:03
    728x90

    학습목표 : 1) 독립적인 추가, 기능의 조합 방법 이해하기 2) 데커레이터 패턴을 통한 기능의 조합 방법 이해

     

    데커레이터 패턴 (Decorator Pattern)

    기본 기능에 추가될 수 있는 많은 수의 부가 기능에 대해서 각 추가 기능을 Decorator 클래스로 정의한다.

    이후 필요한 Decorator 객체들을 조합함으로써 다양한 추가 기능의 조합을 동적으로 구현할 수 있는 패턴이다.

     

    예를 들어 스타벅스에서,

    기본 커피 메뉴에서 추가로 휘핑크림, 모카, 우유 등의 Decorator 옵션을 추가할 수 있다.

     

    여기서 모든 추가 Decorator 옵션들의 모든 경우의 조합을 각각 클래스로서 구현하는 것이 아닌,

    데커레이터 패턴을 이용해서 동적으로 기능을 조합해 객체를 만들 수 있다.

    • Component
      • 기본 기능을 뜻하는 ConcreteComponent와 추가 기능을 뜻하는 Decorator의 공통 기능을 정의한다. 클라이언트는 Component를 통해 실제 객체를 사용
    • ConcreteComponent
      • 기본 기능을 구현하는 클래스
    • Decorator
      • 많은 수가 존재하는 구체적인 Decorator의 공통 기능을 제공
    • ConcreteDecoratorA, ConcreteDecoratorB : Decorator의 하위 클래스로 기본 기능에 추가되는 개별적인 기능을 뜻함

     

     

     

    예시

     

    도로 표시

    • RoadDisplay : 기본 도로 표시 기능 제공
    • RoadDisplayWithLane : 기본 도로 표시에 추가적으로 차선을 표시
    public class Client {
    
    	public static void main(String[] args) {
    		// TODO Auto-generated method stub
    		RoadDisplay road = new RoadDisplay();
    		road.draw();
    		
    		RoadDisplay roadWithLane = new RoadDisplayWithLane();
    		roadWithLane.draw();
    	}
    }
    public class RoadDisplay {
    	public void draw() {
    		System.out.println("도로 기본 표시");
    	}
    }
    public class RoadDisplayWithLane extends RoadDisplay{
    	public void draw() {
    		super.draw();
    		drawLane();
    	}
    	private void drawLane() {
    		System.out.println("차선 표시");
    	}
    }

    문제점

    • 또다른 추가적인 도로 표시 기능을 구현하고 싶다면 어떻게 해야 하나?
      • 기본 도로 표시에 교통량을 표시하고 싶다면?
    • 여러 가지 추가 기능을 조합하여 제공하고 싶다면 어떻게 해야 하는가?
      • 기본 도로 표시에 차선 표시 기능과 교통량 표시 기능을 함께 제공하고 싶다면?

    새로운 기능 추가와 여러 부가 기능을 조합해서 제공 (모든 경우의 조합 기능을 클래스로 구현해야 하는 문제점 발생)

    해결책 (데커레이더 패턴 사용)

    • 추가 기능 별로 개별적인 클래스를 설계하고 이를 조합한다.

    public abstract class Display {
    	public abstract void draw();
    }
    public class DisplayDecorator extends Display{
    	private Display decoratedDisplay;
    	
    	public DisplayDecorator(Display decoratedDisplay) {
    		this.decoratedDisplay = decoratedDisplay;
    	}
    	
    	public void draw() {
    		decoratedDisplay.draw();
    	}
    }
    public class LaneDecorator extends DisplayDecorator {
    	public LaneDecorator(Display decoratedDisplay) {
    		super(decoratedDisplay);
    	}
    	
    	public void draw() {
    		super.draw();
    		drawLane();
    	}
    	private void drawLane() {System.out.println("\t차선 표시");}
    }
    public class RoadDisplay extends Display{
    	public void draw() {
    		System.out.println("도로 기본 표시");
    	}
    }
    public class TrafficDecorator extends DisplayDecorator {
    	public TrafficDecorator(Display decoratedDisplay) {
    		super(decoratedDisplay);
    	}
    	public void draw() {
    		super.draw();
    		drawTraffic();
    	}
    	private void drawTraffic() {System.out.println("\t교통량 표시");}
    }
    public class Client {
    
    	public static void main(String[] args) {
    		// TODO Auto-generated method stub
    		Display road = new RoadDisplay();
    		road.draw();
    		
    		Display roadWithLane = new LaneDecorator(new RoadDisplay());
    		roadWithLane.draw();
    		
    		Display roadWithTraffic = new TrafficDecorator(new RoadDisplay());
    		roadWithTraffic.draw();
    	}
    }
    public class Client_2 {
    
    	public static void main(String[] args) {
    		// TODO Auto-generated method stub
    		Display roadWithLaneAndTraffic = new LaneDecorator(new TrafficDecorator(new RoadDisplay()));
    		roadWithLaneAndTraffic.draw();
    	}
    }

    Client.java 실행
    Client_2.java 실행

      원하는 결과대로 도로 표시 조합이 이루어졌다. 도로를 표시하는 기능만 필요하다면 RoadDisplay 객체만을 이용하면 되고, 차선 표시 기능까지 필요하다면 RoadDisplay, LaneDecorator 객체를 이용하면 된다. 그리고 교통량 표시 기능이 필요하다면 RoadDisplay, TrafficDecorator객체를 함께 이용하면 된다.

     

      Client 코드에서 road, roadWithLane, roadWithTraffic 객체의 접근이 모두 Display 클래스를 통해 이루 진다. 기본도로, 차선, 교통량의 조합에 관계없이Client 클래스는 동일한 Display 클래스만을 통해 일관성 있는 방식으로 도로 정보를 표시할 수 있게 되었다.

     

      Client_2 코드에서 도로 기본 표시와 여러 기능의 조합을 한 번에 조합하여 객체를 생성할 수도 있다.


    데커레이터 패턴 실습

    • 데커레이터 패턴
      • 객체에 추가 요소를 동적으로 더함
      • OCP 원칙 만족 : 확장은 쉽게 (Open), 변경은 어렵게 (Closed)

     

    스타벅스 카페 실습

     

    • 다크로스트는 Beverage를 상속받아 cost() 메소드 가짐
    • 모카는 데코레이터. 모카는 Beverage의 sub class 이므로 Beverage 객체로 간주 가능 cost() 사용 가능
    • 휘핑크림도 데코레이터. Beverage 객체이므로 cost() 사용 가능
    • 최종 cost() 메소드 이용 가격 계산
      • 가장 바깥 데코레이터 Whipping의 cost() 호출 -> cost() 계산 mocha에 위임 -> mocha도 dark Roast에 위임 -> 다크로스트 가격 리턴 -> 모카 가격 추가 후 리턴 -> whipping 가격 추가 후 총액 리턴

     

    소스코드

    Beverage (Component 클래스: 기본 기능과 추가 기능의 공통 기능을 정의)

    public abstract class Beverage {
    	String description = "제목 없음";
    	
    	public String getDescription() {
    		return description;
    	}
    	
    	public abstract double cost();
    }

    CondimentDecorator (Decorator 클래스: 추가 기능의 공통 기능을 정의)

    //커피 첨가물 데코레이터 클래스
    //베버리지 객체 들어갈 수 있어야 하므로 확장 상속받음
    public abstract class CondimentDecorator extends Beverage {
    	//각 데코레이터가 감쌀 음료를 나타내는 Beverage 객체를 저장
    	Beverage beverage;
    	//모든 첨가물 데코레이터에 해당 메소드를 새로 구현
    	public abstract String getDescription(); 
    }

    DarkRoast (ConcreteComponent: 기본 기능을 구현하는 클래스)

    public class DarkRoast extends Beverage {
    	
    	public DarkRoast() {
    		description = "다크로스트 커피";
    	}
    	
    	public double cost() {
    		return 0.99;
    	}
    }

    DeCaffeine (ConcreteComponent: 기본 기능을 구현하는 클래스)

    public class DeCaffeine extends Beverage {
    	
    	public DeCaffeine() {
    		description = "디카페인 커피";
    	}
    	public double cost() {
    		return 1.05;
    	}
    }

    Espresso (ConcreteComponent: 기본 기능을 구현하는 클래스)

    //기본 기능 구현 에스프레소 클래스 (ConcreteComponent)
    public class Espresso extends Beverage {
    	
    	public Espresso() {//생성자
    		description = "에스프레소"; //Beverage에 선언한 description 변수값 설정
    	}
    	
    	public double cost() {
    		return 1.99; //에스프레소 가격 리턴
    	}
    }

    HouseBlend (ConcreteComponent: 기본 기능을 구현하는 클래스)

    public class HouseBlend extends Beverage{
    
    	public HouseBlend() {
    		description = "하우스 블랜드 커피";
    	}
    	
    	public double cost() {
    		return 0.89;
    	}
    }

    Milk (ConcreteDecorator: 기본 기능에 추가되는 기능을 정의)

    public class Milk extends CondimentDecorator{
    	
    	public Milk(Beverage beverage) {
    		this.beverage = beverage;
    	}
    	
    	public String getDescription() {
    		return beverage.getDescription() + ", 우유";
    	}
    	
    	public double cost() {
    		return beverage.cost() + 0.1;
    	}
    }

    Mocha (ConcreteDecorator: 기본 기능에 추가되는 기능을 정의)

    //첨가물(재료)를 확장하는 클래스 - 모카
    public class Mocha extends CondimentDecorator{
    	
    	//인스턴스 변수를 감싸고자 하는 생성자
    	public Mocha(Beverage beverage) { //감싸고자하는 음료를 저장하는 인스턴스 변수
    		this.beverage = beverage; 
    	}
    	//장식(데코레이팅)하는 객체에 대한 설명값에 모카를 더한 값을 리턴
    	public String getDescription() {
    		return beverage.getDescription() + ", 모카";
    	}
    	//장식하고 있는 객체의 가격에 모카 가격을 더한 후 리턴
    	public double cost() {
    		return beverage.cost() + 0.20;
    	}
    }

    SoyMilk (ConcreteDecorator: 기본 기능에 추가되는 기능을 정의)

    //첨가물(재료)를 확장하는 클래스 - 두유
    public class SoyMilk extends CondimentDecorator {
    	
    	public SoyMilk(Beverage beverage) {
    		this.beverage = beverage;
    	}
    	
    	public String getDescription() {
    		return beverage.getDescription() + ", 두유";
    	}
    	
    	public double cost() {
    		return beverage.cost() + 0.15;
    	}
    }

    Whip (ConcreteDecorator: 기본 기능에 추가되는 기능을 정의)

    public class Whip extends CondimentDecorator {
    	
    	public Whip(Beverage beverage) {
    		this.beverage = beverage;
    	}
    
    	public String getDescription() {
    		return beverage.getDescription() + ", 휘핑크림";
    	}
    	
    	public double cost() {
    		return beverage.cost() + 0.10;
    	}
    }

    Starbucks (Main)

    public class Starbucks {
    
    	public static void main(String[] args) {
    		// TODO Auto-generated method stub
    		//기본 커피 에스프레소 주문
    		Beverage beverage = new Espresso();
    		System.out.println(beverage.getDescription()+" $"+beverage.cost());
    		
    		Beverage beverage2 = new DarkRoast();
    		beverage2 = new Mocha(beverage2); //모카로 데코레이팅 (다크로스트에 모카 추가)
    		beverage2 = new Mocha(beverage2); //모카x2 추가
    		beverage2 = new Whip(beverage2); //휘핑크림 추가
    		System.out.println(beverage2.getDescription() + " $" + beverage2.cost());
    	
    		Beverage beverage3 = new HouseBlend();
    		beverage3 = new SoyMilk(beverage3);
    		beverage3 = new Mocha(beverage3);
    		beverage3 = new Whip(beverage3);
    		System.out.println(beverage3.getDescription() + " $" + beverage3.cost());
    		
    		//데코레이터 패턴 이용 다양한 기능(객체)들이 새롭게 추가되는 환경이 많을 때 효율적으로 추가 가능
    		//추가되는 기능이 많은경우 데코레이터 패턴 사용
    	}
    }

    실행 결과

     

    728x90
    반응형

    댓글

Keydi's Tistory