상세 컨텐츠

본문 제목

Decorator Pattern 배워보기

Java

by techbard 2014. 1. 15. 17:10

본문

반응형

요즘 자바를 배우고 있는데... 재미있는 패턴을 공부하게 되어 정리해 본다.


예제는 이곳에서 가져왔다.


이클립스에서 eUML2 플러그인으로 예제소스를 클래스 다이어그램으로 나타난 건 다음과 같다.





먼저 최상위 인터페이스를 작성한다.


1. 인터페이스 이름은 관례적으로 I로 시작하며 두개의 메소드 이름만 정의한다.


public interface ICoffee {

public double getCost();

public String getIngredients();

}


2. 최상위 인터페이스를 직접 구현하는 하부 클래스를 만든다.


public class SimpleCoffee implements ICoffee {


@Override

public double getCost() {

return 1;

}


@Override

public String getIngredients() {

return "coffee";

}

}


여기까지는 추가 기능을 구현하기 전, 기본 기능 구현에 해당한다.

이 SimpleCoffee 클래스를 테스트 해보자.


public class SimpleCoffeeTest {


public static void main(String[] args) {

ICoffee ic = new SimpleCoffee();

System.out.println("Cost: " + ic.getCost() + "; Ingredients: " + ic.getIngredients());

}

}


결과:

Cost: 1.0; Ingredients: coffee


여기까지는 최초 기능 구현에 해당하며, 데코레이터 패턴은 여기서 다시 향후에 구현될 수 있는 여지를 남겨두기 위해 데코레이터 클래스를 작성한다.

3. 핵심 데코레이터 클래스 작성
(멤버로 최상위 클래스를 인자로 받아 저장하도록 하며, 메소드의 인터페이스를 통일시키되 결국 오리지널 메소드를 호출하도록 구현한다.)

abstract public class CoffeeDecorator implements ICoffee {
protected ICoffee decoratedCoffee;
protected String ingredientSeparatorString = ", ";
public CoffeeDecorator(ICoffee decoratedCoffee) {
this.decoratedCoffee = decoratedCoffee;
}

@Override
public double getCost() {
return decoratedCoffee.getCost();
}

@Override
public String getIngredients() {
return decoratedCoffee.getIngredients();
}
}

==> 이 클래스가 데코레이터 패턴 구현에 가장 핵심적인 부분이다.

ICoffee 인터페이스를 구현하였기 때문에 getCost(), getIngredients() 메소드를 오버라이딩한다.

또한, 생성자로 ICoffee 인터페이스 타입을 받아 이것을 protected로 지정하여 이 CoffeeDecorator 클래스를 상속한 클래스에서 ICoffee 인터페이스 타입을 사용할 수 있게한다.

단, 오버라이딩된 메소드에서는 생성자에서 넘겨받은 ICoffee 타입의 메소드 들을 사용할 수 있도록 return decoratedCoffee.getCost, return decoratedCoffee.getIngredients()로 구현한다.

4-1. 상세 데코레이션 클래스 작성

public class Milk extends CoffeeDecorator {

public Milk(ICoffee decoratedCoffee) {
super(decoratedCoffee);
}

@Override
public double getCost() {
return super.getCost() + 0.5;
}

@Override
public String getIngredients() {
return super.getIngredients() + ingredientSeparatorString + "Milk";
}
}

이 클래스에서는 CoffeeDecorator 클래스를 상속받았으며 super를 통해 데코레이터 클래스의 멤버와 메소드들을 사용할 수 있다.

4-2. 두 번째 상세 데코레이션 클래스 작성

public class Whip extends CoffeeDecorator {

public Whip(ICoffee decoratedCoffee) {
super(decoratedCoffee);
}

@Override
public double getCost() {
return super.getCost() + 0.7;
}

@Override
public String getIngredients() {
return super.getIngredients() + ingredientSeparatorString + "Whip";
}
}

