Effective OOP 스터디에서 첫 번째 주제는


"주어진 상황 및 용도에 따라 객체에 책임을 추가할 수 있는 Decorator(장식) Pattern" 

에 대해 알아보았습니다.


관련 내용은 아래 포스팅에서 참고. :-)

과제로 진행할 수 있는 파일까지 아래 포스팅에 제공되고 있습니다.



이 패턴은 기존 클래스의 기능 추가를 위해 is-a 관계(상속)를 하는 것 대신, has-a 관계(의존)를 취함으로써 다양하게 책임을 추가할 수 있고 또한 이 추가 작업을 실행시간에 할 수 있다는 것에 대해 알아보았습니다.

(Favor has-a over is-a, 모든 is-a 는 has-a 로 변경할 수 있습니다.)


또한 Decorator 에 대해 조금 더 알아보기 위해 해당 주제에 대한 과제가 있었고, 이번 포스팅에서는 과제에 대한 리뷰를 작성해보려 합니다.


총 세가지 정도의 과제가 있었고, 단계별로 글을 진행하고자 합니다.


1. 장식 벗기기


첫 번째 실습은 현재 제작된 장식과 장식대상 클래스 사이의 구조에서 장식된 객체의 장식을 한 개씩 제거하는 메소드(removeCondiment)를 만들어 보는 것이었습니다.


그러나 과제에서는 메소드를 제작하기 전, 사전조건이 두 가지가 있었습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// TRY 1.
 
Coffee coffee = new HouseBlendingEspresso();
coffee = new MilkDecorator(coffee);
coffee = new LatteDecorator(coffee);
 
// 장식과 관련된 기능을 장식 대상 클래스에 제공하는 것은 올바르지 않아 보입니다.
// CondimentDecorator 가 없어도 Coffee 는 독립적으로 존재해야 합니다.
coffee = coffee.removeCondiment();
 
 
// TRY 2.
 
Coffee coffee = new HouseBlendingEspresso();
coffee = new MilkDecorator(coffee);
coffee = new LatteDecorator(coffee);
 
// TRY 1 의 조건에 따라, removeCondiment 를 CondimentDecorator 로 옮겼습니다.
// 그러나, 장식을 벗길 때마다 아래처럼 캐스팅을 해야하는 중복 코드가 발생하게 됩니다.
if (coffee instanceof CondimentDecorator) {
    coffee = ((CondimentDecorator)coffee).removeCondiment();
}
cs


위의 두 문제를 해결하기 위해서는 결국 Coffee 의 타입 체크 및 캐스팅과 관련된 작업이 적절한 곳에 캡슐화되어 사용할 수 있어야하는 것 처럼 보입니다.


하지만, removeCondiment 의 위치 적용에 있어서, Coffee 와 CondimentDecorator 두 클래스 모두  애매한 위치임을 앞의 사전 조건에서 볼 수 있었습니다.


보통 이런 기능들은 저같은 경우에는 유틸(util) 기능으로 분리하는 편입니다. 장식과 관련된 기능이니 CondimentDecorator 에 해당 기능을 제작해보려합니다.


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
public abstract class CondimentDecorator extends Coffee {
 
    private Coffee coffee;
 
    /**
     * 정적메소드로 구현된, RemoveCondiment 구현.
     *
     * <pre>
     *     해당 기능에서는 어떤 객체 자신의 상태를 사용하지 않으며, 유틸성 기능을 제공하고자 합니다.
     * 제 습관 중 하나는 정적 메소드(static)의 경우 앞에 대문자를 붙이곤 합니다.
     * </pre>
     *
     * @param coffee
     * @return
     */
    public static Coffee RemoveCondiment(Coffee coffee) {
        if (coffee instanceof CondimentDecorator) {
            // 장식 객체일 경우 장식 대상 객체를 내보냅니다.
            return ((CondimentDecorator) coffee).coffee;
        } else {
            // 아닐 경우 본인을 내보냅니다.
            return coffee;
        }
    }
}
 
// 테스트 코드
Coffee coffee = new HouseBlendingEspresso();
coffee = new MilkDecorator(coffee);
coffee = new LatteDecorator(coffee);
 
// 정적메소드로 제작된, RemoveCondiment 테스트
coffee = CondimentDecorator.RemoveCondiment(coffee);
cs


유틸 기능으로 제공함으로써, 안전하게 장식을 제거할 수 있군요.

기능 역시 잘 작동하는 것처럼 보이네요.


그러나 여담으로 한 가지를 언급해보자면,

보통 장식 패턴에서는 이미 장식된 객체에서 한번 장식된 기능을 제거하기 보다는 동일하게 다시 만드는 경우가 더 많다고 합니다. ㅡㅡ^



