2016년 2월 20일 토요일

데코레이터 패턴(Decorator Pattern)과 프록시 패턴

1) 데코레이터 패턴(Decorator Pattern) 정의

1-1) 데코레이터 패턴(Decorator Pattern) 

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

1-2) 데코레이터 단점 

데코레이터 패턴을 이용해 디자인을 하다 보면 잡다한 클래스들이 많아 질 수 있다. 겹겹이 애워싼 객체의 정체를 알기가 힘들다.

2) 데코레이터 패턴 핵심정리

- 상속을 통해 확장을 할 수도 있지만, 디자인 유연성 면에서는 별로 좋지 않다.
- 기존 코드를 수정하지 않고도 행동을 확장하는 방법이 필요하다.
- 구성과 위임을 통해서 실행중에 새로운 행동을 추가할 수 있다.
- 상속대신 데코레이터 패턴을 통해서 행동을 확장 할 수 있다.
- 데코레이터 패턴에서는 구상 구성요소를 감싸주는 데코레이터들을 사용한다.
- 데코레이터의 수퍼클래스는 자신이 장식하고 있는 객체의 수퍼클래스와 같다.
- 데코레이터 패턴을 사용하면 자질한 객체들이 많이 추가될 수 있고, 데코레이터를 너무 많이 사용하면 코드가 필요 이상으로 복잡해 질 수 있다.

3) OCP (Open-Closed Principle)

- OCP는 가장 중요한 디자인 원칙 가운데 하나다.
- 클래스는 확장에 대해서는 열려 있어야 하지만 코드 변경에 있어서는 닫혀 있어야 한다.
- 즉 기존 코드는 건드리지 않은 채로 확장을 통해서 새로운 행동을 간단하게 추가할 수 있도록 하면,
  새로운 기능을 유연하게 추가할 수 있어, 주변 환경에 잘 적응할 수 있으면서도 강하고 튼튼한 디자인을 만들 수 있다.

4) 데코레이터 패턴(Decorator Pattern) 예제

스타버즈 커피는 엄청난 급속도로 성장한 초대형 커피 전문점으로 유명하다.
스타버즈 커피샵은 워낙 빠르게 생장했기 때문에 다양한 음료를 모두 포괄하는 주문시스템을 이제 갖추려고 한다.

처음 사업을 시작할 무렵에 만들어진 클래스는 아래와 같다.


Beverage는 음료를 나타내는 추상 클래스이며, 커피샵에서 판매되는 모든 음료는 이 클래스의 서브클래스가 된다.
추상메소드인 cost()메소드를 새로 정의하여 가격을 구현한다.
처음에는 하우스블렌드, 다크로스트, 디카페인, 에스프레소 네 가지만 판매 하게 된다..

커피를 주문할 때 스팀 우유나 두유, 모카를 추가하고, 그 위에 휘핑 크림을 얹기도 해야 한다면..



우와 클래스 개수가 말 그대로 폭발적으로 늘어나는 군..
정말 이대로 운영한다면 클래스 관리하는게 만만치가 않겠네.
우유 가격이 인상된다면?, 카라멜 토핑을 새로 추가한다면? 생각만 해도 끔찍하네..

4-1) 첫 번째 리팩토링

이거 정말 황당하네, 왜 이렇게 클래스가 많이 필요한 거죠?
그냥 인스턴스 변수하고 슈퍼클래스 상속을 써서 추가 사항을 관리하면 안될까요?

- Beverage라는 음료 클래스에 우유,두유,모카,휘핑크림을 나타내는 인스턴스 변수를 추가한다.
- 첨가물에 첨부 여부와, 첨가물을 설정하기 위한 has,set메소드를 생성 한다.
- cost()에서 추가된 첨가물의 가격을 계산한다. 서브클래스에서 cost() 메소드를 오버라이드 할 때  그 기능을 확장 하여 특정 음료의 가격을 더한다.
- 이제 클래스가 다섯개 밖에 안되네요. 진작에 이렇게 했어야죠?
- 근데 나중에 어떻게 바뀌어야 할지 생각해 보면 이 접근법에도 문제가 있지 않을까요?



