이번 주부터는 [번외] 로 작성되는 리뷰 입니다.


스터디에서는 가장 많이 활용할 법한 패턴들을 다뤘지만, 

지금부터는 그외에 적어도 "기본서에 서술된 패턴" 들에 대한 이야기를 다뤄볼 생각입니다.


고작, 6개의 패턴정도만 다루고 이 카테고리를 끝내기에는 조금 아쉽더군요..... 

아마 이 것이 More OOP & FP....


참고로 여기서 기본서란 아래 책을 말합니다.


Head First Design Patterns 헤드 퍼스트 디자인 패턴
국내도서
저자 : 에릭 프리먼(Eric Freeman) / 서환수역
출판 : 한빛미디어 2005.09.04
상세보기


번외로 시작하는 이번 포스팅에서 처음 다뤄볼 패턴은 "주어진 상황 및 용도에 따라 객체에 책임을 덧붙일 수 있는 Decorator Pattern" 입니다.


이 패턴을 사용하면 동적으로 기능을 확장할 수 있으며, 클래스의 기능을 확장 하려면 꼭 상속을 해야 한다는 법칙을 대신할 수 있는 대안이 될 수 있을 것 같습니다.


한번, 요구사항과 함께 본격적으로 리뷰를 해보겠습니다.



1. Coffee 의 가격문제.


우리가 마시는 Coffee 에는 다양한 재료가 포함될 수 있습니다. 

예를들어, Latte 에는 Espresso 와 Milk 가 포함되며, Mocha 에는 Latte 재료에 Chocolate Syrup 이 포함됩니다.

또한, Coffee 종류에 따라 Espresso 역시 다른 블랜딩 방식을 사용할 수 있을 것 같습니다.


이렇게 Coffee 마다 사용하는 재료는 다양하며, 

그에 따라 가격을 책정하고 싶은 것이 이번 포스팅의 최종 목표입니다.



2. 상속관계를 통한 Coffee 가격 계산.


요구사항은 그렇게 큰 문제는 아닌 것처럼 보입니다.


Coffee 라는 최상위 클래스를 만들고 해당 클래스에서는 Coffee::getCost 라는 행위에 대한 서명을 명시합시다. 

물론 구체적인 구현은 하위 클래스들이 할 일이죠..


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/**
 * 커피 클래스
 *
 * <pre>
 *     계산에 대한 기능을 명시.
 * </pre>
 *
 * Created by Doohyun on 2017. 6. 25..
 */
public abstract class Coffee {
 
     /**
     * 커피 이름 출력
     *
     * @return
     */
    public abstract String getName();
 
    /**
     * 코스트에 대한 메소드 서명 정의
     *
     * <pre>
     *     해당 클래스를 상속받는 클래스는 모두 이 기능을 구현해야함.
     * </pre>
     *
     * @return
     */
    public abstract Integer getCost();
}
 
/**
 * 라떼 기능 구현.
 *
 * Created by Doohyun on 2017. 6. 25..
 */
public class Latte extends Coffee{
 
    @Override
    public String getName() {
        return "라떼";
    }
 
    /**
     * 라떼의 가격은 5900 원.
     *
     * @return
     */
    @Override
    public Integer getCost() {
        return 5900;
    }
}
cs


이렇게 제품마다 가격에 대한 기능을 적절하게 구현해준다면, 요구사항을 쉽게 정리할 수 있을 것 같습니다. 


이를 UML 로 표현해보면 다음과 같을 것 같군요.

 




일단, 제품에 따라 가격이란 개념을 표현할 수는 있습니다. 


하지만 요구사항에서는 재료따라 Coffee 의 가격이 책정되길 바랍니다. 

이는 즉 재료의 가격이 변하면 Coffee 의 가격도 변하길 바람을 말하는 듯 합니다.



3. 재료기능에 대한 기능 추가 및 상속구조 변화.


재료에 대한 기능을 상위 클래스에 정의를 함으로써, 이 문제를 해결할 수 있을 것 같습니다.


기본적인 커피 가격을 부모클래스에서 책정하고, 사용하는 재료에 따라 하위클래스에서 적절하게 가격을 추가하는 것이죠.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
/**
 * 커피 클래스
 *
 * <pre>
 *     계산에 대한 기능을 명시.
 * </pre>
 *
 * Created by Doohyun on 2017. 6. 25..
 */
public abstract class Coffee {
 
    /**
     * 우유 사용여부
     *
     * @return
     */
    public abstract Boolean getHasMilkYn();
 
    /**
     * 코스트에 대한 메소드 서명 정의
     *
     * <pre>
     *     기본적인 커피가격.
     * </pre>
     *
     * @return
     */
    public Integer getCost() {
        // 처음 가격은 4000 원.
        Integer cost = 4000;
 
        if (getHasMilkYn()) {
            // 우유 가격 추가.
            cost+=900;
        }
 
        return cost;
    }
}
 