2. 전략패턴으로 장식 구현


제게 OOP 를 가르쳐 주셨던 교수님께서는 


프로그래머는 같은 문제를 다른 방법으로도 풀 수 있어야 하며,


그 중에 가장 좋은 방법을 선택해야 한다


고 하셨습니다.


그런 의미에서, 두 번째 과제는 이미 구현된 장식 패턴의 구조를 전략 패턴으로 제작해보는 것이었습니다.


전략패턴에 대한 설명은 아래 글을 참고. @.@



이를 위해, CondimentDecorator 가 Coffee 에 의존했던 구조Coffee 가 장식 전략에 의존하도록 변경할 계획입니다.


Coffee 에는 여러 장식을 저장할 수 있도록 목록(List) 형태로 장식들을 가지고 있을 생각이며, [장식 전략 인터페이스 ICondiment] 역시 제공해보려 합니다.


일단, Coffee 내부에는 장식 전략을 의존할 수 있도록 Coffee::addCondiment 를 제공해도록 하겠습니다.


아래 코드를 참고해주세요. ^^


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
/**
 * 커피 클래스
 * @author khj1219
 *
 */
public abstract class Coffee {
 
    // 장식을 유지할 수 있는 멤버 변수
    private List<ICondiment> condimentList = new ArrayList<>();
 
    /**
     * 장식을 추가하는 메소드
     * 
     * @param condiment
     */
    public void addCondiment(ICondiment condiment){
        this.condimentList.add(condiment);
    }
 
    /**
     * 커피 가격 출력
     * 
     * 
     * @return
     */
    public Integer getCost(){
        // Coffee 의 특정 기능이 수행될 때, 장식 목록을 이용할 예정..
        for(ICondiment condiment : condimentList){
            // something work....
            // 장식의 속성은 어떻게 사용을 하나요?? @.@
        }
        
        return cost;
    }
 
}
cs



추가된 장식의 순서를 유지하고자 컨테이너로 List 를 선택하였습니다.


또한 장식들은 추가될 때마다 Coffee 클래스의 속성을 바로 변경하는 것이 아닌, 특정 기능을 수행될 때 목록을 순회하며 장식의 속성을 반영할 생각입니다.


그러나 장식 패턴의 장식들은 장식 대상 객체를 has-a 관계로 가지고 있기 때문에 이전 속성들과 현재 속성을 반영하여 결과를 출력할 수 있던 반면, 전략 장식들은 더이상 장식 대상 객체들을 가지고 있지 않습니다.


즉 이전 속성들을 알 수 없기 때문에, ICondiment 에서는 특정 기능을 수행할 때 파라미터로 Coffee 의 현재 속성을 넘기는 방식을 이용할 생각입니다.


그에 따른 인터페이스 명세는 다음과 같습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public interface ICondiment{
    
    /**
     * 커피 이름 출력
     * 
     * @param name
     * @return
     */
    String getName(String name);
    
    
    /**
     * 코스트에 대한 메소드 서명 정의 
     * 
     * @param cost
     * @return
     */
    Integer getCost(Integer cost);
    
}
cs



이를 이용하여, Coffee::getCost 를 구현을 완성해보려 합니다.


아 물론 처음에 제공을 했던 Coffee::getCost 는 추상 메소드였고, 현재는 ICondiment 목록을 이용하기 위해 정의를 할 생각입니다.


기존 Coffee::getCost 의 추상적인 역할은 Coffee::getTemplateCost 를 따로 정의함으로써, 기존 구조를 유지하고자 합니다.


그에 따라 작성된 Coffee 클래스는 다음과 같습니다.

(Coffee::getName 과 관련된 내용은 작성하지 않았습니다. 이 글을 확인하고, 한번 직접 작성해보세요.)


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
/**
 * 커피 클래스
 * 
 * @author ndh1002
 */
public abstract class Coffee {
 
    // 장식을 유지할 수 있는 멤버 변수
    private List<ICondiment> condimentList = new ArrayList<>();
 
    /**
     * 커피 가격 출력
     * 
     * @return
     */
    public Integer getCost(){
        Integer cost = coffee.getTemplateCost();
        
        for(ICondiment condiment : this.condimentList){
            cost = condiment.getCost(cost);
        }
        
        return cost;
    }
 
    /**
     * 가격에 대한 메소드 서명 정의
     *
     * <pre>
     *    기존 Coffee::getCost 의 추상적인 역할을 수행하기 위한 추상 메소드 서명 정의
     * </pre>
     * @return
     */
    public abstract Integer getTemplateCost();
}
cs


전략 패턴에 따라 구조를 변경 하였고, 그에 따른 테스트 코드는 다음과 같습니다.