public class Beverage{
public float cost();
float condimentCost = 0.0;

if(hasMilk())
condimentCost += milkCost;

if(hasSoy())
condimentCost += soyCost;

...
return condimentCost;
}



public class DarkRoast extends Beverage{

public DarkRoast(){
description = "최고의 다크로스트";
}

public float cost(){
return 1.99+super.cost();
}
}

어떤 문제가 있을까요?
- 첨가물 가격이 바뀔 때마다 기존 코드를 수정해야 한다.
- 첨가물의 종류가 많아지면 새로운 메소드를 추가해야 한다.
- 새로운 음료가 출시될 수도 있습니다. 그 중에는 특정 첨가물이 들어가면 안 되는 경우도 있을 겁니다.
- 아이스 티를 생각해 보면 Tea서브 클래스에서도 hasWhip()같은 메소드를 여전히 상속 받을 것이다.
- 손님이 더블 모카를 주문하면 어떻게 해야 할까요?



public class Beverage{
public float cost();
float condimentCost = 0.0;

if(hasMilk())
condimentCost += milkCost;

if(hasSoy())
condimentCost += soyCost;

...
return condimentCost;


}
사부와 제자 - 서브클래스를 만드는 방식으로 행동을 상속 받으면 그 행동은 컴파일시에 완전히 결정이 된고, 모든 서브 클래스에서 똑같은 행동을 상속 받아야 한다. - 하지만 구성을 통해 객체의 행동을 확장하면 실행중에 동적으로 행동을 설정 할 수 있다

(참고1) OCP( Open-Closed Principle)
- OCP( Open-Closed Principle)는 가장 중요한 디자인 원칙 가운에 하나다. - 클래스는 확장에 대해서는 열려 있어야 하지만 코드 변경에 대해서는 닫혀 있어야 한다. - 즉 기존 코드는 건드리지 않은 채로 확장을 통해서 새로운 행동을 간단하게 추가할 수 있도록 하면, 새로운 기능을 유연하게 추가할 수 있어, 주변 환경에 잘 적응할 수 있으면서도 강하고 튼튼한 디자인을 만들 수 있다.

4-2) 데코레이터 패턴 적용

이번에 사용 할 방법은 특정 음료에서 시작해서 첨가물로 그 음료를 장식(decorate) 할 것이다. 예를 들어 모카와 휘핑크림을 추가한 다크로스트 커피를 주문 한다면 아래와 같이 할 수 있을 것이다.
① DarkRoast 객체를 가져온다.
 ② Mocha 객체로 장식한다.
③ Whip 객체로 장식한다.
④ cost() 메소드를 호출한다.
이때 첨가물의 가격을 계산하는 일은 해당 객체들에게 위임한다.

① DarkRoast 객체에서 시작 합니다.








② 손님이 Mocha를 주문했으니 Mocha객체를 만들고 그 객체로 DarkRoast를 감쌉니다.

 











③ 손님이 휘핑크림도 같이 주문했기 때문에  Whip 데코레이터를 만들고 그 자체로 Mocha를 감쌉니다.














④ 이제 가격을 계산해 볼까요? 
- 가장 바깥쪽에 있는 데코레이터 Whip의 cost()를 호출하면 된다. - 가장 바깥쪽에 있는 데   코레이터 Whip의 cost()를 호출한다. 
- Whip에서는 Mocha의 cost() 메소드를 호출한다. 
- Mocha에서는 다시 DarkRoast의 cost()를 호출한다. 
- DarkRoast에서는 가격과 이름을 반환한다. 
- Mocha에서는 DarkRoast의 리턴값과 모카값을 더해 반환한다. 
- Whip에서는 Mocha에서 받은 가격에 Whip가격을더해 최종 가격을 반환한다.


4-3) Beverage 클래스를 장식해 봅시다.








- Beverage는 가장 기본이 되는 Component 추상 클래스로 볼 수 있다.
- 커피 종류마다 Beverage에 대한 구상 클래스를 하나씩 만든다. (HouseBlend, DarkRoast, Expresso, Decaf)
  (Beverage클래스를 상속 받아 새로운 행동을 동적으로 추가하게 된다.)