/**
 * 하우스 블렌딩 방식으로 만든 에스프레소
 *
 * Created by Doohyun on 2017. 6. 25..
 */
public class HouseBlendingEspresso extends Coffee{
 
    @Override
    public Boolean getHasMilkYn() {
        return false;
    }
 
    /**
     * 하우스 블렌딩 방식은 일반 에스프레소보다 가격이 1000 원 비쌈.
     *
     * @return
     */
    @Override
    public Integer getCost() {
        return 1000 + super.getCost();
    }
}
 
/**
 * 라떼는 하우스 블렌딩 방식 에스프레소를 사용.
 *
 * Created by Doohyun on 2017. 6. 25..
 */
public class Latte extends HouseBlendingEspresso{
 
    @Override
    public Boolean getHasMilkYn() {
        return true;
    }
}
cs


이제, 재료에 따라 가격도 연동될 것입니다. 

우유 가격을 정확히 명시하였으며, 우유를 사용하는 Latte 는 사용여부만 적절히 구현해주면 됩니다.

(우유 가격 변동에 따라 유연하게 Latte 의 가격을 변동시킬 수 있습니다.)


심지어, 상속관계를 이용하여 Blending 방식에 따른 커피가격 역시 변동할 수 있도록 구현하였습니다. 


즉 상속구조 및 적절한 메소드 추가를 통해 부모의 기능에 자신의 입맛에 맞도록 적절한 치장을 하고 있습니다만, 이 구조는 다음과 같은 아쉬움이 존재합니다.


- 재료 추가에따라 Coffee 에 기능을 추가하고, Coffee::getCost 를 수정해야합니다.


- Syrup 을 추가해야한다면, Coffee::getHasSyrupYn 를 추가해야합니다.

Coffee::getHasSyrupYn 의 추가는, 모든 구현클래스들에 이 기능을 구현해야함을 의미합니다. 

  (Interface 서명 변경과 같은 문제입니다.)

- 또한, Coffee::getCost 역시 수정해야 합니다.


- 각 최종 구현클래스들은 재료에 유연할 수 없습니다.


- 상속구조 기반으로 재료관계를 명시하였기 때문에, 컴파일 시간에 모든 행위가 결정 됩니다.

- Latte 는 무조건 HouseBlendingEspresso 만 사용합니다. 

  즉, 다른 블렌딩을 사용하려면 새 클래스를 만들어야 합니다.

- 또한 더블샷, 시럽추가 등의 재료에 대한 유연함을 줄 수 없음을 의미합니다.


즉, 지금 구조는 새로운 요구사항 추가에 있어서 기존 코드를 수정해야할 가능성이 농후하네요..



4. 상속관계에서 연관관계로 리팩토링.


사실 모든 상속관계는 연관관계(has-a) 로 풀 수 있습니다. (Favor has-a over is-a )


우리는 이 규칙을 Observer 패턴의 리뷰 중 [자바 내장 모듈의 한계 및 극복] 에서 다뤄본 적이 있습니다.



연관관계를 이용하면, 상속관계를 실행시간에 동적으로 정의해줄 수 있습니다.


Decorator 패턴은 이러한 연관관계를 이용하여 기능의 변화를 수행하기 위한 패턴입니다.


이를 이용하여 현재 문제로 드러난 "상속에 의한 컴파일 시점에 정의된 재료의 변경 문제" 를 해결할 수 있을 것 같아 보이며, 연관관계이기 때문에 "재료 추가 및 변경에 대해 자유" 를 기대해 볼 수 있을 것 같습니다.


한 번, 장식을 위한 클래스를 다음과 같이 정의해보죠..


장식(Decorator) 객체는 기본적으로 장식 대상 객체와 동일한 Type 이며, 

장식된 객체는 기존의 객체를 대신하여 역할을 수행할 수 있도록 구현할 것입니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
 * 조미료가 되는 클래스 정의.
 *
 * Created by Doohyun on 2017. 6. 25..
 */
public abstract class CondimentDecorator extends Coffee {
 
    private Coffee coffee;
 
    /**
     * 커피를 has-a 관계로 표현.
     *
     * @param coffee
     */
    public CondimentDecorator(Coffee coffee) {
        this.coffee = coffee;
    }
 
    /**
     * 커피를 출력하도록 변경.
     *
     * @return
     */
    protected Coffee getCoffee() {
        return this.coffee;
    }
}
 
cs


생성자 부분에서는 장식대상이 되는 객체를 입력받아 has-a 관계로 유지하고자 합니다.

행위의 추가는 장식대상이 되는 객체를 사용하여, 구현을 할 것입니다.


이제, 아래와 같이 장식을 구현해 볼 수 있습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
/**
 * 우유 데코레이터
 *
 * Created by Doohyun on 2017. 6. 25..
 */