모든 장식들을 전략으로 제공하지는 않고, 샘플로 ICondiment 를 구현하는 MilkDecorator 를 제공하고자 합니다.

(이 글을 보고 있는 스터디 그룹원들은 모두 잘하니, 나머지는 잘 구현할 수 있겠죠? ^^)


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
/**
 * 우유 데코레이터
 * 
 * <pre>
 * 샘플로 작성된 우유 데코레이터..
 * </pre>
 *
 * Created by Doohyun on 2017. 6. 25..
 */
public class MilkDecorator implements ICondiment {
 
    /**
     * 장식대상 객체이름을 그대로 사용.
     *
     * @return
     */
    @Override
    public String getName(String name) {
        return name;
    }
 
    /**
     * 우유 가격인 900원 추가.
     *
     * @return
     */
    @Override
    public Integer getCost(Integer cost) {
        return cost + 900;
    }
}
 
// TEST CODE
 
Coffee coffee = new HouseBlendingEspresso();
 
// 우유 -> 라떼 -> 샷 -> 샷
coffee.addCondiment(new MilkDecorator());
coffee.addCondiment(new LatteDecorator());
coffee.addCondiment(new ShotDecorator());
coffee.addCondiment(new ShotDecorator());
cs



작성된 코드는 기존 장식 패턴 구조와 비교하여 잘 작동하는 듯 합니다.


이제 한 번 고민해 볼만한 내용이 또 있습니다.

Coffee 의 각 기능에 대한 책임을 덧붙이기 위한 문제는 아래와 같이 세 가지 방법 정도를 구현해 보았습니다.


- 상속구조를 이용하는 방법


- 장식 패턴을 이용하는 방법


- 전략 패턴을 이용하는 방법


한 문제를 다양한 방법으로 생각해보았고, 어떤 방법이 가장 적절하지 한번 다시 리뷰를 해보면 좋을 것 같습니다. ^^;



3. 장식 비교


마지막 과제는 장식된 객체들 간의 비교를 구현하는 내용이었습니다.


비교에 있어서, 사전 조건은 아래와 같았습니다.


- 장식의 순서에 상관없이 동일하고, 같은 커피(예를들어 에스프레소)를 사용할 때 동일


- 자바의 객체 비교 메소드 equals 를 재정의할 것.


비교 연산를 함에 있어서, CondimentDecorator 나 Coffee 든 동일하게 비교는 가능해야 할 것 같습니다.


일단 해당 문제를 해결하기 위해서, Coffee 와 CondimentDecorator 에 각각 다른 equals 를 제작하려 합니다.


각자 구현 함으로써 Coffee 는 장식이 안된 순수 클래스, CondimentDecorator 는 장식이 된 클래스로 분류를 시킬 수가 있습니다.

이는 Coffee::equals 를 구현 시 장식에 대해 신경쓰지 않아도 되며, 오직 CondimentDecorator 에서만 장식 관련 역할을 수행하도록 구현할 수 있음을 의미합니다.

(SRP : 단일 책임 원칙)


한번 이 논리에 따라 기능을 구현해보도록 하겠습니다.


첫 번째는 Coffee::equals 구현입니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
public abstract class Coffee {
 
    /**
     * Coffee 객체의 동일 여부 판단
     * 
     * @param coffee
     * @return
     */
    @Override
    public Boolean equals(Object coffee) {
        // 장식을 하지 않은 단일 객체, 오직 본인과 동일 클래스 여부를 확인합니다.
        return this.getClass().getName().equals(coffee.getClass().getSimpleName());
    }
}
cs


Coffee::equals 의 경우 장식이 안된 순수 클래스임이 위의 논리에 따라 정해졌습니다.

오직 파라미터로 넘어온 비교 대상 객체가 자신과 동일한 클래스인지만 확인하면 될 것 같습니다.


두 번째는 CondimentDecorator::equals 의 구현입니다.

장식 클래스에서 재정의를 하기 때문에 Coffee::equals 를 따라가지 않으며, 장식의 비교만 이 곳에서 구현하면 될 것 같습니다.


CondimentDecorator 의 동일여부 판단은 사전조건에서 나타난 것처럼, 여태까지 장식된 종류 및 베이스 커피가 무엇인지 확인하는 것이 먼저일 것 같습니다.

장식된 종류 목록을 구하기 위해서는 CondimentDecorator 가 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
public abstract class CondimentDecorator extends Coffee {
 