- 각각의 첨가물을 나타내는 데코레이터를 추가합니다. cost() 뿐만 아니라 getDescription() 도 구현해야 한다.  (Mocha, Milk, Soy, Whip)
- 각 데코레이터 안에는 Beverage 클래스가 들어있다.
  즉, 데코레이터에는 구성요소에 대한 레퍼런스가 들어있는 인스턴스 변수가 있지요.

사무실에서 들은 이야기..
- CondimentDecorator에서 Beverage클래스를 확장하는 것은 상속이 맞다 - 데코레이터 패턴에서는 상속을 이용해서 형식을 맞추는 거지, 상속을 통해서 행동을 물려 받는게 목적이 아니다. - 데코레이터 패턴에서는 객체 구성(인스턴스 변수로 다른 객체를 저장하는 방식)을 이용하고 있기 때문에 음료하고 첨가물을 다양하게 섞어도 유연성을 잃지 않을 수 있다.

4-4) 코드를 만들어 봅시다.


//Beverage.java
public abstract class Beverage {

protected String description = "제목없음";

public abstract double cost();
public String getDescription() {
return description;

}
}

//Espresso.java
//에스프레소 커피
public class Espresso extends Beverage {

public Espresso(){
//Beverage로부터 상속받음
description = "에스프레소 커피";
}

@Override
public double cost() {
return 1.99;
}
}

//CondimentDecorator.java
public abstract class CondimentDecorator extends Beverage {

//모든 첨가물 데코레이터에서 getDescription() 메소드를 새로 구현하도록 만들 계획임
public abstract String getDescription();


Mocha.java
//Mocha는 데코레이터기 때문에 CondimentDecorator를 확장 합니다.
public class Mocha extends CondimentDecorator {

//감싸고자 하는 음료(하우스블렌드,다크로스트,디카페인,에스프레소)를 저장하는 인스턴스.
Beverage beverage;

//생성자를 이용해서 감싸고자 하는 음료 객체를 전달한다.
public Mocha(Beverage beverage){
this.beverage = beverage;
}

@Override
public String getDescription() {
//음료 명에 첨가물명을 추가한다.
return beverage.getDescription() + ", 모카";
}

//CondimentDecorator는 Beverage를 확장 하죠
@Override
public double cost() {
//음료 가격에 모카 가격을 추가한다.
return .20 + beverage.cost();
}
}

//StarbuzzCoffee.java
public class StarbuzzCoffee {
public static void main(String[] args) {

//에스프레소 커피
Beverage espresso = new Espresso();
System.out.println(espresso.getDescription()+
" : $"+espresso.cost());

//다크로스트 커피 + 모카+ 모카 + 휘핑크림
Beverage darkRoast = new DarkRoast(); //다크로스트 커피
darkRoast = new Mocha(darkRoast); //모카 추가
darkRoast = new Mocha(darkRoast); //모카 한번 더 추가
darkRoast = new Whip(darkRoast); //휘핑크림 추가
System.out.println(darkRoast.getDescription()+
" : $"+darkRoast.cost());

//하우스 블렌드 커피, 두유, 모카, 휘핑크림
Beverage houseBlend = new HouseBlend(); //하우스 블렌드 커피
houseBlend = new Soy(houseBlend); //두유 추가
houseBlend = new Mocha(houseBlend); //모카 추가
houseBlend = new Whip(houseBlend); //휘핑크림 추가
System.out.println(houseBlend.getDescription()+
" : $"+houseBlend.cost());
}
}
에스프레소 커피 : $1.99 다크 로스트 커피, 모카, 모카, 휘핑크림 : $1.49 하우스 블렌드 커피, 두유, 모카, 휘핑크림 : $1.34


5) 데코레이터가 적용된 예 : 자바 I/O

java.io 패키지에는 어마어마하게 많은 클래스들이 있지만, 많은 부분이 데코레이터 패턴을 바탕으로 만들어져 있다.


스타 버즈 디자인하고 별로 다르지 않죠? 출력 스트림의 디자인도 똑같다.
자바 I/O를 보면 데코레이터의 단점도 발견 할 수 있다.
데코레이터 패턴을 이용해서 디자인을 하다 보면 잡다한 클래스들이 너무 많아 진다.