이제 다 되었다. SimpleCoffee, Milk, Whip 클래스를 테스트 해보자.

public class CoffeeTest {

public static void main(String[] args) {
ICoffee c = new SimpleCoffee();
System.out.println("Cost: " + c.getCost() + "; Ingredients: " + c.getIngredients());
c = new Milk(c);
System.out.println("Cost: " + c.getCost() + "; Ingredients: " + c.getIngredients());
c = new Whip(c);
System.out.println("Cost: " + c.getCost() + "; Ingredients: " + c.getIngredients());
}
}

결과:
Cost: 1.0; Ingredients: coffee
Cost: 1.5; Ingredients: coffee, Milk
Cost: 2.2; Ingredients: coffee, Milk, Whip

c = new Milk(c); 블럭을 없앤 결과를 다시 보자.

public class CoffeeTest {

public static void main(String[] args) {
ICoffee c = new SimpleCoffee();
System.out.println("Cost: " + c.getCost() + "; Ingredients: " + c.getIngredients());

c = new Whip(c);
System.out.println("Cost: " + c.getCost() + "; Ingredients: " + c.getIngredients());
}
}

결과:

Cost: 1.0; Ingredients: coffee

Cost: 1.7; Ingredients: coffee, Whip


즉, 실행되지 않은 부분을 제외하고 반복호출을 통해 메소드들이 실행됨을 알 수 있다.



이 패턴의 오묘한 점은 반복 실행에 있는데 이전 클래스를 호출하면 그 호출된 클래스가 또 상위를 호출하는 걸 반복한다.


6번 라인에서 SimpleCoffee 클래스의 메소드들이 실행되어 Cost는 1, 재료는 coffee를 출력한다.


9번 라인에서는 Milk 클래스가 CoffeeDecorator 클래스를 상속하고 있으므로 멤버로 decoratedCoffee에 SimpleCoffee의 객체주소를 가지는 클래스를 생성하게 된다.

A. Milk 클래스의 super.getCost()는 CoffeeDecorator의 decoratedCoffee.getCost() 메소드를 가리키고, 이것은 메소드 인자로 받은 SimpleCoffee의 getCost() 메소드를 호출하게 된다.

B. 그 결과가 Milk 클래스 내의 1.0 + 0.5로 계산된다.

C. 12번 라인의 Whip 클래스에서는

9번 라인에서의 Milk 클래스를 가리키는 객체변수를 인자로 받고, 그 Milk 클래스 내부에는 앞서 과정을 통해 다시 SimpleCoffee를 가리키고 있기 때문에 반복해서 체인을 타듯이 앞에서부터 결과가 실행되어

1 + 0.5 + 0.7 = 2.2 가 된다.

 

동적으로 기능을 추가할 수 있지만 클래스를 추가할 때마다 체인을 타는 과정을 계속 반복하게 된다.

 

이것이 가능한 이유는 추상 데코레이터인 CoffeeDecorator를 구상 데코레이터들이 상속 (extends)하고 있으므로 부모에 있는 멤버인 decoratedCoffee를 자신의 멤버로 가질 수 있으며 CoffeeTest에서 new로 구상 객체를 생성할 때 제각각 앞에서 호출한 객체를 decoratedCoffee 멤버에 담고 있는 상태 자체를 뒤에서 포함시켜 다시 가리키고 있기 때문이다.

따라서, 구상 데코레이터에서의 super.getCost() 메소드의 동작이 맨 처음 SimpleCoffee를 담을 때와 그 뒤에 Milk를 담을때가 각각 다르다. SimpleCoffee를 담을 때는 그냥 단순한 다형성을 이용해 자기 자신의 메소드를 호출한다면, Milk 부터는 멤버에 있는 decorated.getCost()를 호출하면 그것이 연쇄적으로 자신의 내부에 포함된 getCost()를 호출하게 된다.

DecoratorPattern.zip


반응형

관련글 더보기

댓글 영역