    /**
     * Coffee 를 받아, 장식된 목록을 출력하는 메소드 정의.
     *
     * <pre>
     *     - 클래스명에 따라 정렬이 됨을 보장.
     *     - 객체의 상태를 따로 사용하지 않음으로, 정적메소드 형태로 구현.
     * </pre>
     *
     * @param coffee
     * @return
     */
    private static List<String> GetSortedDecoratorList(Coffee coffee) {
 
        ArrayList<String> decoratorList = new ArrayList<>();
 
        // 커피 목록 세팅.
        {
            Coffee targetCoffee = coffee;
 
            do {
                // 커피 이름 추가.
                decoratorList.add(targetCoffee.getClass().getSimpleName());
 
                if (targetCoffee instanceof CondimentDecorator) {
                    // 장식 커피라면, 현재 장식 대상이 된 커피를 출력.
                    targetCoffee = ((CondimentDecorator) targetCoffee).coffee;
                } else {
                    targetCoffee = null;
                }
 
            } while (targetCoffee != null);
        }
 
        // 이름에 따라 정렬을 수행.
        {
            decoratorList.sort(String::compareTo);
        }
 
        return decoratorList;
    }
}
cs


장식 목록에 정렬을 수행한 이유는 장식의 순서에 상관없이 비교를 해야하기 때문입니다.

클래스명을 오름차순으로 정렬하면, 동일한 장식이 사용된 경우 쉽게 비교를 할 수 있겠죠?



이 메소드를 이용한 CondimentDecorator::equals 의 구현은 아래와 같습니다.


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
public abstract class CondimentDecorator extends Coffee {
 
    /**     
     * Coffee 객체의 동일 여부 판단
     *
     * @param targetCoffee
     * @return
     */
    @Override
    public boolean equals(Object targetCoffee) {
        if (targetCoffee instanceof Coffee) {
 
            List<String> myDecoratorCoffeeList = GetSortedDecoratorList(this);
 
            List<String> targetDecoratorCoffeeList = GetSortedDecoratorList((Coffee) targetCoffee);
 
            // 목록이 서로 같은지 비교.
            // List 의 equals 는 내부적으로 데이터가 동일한지 비교합니다.
            // khj1219 감사합니다.
            return myDecoratorCoffeeList.equals(targetDecoratorCoffeeList);
        } else {
            // 비교 대상 객체가 커피가 아니라면, 동일하지 않다고 볼 수 있음.
            return false;
        }
    }
}
cs


커피의 장식 목록을 구할 수 있으니, 본인과 비교대상의 장식들을 조회하여 비교하는 로직입니다.

재미있는 점은 List::equals 는 내부적으로 목록간 데이터가 동일한지 이미 구현이 되어있습니다.


과제를 해준 한 스터디원이 알려줬습니다. 감사합니다. @.@

스터디 내에서는 이에 대해 불안해서 못쓰겠다고 하였지만, 잘 숙지하고 좋은 툴을 쓰는 것이 올바른 자세인 듯 합니다. 아직 옹졸한듯... ^^;


모두 구현을 하였고, 이에 대한 테스트 코드는 다음과 같습니다.


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
 Coffee coffee1 = new HouseBlendingEspresso();
 
// 우유 데코레이터로 치장.
coffee1 = new MilkDecorator(coffee1);
// 라떼 데코레이터로 치장.
coffee1 = new LatteDecorator(coffee1);
// 더블샷 데코레이터 치장.
coffee1 = new ShotDecorator(coffee1);
coffee1 = new ShotDecorator(coffee1);
        
        
Coffee coffee2 = new HouseBlendingEspresso();
 
// 더블샷 데코레이터 치장.
coffee2 = new ShotDecorator(coffee2);
// 우유 데코레이터로 치장.
coffee2 = new MilkDecorator(coffee2);
coffee2 = new ShotDecorator(coffee2);
// 라떼 데코레이터로 치장.
coffee2 = new LatteDecorator(coffee2);
 
 
 
System.out.println("coffee1과 coffee2는 같나요? " + coffee1.equals(coffee2));
 
// CONSOLE LOG
// coffee1과 coffee2는 같나요? true
cs


잘 작동하는 듯 합니다.... ㅎㅎ



장식 패턴과 관련된 과제였지만, 

조금 더 중요하게 생각했던 것은 리팩토링에 대한 내용을 다룰 때 다뤘던 것들을 해볼 수 있었던 것 같습니다.



이를테면, 아래 내용에 대해 한 번 생각해주세요.


- 정적(static) 메소드를 왜 사용했는가?


- Coffee 에 장식과 관련된 내용을 담지 않고, CondimentDecorator 로 분리한 이유.

 (CondimentDecorator::RemoveCondiment, Coffee 와 CondimentDecorator 의 다른 equals 재정의)


또한, 해당 문제 해결을 위해 주말동안 수고 많으셨습니다.

이 노력들이 꼭 도움이 되길 바랍니다. 


감사합니다. :-)

반응형
Posted by N'

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


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

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


고작, 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'