6) 프록시 패턴(proxy pattern)이란?

어떤 객체에 대한 접근을 제어하기 위한 용도로 대리인이나 대변인에 해당하는 객체를 제공하는 패턴


- 원격프록시 : 원격객체에 대한 접근을 제어
- 가상프록시 : 생성하기 힘든 자원에 대한 접근을 제어
- 보호프록시 : 접근권한이 필요한 자원에 대한 접근을 제어

6-1) 원격프로시 

6-1-1) 원격프록시 개요

로컬 환경에 존재하면서, 원격객체에 대하여 대변자 역할을 하는 객체를 원격 프록시라고 한다.

- 원격객체 : 다른 JVM의 Heap영역에 살고있는 객체

6-1-2) 원격에 있는 왕뽑기 기계를 감시하는 모니터링 프로그램 만들기


클라이언트 객체(GumballMonitor)에서는 원격객체(GumballMachine)의 메소드를 호출하는 것처럼 행동하지만, 실제로는 로컬 힙에 들어있는 원격객체 모양과 비슷한 '프록시'객체의 메소드를 호출하고 있는 것이다.
네트워크 통신과 관련된 저수준 작업은 이 프록시 객체에서 처리해준다.

6-1-3) 원격메소드의 기초


1. 클라이언트 객체에서 클라이언트 보조객체의 메소드를 호출한다.
2. 클라이언트 보조객체에서는 메소드 호출에 대한 정보(인자, 메소드이름 등)를 잘 포장해서 네트워크를 통해 서비스 보조객체한테 전달한다.
3. 서비스 보조객체에서는 클라이언트 보조객체로 부터 받은 정보를 해석하여 어떤 객체의 어떤 메소드를 호출할지 알아낸 다음 진짜 서비스객체의 '진짜 메소드'를 호출한다.
4. 서비스객체의 메소드가 호출되고, 메소드 실행이 끝나면 서비스 보조객체에 결과를 리턴해준다.
5. 호출결과로 리턴된 정보를 포장해서 서비스 보조객체에서 클라이언트 보조객체한테 전달한다.
6. 클라이언트 보조객체에서는 리턴된 값을 해석하여 클라이언트 객체한테 리턴한다.

클라이언트 객체 입장에서는 메소드 호출이 어디로 전달되었었는지, 어디에서 왔는지 전혀 알 수 없다.

6-2) 가상프록시

6-2-1) 가상프록시 개요

생성하는데 많은 비용이 드는 객체를 대신하는 역할을 한다. 실제로 진짜 객체가 필요하게 되기 전까지 객체의 생성을 미루게 해 주는 기능을 제공하거나, 객체 생성 전, 또는 생성도중에 객체를 대신하기도 한다. 객체 생성이 완료되고나면 프록시에서 RealSubject에 요청을 직접 전달한다.






import java.util.*;

interface Image {
public void displayImage();
}

//on System A
class RealImage implements Image {
private String filename;
public RealImage(String filename) {
this.filename = filename;
loadImageFromDisk();
}

private void loadImageFromDisk() {
System.out.println("Loading " + filename);
}

public void displayImage() {
System.out.println("Displaying " + filename);
}
}

//on System B
class ProxyImage implements Image {
private String filename;
private Image image;

public ProxyImage(String filename) {
this.filename = filename;
}
public void displayImage() {
if (image == null)
{
image = new RealImage(filename);
}
image.displayImage();
}
}

class ProxyExample {
public static void main(String[] args) {
Image image1 = new ProxyImage("HiRes_10MB_Photo1");
Image image2 = new ProxyImage("HiRes_10MB_Photo2");

image1.displayImage(); // loading necessary
image2.displayImage(); // loading necessary
}
}



*reference

  • http://wiki.gurubee.net/pages/viewpage.action?pageId=1507398
  • http://wiki.gurubee.net/pages/viewpage.action?pageId=1507415
  • https://ko.wikipedia.org/wiki/%ED%94%84%EB%A1%9D%EC%8B%9C_%ED%8C%A8%ED%84%B4



    Share:

    0 개의 댓글:

    댓글 쓰기