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'