public class MilkDecorator extends CondimentDecorator{
    public MilkDecorator(Coffee coffee) {
        super(coffee);
    }
    /**
     * 장식대상 객체이름을 그대로 사용.
     *
     * @return
     */
    @Override
    public String getName() {
        return getCoffee().getName();
    }
    /**
     * 우유 가격인 900 원 추가.
     *
     * @return
     */
    @Override
    public Integer getCost() {
        return getCoffee().getCost() + 900;
    }
}
 
/**
 * 라떼 데코레이터
 *
 * Created by Doohyun on 2017. 6. 25..
 */
public class LatteDecorator extends CondimentDecorator {
 
    public LatteDecorator(Coffee coffee) {
        super(coffee);
    }
 
    /**
     * 이름을 재정의.
     *
     * @return
     */
    @Override
    public String getName() {
        return "라떼";
    }
 
    /**
     * 가격은 장식 대상 객체의 기존 가격을 그대로 사용.
     *
     * @return
     */
    @Override
    public Integer getCost() {
        return getCoffee().getCost();
    }
}
cs


우유와 관련된 기능은 MilkDecorator 로 구현하여, 장식대상 객체의 가격을 900원 상승시키도록 하였습니다.


LatteDecorator 는 장식대상 객체의 명을 "라떼"로 변경하는 기능을 담당합니다. 

가격은 그대로군요.


우리는 이 장식들을 이용하여, HouseBlendingEspresso 객체 대하여 동적으로 상속받는 것과 같은 효과를 줄 수 있습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
Coffee coffee = new HouseBlendingEspresso();
System.out.println(String.format("커피명 : %s, 커피가격 : %d", coffee.getName(), coffee.getCost()));
 
// 우유 데코레이터로 치장.
coffee = new MilkDecorator(coffee);
// 라떼 데코레이터로 치장.
coffee = new LatteDecorator(coffee);
 
System.out.println(String.format("커피명 : %s, 커피가격 : %d", coffee.getName(), coffee.getCost()));
 
// CONSOLE LOG
// 커피명 : 하우스블렌딩 에스프레소, 커피가격 : 5000
// 커피명 : 라떼, 커피가격 : 5900
cs


만약 샷을 추가하는 기능을 제작한다면, 


마찬가지로 ShotDecorator 를 제작하여 치장을 해줄 수 있을 것 같습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
 * 샷을 추가하는 데코레이터
 * 
 * Created by Doohyun on 2017. 6. 25..
 */
public class ShotDecorator extends CondimentDecorator{
 
    public ShotDecorator(Coffee coffee) {
        super(coffee);
    }
 
    @Override
    public Integer getCost() {
        return getCoffee().getCost() + 500;
    }
 
    @Override
    public String getName() {
        return getCoffee().getName();
    }
}
 
Coffee coffee = new HouseBlendingEspresso();
System.out.println(String.format("커피명 : %s, 커피가격 : %d", coffee.getName(), coffee.getCost()));
 
// 우유 데코레이터로 치장.
coffee = new MilkDecorator(coffee);
// 라떼 데코레이터로 치장.
coffee = new LatteDecorator(coffee);
 
// 더블샷 데코레이터 치장.
coffee = new ShotDecorator(coffee);
coffee = new ShotDecorator(coffee);
 
System.out.println(String.format("커피명 : %s, 커피가격 : %d", coffee.getName(), coffee.getCost()));
 
// CONSOLE LOG
// 커피명 : 하우스블렌딩 에스프레소, 커피가격 : 5000
// 커피명 : 라떼, 커피가격 : 6900
cs


이러한 장식구조를 이용하여 동적으로 상속구조를 취할 수 있으며, 

자주 사용할 법한 클래스(이를테면, Americano, Latte)들은 내부에서 일정한 방식으로 장식하도록 하는 프리셋을 만들어 제공해 줄 수 있을 것 같군요.


[샷추가나 시럽추가]와 같은 추가기능에도 대응이 가능할 것 같습니다.



Decorator 패턴은 이와 같이 행위 추가를 하고자 하는 타겟 객체를 has-a 관계로 유지함으로써, 적절하게 기존 기능에 행위를 추가를 할 수 있는 패턴입니다.


이는 상속을 이용하여 메소드를 재정의하는 것과 비슷하지만, 


- 실행시간에 동적으로 상속구조를 흉내낼 수 있다는 점


- 상속구조에 따라 클래스를 생성하는 것이 아닌, 장식의 조립을 통해 객체를 생성한다는 점


복잡한 클래스의 상속구조에서 벗어나는 것에 큰 도움이 될 것입니다.


위의 예제에 대하여, 정리가 된 [실행 가능 프로젝트] 를 제공하며 이번 포스팅을 마무리합니다.

이 글이 모두에게 도움이 되길 바랍니다.


 

STUDY_OOP8.zip


반응형
Posted by N'