안녕하세요. N` 입니당.


이번 포스팅 에서는 드디어 길고 길었던 "패턴이야기"의 마지막을 장식합니다.


마지막으로 다루는 패턴은 "컴파운드 패턴 (Compound Pattern)" 으로, 이 패턴은 여태까지 배웠던 패턴들의 종합이라 볼 수 있습니다.

 

컴파운드 패턴은 


일반적으로 자주 생길 수 있는 문제를 해결하기 위한 용도로, 

2개 이상의 패턴을 결합해서 사용하는 것


의미합니다.


즉, 이 패턴의 사용한다는 것은 여태까지 배웠던 여러 패턴들을 결합해서 특정 문제를 해결하는 것을 의미하며, 대표적으로 꽤 유명한 패턴 중 하나인 MVC(Model-View-Controller) 패턴은 이러한 컴파운드 패턴에 속합니다.


오늘 리뷰는 요구사항을 살펴보며 그에 따라 여러 패턴을 사용하게 되는 예제와 함께 MVC 패턴 내부의 컴파운드 원리를 간략하게 알아보려 합니다.

(사용하는 예제 및 자료는 제게 OOP 를 가르쳐 주셨던 한국기술교육대 김상진 교수님의 자료를 참고하였습니다.)



1. 오리꽥꽥 시뮬레이터!


요구사항은 간단합니다.

여러가지 특징을 가진 오리들을 제작하고, 오리들이 꽥꽥하며 우는 시뮬레이터 하는 것이죠.


여러 오리를 제작하기 위해, 첫 번째로 설계한 UML 은 아래와 같습니다. (코드 대신 UML 로 대치...)

 

 

 

이 UML 에 따라 제작된 클래스를 시뮬레이션 하는 프로그램을 저는 다음과 같이 제작하였습니다.


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
/**
 * 오리 시뮬레이터 정의.
 *
 * Created by Doohyun on 2017. 11. 19..
 */
public class DuckSimulator {
 
    /**
     * 테스트 클래스 제작.
     *
     * @param args
     */
    public static void main(String[] args) {
        DuckSimulator duckSimulator = new DuckSimulator();
        duckSimulator.simulate();
    }
 
    /**
     * 시뮬레이터 작성.
     *
     * <pre>
     *     이 곳에 비지니스 시뮬레이션 기능을 넣을 것!
     * </pre>
     */
    public void simulate(){
        Duck rubberDuck = new RubberDuck();
        simulate(rubberDuck);
    }
 
    /**
     * 오리를 넣어 시뮬레이션.
     *
     * @param duck
     */
    private void simulate(Duck duck){
        duck.quack();
    }
}
 
// CONSOLE RESULT
// 삐이익~~ 
cs



2. 오리가 아닌 다른 동물을 테스트!


테스트 클래스는 현재 잘 작동하고 있습니다만, 오리와 비슷한 성격을 가진 거위 클래스를 이 테스트에 포함하고 싶습니다.


거위 클래스 명세는 다음과 같습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
 * 거위 클래스
 *
 * Created by Doohyun on 2017. 11. 19..
 */
public class Goose {
 
    /**
     * 거위의 울음.
     */
    public void honk() {
        System.out.println("Honk");
    }
}
cs



해당 테스트 클래스에서 거위를 사용하려면, 오리와 호환을 맞춰야할 것으로 보입니다. 

좋은 방법이 있을까요? 여러분은 알고 있을 것이라 생각합니다. 


아래 패턴을 기억해 보죠.



제작한 거위 어댑터는 아래와 같습니다.


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
/**
 * 거위를 오리로 호환시켜주는 클래스
 *
 * Created by Doohyun on 2017. 11. 19..
 */
public class GooseAdapter extends Duck {
 
    private Goose goose;
 
    public GooseAdapter(Goose goose) {
        this.goose = goose;
    }
 
    @Override
    public void quack() {
        goose.honk();
    }
}
 
// TEST CLASS
public class DuckSimulator {
    /**
     * 시뮬레이터 작성.
     *
     * <pre>
     *     이 곳에 비지니스 시뮬레이션 기능을 넣을 것!
     * </pre>
     */
    public void simulate(){
        // 거위를 오리로 호환하여, 테스트
        Duck goose = new GooseAdapter(new Goose());
        simulate(goose);
    }
}
 
// CONSOLE RESULT
// Honk
cs



3. 오리들의 꽥꽥한 횟수 구하기!


추가된 요구사항은 시뮬레이션을 한번 할 때, 전체 오리들이 꽥꽥한 횟수를 구해야하는 것입니다. 

이를 구현하기 위한 방법으로 static 변수를 이용하고자 합니다.


구현 알고리즘으로 떠오르는 것은 quack 메소드가 호출될 때마다 해당 변수를 증가시키면 되지만, 이를 위해서는 Duck 을 구현한 모든 클래스를 손봐야하는 것으로 보입니다. (많이 고쳐야 함.. 짜증...)

(Duck::quack 자체를 추상 메소드에서 일반 메소드로 변경하고 카운트를 증가시켜도 되지만, 이를 위해서는 상속받은 모든 오리클래스들이 quack 을 재정의할 때 super.quack 을 반드시 사용하도록 모두 고쳐야 합니다.)


이를 위해서 제안하는 방법은 꽥꽥한 횟수만을 관리하는 클래스를 제공하는 것을 목표로 합니다.

요구사항인 Duck::quack 을 요청했을 때 카운트가 증가해야하는 재정의가 필요하며, 이는 편의에 맞게 상속을 우회하는 패턴을 생각해 볼 수 있습니다.


우리는 이런 방법론을 배운적이 있습니다.



그렇습니다. 횟수를 관리하는 장식자를 만들 것입니다.

저는 아래와 같이 장식자를 제작하였고, 그에 따라 시뮬레이터 역시 변경하였습니다.


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
/**
 * 꽥꽥한 횟수를 관리하는 장식자
 *
 * Created by Doohyun on 2017. 11. 19..
 */
public class QuackCountDecorator extends Duck{
    private Duck duck;
 
    private static int QUACK_COUNT = 0;
 
    public QuackCountDecorator(Duck duck) {
        this.duck = duck;
    }
 
    @Override
    public void quack() {
        // 오리가 울 때마다, 카운트를 관리하도록 처리.
        ++QUACK_COUNT;
 
        duck.quack();
    }
 
    /**
     * 오리가 꽥꽥한 횟수 출력.
     *
     * @return
     */
    public static int GetQuackCount() {
        return QUACK_COUNT;
    }
}
 
/**
 * 오리 시뮬레이터 정의.
 *
 * Created by Doohyun on 2017. 11. 19..
 */
public class DuckSimulator {
 
     /**
     * 시뮬레이터 작성.
     *
     * <pre>
     *     이 곳에 비지니스 시뮬레이션 기능을 넣을 것!
     * </pre>
     */
    public void simulate(){
 
        // 고무오리 테스트.
        {
            Duck rubberDuck = new QuackCountDecorator(new RubberDuck());
            simulate(rubberDuck);
        }
 
        // 거위를 오리로 호환하여, 테스트
        {
            // 장식자로 치장.
            Duck goose = new QuackCountDecorator(new GooseAdapter(new Goose()));
            simulate(goose);
        }
 
        System.out.println(String.format("꽥꽥 횟수 출력 : %d회", QuackCountDecorator.GetQuackCount()));
    }
}
 
// CONSOLE RESULT
// 삐이익~~
// Honk
// 꽥꽥 횟수 출력 : 2회
 
cs



4. 장식자 생성의 불편함, 이를 위한 객체 생성의 캡슐화!


장식자를 이용하여, 꽥꽥 횟수에 대한 관심을 한 곳으로 몰아 버린 것은 적절한 선택인 듯 합니다.


하지만, 그에 대한 비용으로 불편한 API 사용이란 대가를 치뤄야 합니다.

대표적으로 주목할 코드는 아래의 어댑터와 장식자를 같이 사용한 사례입니다.


1
 Duck goose = new QuackCountDecorator(new GooseAdapter(new Goose()));
cs


카운트를 모든 오리에 대해서 관리해야 한다면, 매번 위와 같이 장식자를 붙여줘야 할 것으로 보이는데요. 

차라리, 해당 방식으로 객체를 생성해야할 일이 많다면 객체생성 방법 자체를 캡슐화하는 것도 방법으로 보입니다.


특정 목적에 맞게 객체를 생산해주는 클래스를 팩토리라 하며, 카운트에 대한 관심을 지원할 수 있는 팩토리 클래스를 제작해 봅시다.

(이에 대한 자세한 내용은 아래에서 참고!)




이 예제에서는 여러 타입의 객체를 목적에 맞게 생산해주는 추상 팩토리(Abstract Factory) 를 사용하였으며,

그에 대한 코드는 아래와 같습니다.


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
/**
 * 카운트 뿐만이 아닌 특정 관심사에 따라 팩토리를 생성할 수 있음으로, 추상 팩토리를 제작.
 *
 * Created by Doohyun on 2017. 11. 19..
 */
public abstract class AbstractDuckFactory {
 
    /**
     * 고무오리 생산.
     *
     * @return
     */
    public abstract Duck createRubberDuck();
 
    /**
     * 거위 생산.
     *
     * @return
     */
    public abstract Duck createGoose();
}
 
/**
 * 꽥꽥 카운팅 데코레이터를 장식한 오리 객체를 생산하는 팩토리.
 *
 * Created by Doohyun on 2017. 11. 19..
 */
public class CountingDuckFactory extends AbstractDuckFactory{
 
    private static CountingDuckFactory INSTANCE = new CountingDuckFactory();
 
    private CountingDuckFactory(){}
 
    /**
     * 싱글톤으로 제작.
     *
     * <pre>
     *     팩토리는 의미적으로 한개여도 충분해 보임.
     * </pre>
     *
     * @return
     */
    public static CountingDuckFactory GetInstance() {
        return INSTANCE;
    }
 
    /**
     * 고무오리 생산.
     *
     * @return
     */
    @Override
    public Duck createRubberDuck() {
        // QuackCountDecorator 으로 장식하였음!
        return new QuackCountDecorator(new RubberDuck());
    }
 
    /**
     * 거위 생산.
     *
     * @return
     */
    @Override
    public Duck createGoose() {
        return new QuackCountDecorator(new GooseAdapter(new Goose()));
    }
}
 
cs


현재까지의 요구사항에서는 AbstractDuckFactory 를 굳이 제작할 필요는 없어보이지만,

추 후 오리들에 대한 새로운 관심사가 생길 경우 그에 맞는 적절한 객체를 생성할 수 있도록 추상 클래스를 제작했습니다.


또한 구현된 CountingDuckFactory 는 현재 의미적으로 한개여도 충분해 보인다는 생각이 들며, 그에 따라 싱글톤 클래스로 제작하였습니다.



팩토리를 제작하였으니, 그에 따라 테스트 클래스는 다음과 같이 편리하게 고칠 수 있을 것 같네요..


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
/**
 * 오리 시뮬레이터 정의.
 *
 * Created by Doohyun on 2017. 11. 19..
 */
public class DuckSimulator {
 
    /**
     * 시뮬레이터 작성.
     *
     * <pre>
     *     이 곳에 비지니스 시뮬레이션 기능을 넣을 것!
     * </pre>
     */
    public void simulate(){
 
        // 고무오리 테스트.
        {
            // 팩토리로 객체 생성을 캡슐화
            Duck rubberDuck = CountingDuckFactory.GetInstance().createRubberDuck();
            simulate(rubberDuck);
        }
 
        // 거위를 오리로 호환하여, 테스트
        {
            // 팩토리로 객체 생성을 캡슐화
            Duck goose = CountingDuckFactory.GetInstance().createGoose();
            simulate(goose);
        }
 
        System.out.println(String.format("꽥꽥 횟수 출력 : %d회", QuackCountDecorator.GetQuackCount()));
    }
}
cs



5. 오리떼 관찰!


현재 제작된 오리 시뮬레이터에서는 각각의 오리객체가 꽥꽥하도록 정의되어 있습니다.

하지만 오리 객체를 더 많이 테스트 하고 싶다면, 팩토리에서 객체를 생성하고 DuckSimulator::simulate 메소드를 매번 호출해야 합니다.

(즉 여러 오리 객체에 대한 관리가 불편합니다. ㅡㅡ^)


만약 테스트의 요구사항이 지금과 같이 여러 오리들을 순회하며 Duck::quack 을 실행하는 것이 목적이라면, 아래 패턴을 이용해 볼 수 있을 것 같습니다.



지금부터 오리떼(Flock)를 나타낼 수 있는 클래스의 제작을 목표로 하며, DuckSimulator::simulate 를 사용할 수 있도록 하려 합니다.

이 때 오리들을 순회하면서 동작하고, 기존 Duck 의 인터페이스를 그대로 계승하도록 해야할 것 입니다.


즉, 오리들을 그룹으로 묶은 복합계층(Composite)을 만들고 단일계층(RubberDuck..)과 동일한 인터페이스를 제공할 생각입니다.


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
/**
 * 오리떼 클래스.
 *
 * <pre>
 *     오리떼라는 복합계층을 제작하며, 단일계층과 동일한 취급을 할 수 있게 해줌.
 *     Composite Pattern
 * </pre>
 *
 * Created by Doohyun on 2017. 11. 19..
 */
public class Flock extends Duck{
 
    private List<Duck> duckList = new ArrayList<>();
 
    public void addDuck(Duck duck) {
        duckList.add(duck);
    }
 
    @Override
    public void quack() {
        // 순회하면서, Duck::quack 기능을 수행.
        // 반복자 패턴(Iterator)
        duckList.forEach(Duck::quack);
    }
}
 
 
/**
 * 오리 시뮬레이터 정의.
 *
 * Created by Doohyun on 2017. 11. 19..
 */
public class DuckSimulator {
 
     /**
     * 시뮬레이터 작성.
     *
     * <pre>
     *     이 곳에 비지니스 시뮬레이션 기능을 넣을 것!
     * </pre>
     */
    public void simulate(){
 
        // 오리떼 생성.
        Flock flock = new Flock();
 
        // 고무오리 추가.
        flock.addDuck(CountingDuckFactory.GetInstance().createRubberDuck());
 
        // 거위 추가
        flock.addDuck(CountingDuckFactory.GetInstance().createGoose());
 
        // 오리떼 테스트.
        simulate(flock);
    }
}
 
cs



여기까지, 오리 시뮬레이터의 요구사항을 일단 마치려고 합니다.

이 곳에서 사용된 패턴은 Adapter, Decorator, Factory, Composite, Iterator 패턴으로 오리 시뮬레이터를 제작하기 위해 연속적으로 패턴이 사용되었습니다.

위의 언급된 예제에서는 일련의 패턴들이 어떻게 연속적으로 사용될 수 있는지 볼 수 있습니다. 


하지만, 이 것이 컴파운드-패턴은 아닙니다. 

여러 패턴을 동시에 사용하였다고 컴파운드-패턴은 아니고, 여러 패턴이 결합되어 특정 용도로 사용되는 것을 말하기 때문이죠.


지금부터는 대표적인 컴파운드-패턴인 MVC 를 살짝 보며, MVC 가 어떤 용도로 여러가지 패턴들을 사용하고 있는지 볼 것입니다.



6. MVC


MVC 는 Model-View-Controller 의 약자로 Model(데이터 및 비지니스 문제)과 View(화면) 사이에 제어하는 층(Controller)를 두는 것을 목표로 합니다.

 


세 가지의 각 컴포넌트는 다음과 같은 역할을 하고 있습니다.


- Model 


모든 데이터 및 비지니스 로직들을 포함합니다. 또한 View 와 Controller 에게 본인의 상태(데이터)를 제공할 수 있는 인터페이스 및 상태 변경 이벤트를 제공하지만, 보통은 다른 두 컴포넌트들에게 별 관심은 없습니다.


- View 


Model 을 표현할 수 있는 기능을 담당합니다. 

데이터를 표현하기 위해 Model 로 부터 상태를 가져오도록 하며, 사용자의 입력을 Controller 로 전달하곤 합니다.


- Controller


View 로부터 사용자의 입력 이벤트를 인지하며, Model 의 상태변경을 요청하거나 View 에게 표시변경을 요청합니다.



이 세 가지 컴포넌트를 패턴 관점에서 보면, 보편적으로 다음과 같이 나누곤 합니다.

 


MVC 의 목적은 Model 과 View 를 분리하는 것으로 이는 꽤 중요한 이슈입니다. 

이 원칙을 지킬 시, 이를테면 아래와 같은 이득을 볼 수 있습니다.


비지니스적인 문제는 일관될 수 있지만, 이를 표현하는 방법은 다양해질 수 있습니다. 

  CUI -> GUI 로 변경 시, 비지니스 문제를 변경할 필요가 있을지 고민해봐야 합니다. 

  혹은 일관된 데이터로 여러 뷰를 표현해야 할 수도 있죠.


표현하기 위한 방법이 분리되어 있다면, 

  비지니스 문제가 변경되도 표현방법만 알맞게 호환해주면 원활하게 개발할 수 있습니다.



이 것으로, Effective OOP 카테고리도 마무리입니다. 

올해부터 시작한 OOP 에 대한 정리가 끝나게 되니, 정말 감회가 새로운 것 같네요. :-) 


여기까지 정리를 할 수 있었고, 공부를 할 수 있게 해준 것은 꾸준히 제 블로그를 봐주고 스터디에 적극적으로 참여해주신 구성원분들 덕이라고 생각을 합니다.

(잊지 못할 것입니다. ㅠㅡㅠ)


또한, 이러한 기반 지식 및 자료를 제게 알려주신 교수님께도 감사합니다. ㅎㅎ


여기까지 공부를 마쳤다면, 저를 포함한 이 글을 읽는 모든 분들은 이 책을 한 권 마스터한 것입니다.

나름, 수준있는 예제를 들고왔기 때문에 이 책을 보며, 블로그를 다시 읽어주신다면 정말로 도움이 되지 않을까 생각이 드네요. 

(여러분들도 이제 패턴병에 걸릴 시간입니다. 히히히)


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



조금 부족한 면이 없는 글들이긴 하지만, 

이 글들을 읽은 모든 분들이 정말 멋있고 훌륭한 개발자가 되는 것에 조금이라도 도움이 되길 바랍니다. ^^







반응형
Posted by N'

안녕하세요! 

필명이 "성숙한 개발을 지향하는" 이라는 타이틀이 붙은 Ndroid 입니다. @.@


바로 패턴에 대한 리뷰를 하지 않고, 인사글을 쓰는 이유는 바로 아래 사진 때문입니다.



네, 맞습니다. 이번 글이 100회 기념 포스팅 기 때문이죠. :-)


하지만, 기념 포스팅 이라고 특별하진 않습니다.

지인 기준으로 이름과 댓글을 작성해주면, 커피를 사주는 정도까지만... ^^;

(101 번째 게시글이 작성 되었으니, 이벤트는 종료. @.@)


또한, 100회이기 때문에 조금 더 정성들여 작성하고자 합니다.

(그렇다고, 여태까지 쓴 글에 정성이 없는 것은 아닙니다. ㅡㅡ^)


이제, 본론으로...

이번 포스팅의 주제는 Too much complexity(매우 복잡)한 클래스를 리팩토링할 때 사용하기 괜찮은 방문자(Visitor) 패턴에 대해 알아보려 합니다.


방문자 패턴은 복잡한 데이터를 유지하는 클래스에 다양한 연산과정이 추가되어 클래스가 복잡해지는 것을 방지하는 패턴으로, 일련의 알고리즘과 객체 구조를 분리하는 것을 목표로 합니다.

방문자라는 이름이 붙은 이유는 [알고리즘을 구현한 객체]가 [복잡한 데이터들]을 방문하며 처리하도록 모델링 되었기 때문입니다.


사실, 이 패턴은 [OOP 스터디] 첫 장에서 다뤄봤습니다.



한 번 과제로 진행했던 내용을 다시 한번 리마인드 하며, 아래 내용들을 보면 더 좋을 것 같습니다.


이번 리뷰에서는 대표적으로 알려진 방문자 패턴의 예제와 제가 조금 변형한 N식 예제를 작성해보려 합니다.



1. 복잡한 모델과 행위들, 결국은 복잡한 클래스로...


클래스의 존재 목적은 데이터와 데이터를 이용한 행위의 결합이라 할 수 있습니다.


그렇기 때문에, 이제까지 진행했던 OOP 의 많은 포스팅에서는 SRP(단일 책임 원칙)를 강조하며 클래스들이 각자의 역할에 충실해야한다고 많이 언급 했었습니다.

특히 리팩토링을 다룬 포스팅에서는 메소드 추출, 메소드 이동 등 불필요한 책임을 가진 클래스 수정 내용까지 다뤄본 적까지 있습니다.



이와 관련된 많은 내용에 따라 결론을 조금 내본다면, 클래스 내부의 데이터(멤버변수, 속성)를 이용하는 행위는 대부분 해당 클래스의 메소드로써 존재해야할 것입니다.


아래 예제로 작성된 클래스 처럼 말이죠..


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
/**
 * 계정 클래스 정의
 *
 * Created by ndh1002 on 2017. 9. 10..
 */
public class Account {
 
    // 계정정보
    private String email;
    private String password;
 
    // 회원 기본정보.
    private String name;
    private Date birthDay;
    private String genderFlag;
 
    // 회원 연락처.
    private String mobile;
    private String mobileCompany;
    private String address;
 
    /**
     * 현재 입력된 계정정보의 유효여부를 출력.
     *
     * <pre>
     *     Account 의 모든 멤버변수의 유효여부를 검사.
     * </pre>
     *
     * @return
     */
    public Boolean checkValid() {
        // 유효여부 결과.
        // 각 유효성 체크에 따라 유효여부를 세팅하는 알고리즘을 수행.
        Boolean validYn = true;
 
        // 이메일 유효성 검사.
        {
        }
 
        // 비밀번호 유효성 검사.
        {
        }
 
        // 이름 유효성 검사.
        {
        }
 
        // 생년월일 유효성 검사.
        {
        }
 
        // 이하 모든 정보 유효성 검사.
 
        return validYn;
    }
 
    // SETTER, GETTER 는 생략.
}
 
cs


Account 클래스는 계정에 대한 정보를 가진 클래스로, Account::checkValid 라는 메소드를 가지고 있습니다.

(아마도, 회원가입과 같은 비지니스 문제에서 사용하던 클래스로 보입니다. @.@)


Account::checkValid 에서는 순차적으로 Account 클래스의 멤버변수의 유효여부를 체크하고 있습니다.

해당 메소드가 Account 클래스의 모든 멤버변수를 검사하는 로직이라 가정할 때, 이 메소드의 복잡도는 Account 가 소유하는 데이터만큼의 책임을 가지며 그 책임만큼 수정, 변화가 일어날 것입니다.


뭐, 좋습니다. 잘 작동만 하면, 일단 넘어갈 수 있는 문제입니다.

그러나 언제나 요구사항은 계속 추가되는 법! Account 에 대하여, 아래와 같은 추가 기능(checkValidType2) 이 요청되었습니다.


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
/**
 * 계정 클래스 정의
 *
 * Created by ndh1002 on 2017. 9. 10..
 */
public class Account {
 
    /**    
     * 현재 입력된 계정정보의 유효여부를 출력.
     *
     * <pre>
     *     Account 의 모든 멤버변수의 유효여부를 검사.
     * </pre>
     *
     * @return
     */
    public Boolean checkValid() {
        ...
    }
 
    /**
     * 이름, 생년월일, 연락처 정보의 유효성만 체크하는 메소드.
     *
     * <pre>
     *     각 필드의 유효성 검사 알고리즘은 Account::checkValid 와 동일.
     * </pre>
     *
     * @return
     */
    public Boolean checkValidType2() {
 
        // 유효여부 결과.
        // 각 유효성 체크에 따라 유효여부를 세팅하는 알고리즘을 수행.
        Boolean validYn = true;
 
        // 이름 유효성 검사.
        {
        }
 
        // 생년월일 유효성 검사.
        {
        }
 
        // 연락처 정보 유효성 검사.
        {
        }
 
        return validYn;
    }
    // SETTER, GETTER 는 생략.
}
 
cs


새로운 요구사항은 부분적으로 Account 의 정보를 유효성 검사하는 로직입니다.

기본적으로 Account::checkValid 의 알고리즘을 이용한다고 하였기 때문에, Account::checkValid 에서 특정로직을 메소드 추출 해야할 것 같습니다.


메소드 추출 리팩토링 후, 클래스의 상태는 아래와 같습니다.


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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
public class Account {
    /**
     * 현재 입력된 계정정보의 유효여부를 출력.
     *
     * <pre>
     *     Account 의 모든 멤버변수의 유효여부를 검사.
     * </pre>
     *
     * @return
     */
    public Boolean checkValid() {
        // 유효여부 결과.
        // 각 유효성 체크에 따라 유효여부를 세팅하는 알고리즘을 수행.
        Boolean validYn = true;
 
        // 기타 검사 생략
 
        // 이름 유효성 검사.
        checkValidByName();
 
        // 생년월일 유효성 검사.
        checkValidByBirthDay();
 
        // 연락처 정보 유효성 검사.
        {
            // 휴대전화번호 검사.
            checkValidByMobile();
            // 통신사정보 유효성 검사.
            checkValidByMobileCompany();
            // 주소 유효성 검사.
            checkValidByAddress();
        }
 
        // 이후 검사 생략.
    }
 
    /**
     * 이름, 생년월일, 연락처 정보의 유효성만 체크하는 메소드.
     *
     * <pre>
     *     각 필드의 유효성 검사 알고리즘은 Account::checkValid 와 동일.
     * </pre>
     *
     * @return
     */
    public Boolean checkValidType2() {
 
        // 유효여부 결과.
        // 각 유효성 체크에 따라 유효여부를 세팅하는 알고리즘을 수행.
        Boolean validYn = true;
 
        // 이름 유효성 검사.
        checkValidByName();
 
        // 생년월일 유효성 검사.
        checkValidByBirthDay();
 
        // 연락처 정보 유효성 검사.
        {
            // 휴대전화번호 검사.
            checkValidByMobile();
            // 통신사정보 유효성 검사.
            checkValidByMobileCompany();
            // 주소 유효성 검사.
            checkValidByAddress();
        }
 
 
        return validYn;
    }
 
    /**
     * 이름 유효성 검사 처리.
     *
     * @return
     */
    public Boolean checkValidByName() {
 
    }
 
    /**
     * 생년월일 유효성 검사 처리.
     *
     * @return
     */
    public Boolean checkValidByBirthDay() {
 
    }
 
    // SETTER, GETTER 는 생략.
}
 
cs


Account::checkValidByName, Account::checkValidByBirthDay 등 Account 클래스 내부 멤버 변수 한 개 단위로 유효성 검사하는 메소드를 제공하며, 기존 Account::checkValid, Account::checkValidType2 등이 해당 메소드를 사용하도록 하고 있습니다.


많은 OOP 관련 내용에서 하라는 것과 같이 너무 많은 책임을 가진 메소드에 대하여 책임을 분리하였고, 그에 따라 특정 로직 수정에 있어서도 유지보수에 좋을 것이라 생각합니다. 


그러나 한 단위로 유효성 검사하는 메소드를 모두 제작해버리면, 클래스의 멤버변수만큼 메소드가 늘어날 것입니다.


그래요. 그것까지는 OK! 

그러나, 멤버변수들을 가지고 유효성 검사를 하는 것이 아닌 기타 다른 행위1, 행위2 등을 작성해야 한다면 어떻게 하죠?


결국 이 룰에 따라 한번 Account 클래스의 메소드 개수를 어림짐작해보죠..

최소 다음과 같습니다.


Account 클래스의 멤버변수 X Account 가 제공해야하는 기능 = Account 가 가질 최소 메소드 개수.


무언가 잘못되었습니다.. ㅜㅡㅜ



2. Single Dispatch vs Double Dispatch (방문자를 이용한 리팩토링)


앞서, 살펴본 예제에서 딱히 잘못된 것을 느끼지는 못하겠습니다.

사실 잘못된 것이 아닐 수도 있습니다. 심리적으로 많은 메소드를 가지는 것이 불편한 것이죠.... @.@


한번 알고있는 OOP rule 에 따라 고려해봐도, SRP 및 기타 리팩토링 지식을 철저하게 지킨 것 밖에 없습니다.

원칙에 따라 해본 것은 아래정도 될 것 같습니다.


- 클래스가 멤버변수를 가지고 있으니, 멤버변수를 이용한 행위는 해당 클래스에 작성합니다.


- 특정 메소드가 너무 많은 책임을 가지고 있으니, 메소드 추출을 하였습니다.


- 중복된 로직을 캡슐화하여, 재활용하고 있습니다.


그러나, 결과적으로는 많은 메소드들을 가진 복잡하다고 생각할 수 있는 클래스를 제작하게 되었습니다.

이와 같이 한 클래스에 모든 책임을 구현하는 방법을 Single Dispatch 고 하며, 대부분 로직은 이와 같이 구현이 됩니다.


여기에서 Dispatch 란, 메소드를 호출하기 위해 하는 일련의 과정을 말합니다.

조금 더 이 개념을 살펴보면, 컴파일 시점부터 어떤 메소드가 호출될지 정해진 적 디스패치(Static Dispatch)인터페이스의 참조에 따라 어떤 메소드가 호출될지 정해지는 동적 디스패치(Dynamic Dispatch) 존재합니다.


한 클래스에서 모든 책임을 구현하게 된다면, 보통은 정적 혹은 동적 디스패치단일적으로 일어나게 될 것입니다.

(Dispatch 가 한번만 일어나기 때문에, Single Dispatch 라 부릅니다. @.@)


갑자기 Single Dispatch 를 언급한 것은 Double Dispatch 역시 존재하기 때문입니다.

Double Dispatch 는 메소드를 호출하기 위한 행위가 두번 일어나는 것을 말하며,  오늘 포스팅에서는 Double Dispatch 를 이용하는 방문자 패턴으로 복잡한 클래스를 리팩토링해볼 것입니다.


리팩토링의 목표는 모델의 데이터 구조에 따라 제공해야하는 기능 중 한 가지를 모두 구현하는 클래스를 제작할 것이며, 이를 묶어주는 인터페이스 한 개를 만들 것입니다.


일단, 첫 번째로 만들어볼 것은 데이터 구조에 따라 기능을 구현한 클래스입니다.

여기에서 기능을 구현하는 클래스를 방문자(Visitor)로 칭하겠습니다.


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 ndh1002 on 2017. 9. 11..
 */
public class CheckVisitor {
 
    /**
     * 이름에 대한 유효성 검사를 수행하는 메소드.
     *
     * @param name
     */
    public void check(Name name) {
        System.out.println("이름에 대한 유효성 검사.");
    }
 
    /**
     * 생년월일에 대한 유효성 검사를 수행하는 메소드.
     *
     * @param birthDay
     */
    public void check(BirthDay birthDay) {
        System.out.println("생일에 대한 유효성 검사.");
    }
    
    // 기타 다른 모델들은 생략...
}
 
cs


오버로딩을 통해, 각 데이터 모델에 따라 유효성 검사를 수행하는 클래스입니다.

즉, 이곳에서는 기존 Account 에 있어야 했던 데이터 단위의 유효성 검사로직이 존재합니다.



이제 이를 묶어줄 인터페이스 한 가지를 제작할 생각입니다. 

해당 인터페이스는 방금 작성한 CheckVisitor 를 파라미터로 받으며, 그에 따른 각 구현 클래스들은 자기자신을 넘김으로 CheckVisitor 의 어떤 check 메소드가 실행할지 결정하도록 할 것입니다.


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
/**
 * 유효성 검사 인터페이스.
 *
 * Created by Doohyun on 2017. 9. 12..
 */
public interface ICheckAble {
    void check(CheckVisitor visitor);
}
 
/**
 * 이름을 저장하는 클래스.
 *
 * Created by Doohyun on 2017. 9. 17..
 */
public class Name implements ICheckAble {
 
    @Override
    public void check(CheckVisitor visitor) {
        visitor.check(this);
    }
}
 
/**
 * 생년월일에 대한 모델.
 *
 * Created by Doohyun on 2017. 9. 17..
 */
public class BirthDay implements ICheckAble {
 
    @Override
    public void check(CheckVisitor visitor) {
        visitor.check(this);
    }
}
cs


이를 이용해, 새롭게 구현된 Account::checkValidType2 는 아래와 같습니다.

기타 다른 모델 클래스들은 살짝 생략했습니다. @.@


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
public class Account {
 
    private Name name = new Name();
    private BirthDay birthDay = new BirthDay();
    private Mobile mobile = new Mobile();
    private MobileCompany mobileCompany = new MobileCompany;
 
    /**
     * 이름, 생년월일, 연락처 정보의 유효성만 체크하는 메소드.
     *
     * <pre>
     *     각 필드의 유효성 검사 알고리즘은 Account::checkValid 와 동일.
     * </pre>
     *
     * @return
     */
    public Boolean checkValidType2() {
 
        CheckVisitor checkVisitor = new CheckVisitor();
 
        // 유효성 검사를 수행할 객체를 방문자가 순회하며, 알고리즘 수행.
        for (ICheckAble checkAble : Arrays.asList(name, birthDay, mobileCompany, mobile)) {
            checkAble.check(checkVisitor);
        }
 
 
        return false;
    }
 
 
    // SETTER, GETTER 는 생략.
}
cs


사용 로직을 살펴보면, 원하는 모델에 따라 목록을 만들어 유효성 검사를 체크하는 방문자를 사용하도록 하고 있습니다.


Account 를 복잡하게 만들었던 알고리즘들을 특정 클래스로 분리해냈고, 추 후 Account 에 또 다른 기능이 추가된다면 방문자 클래스를 만드는 것으로 더 복잡하지 않게 만들 수 있을 것 같습니다.

즉, Account 에는 데이터들과 방문자를 사용하는 메소드만 남게 될 것입니다.


이와 같은 방법을 앞써 언급한 Double Dispatch 와 연관지어 보지 않을 수가 없을 것 같습니다. @.@

로직에서는 ICheckAble::check 를 사용하기 위해 다형성에 의한 동적 디스패치가 한 번 일어났고, 실제 CheckVisitor::check 에서 오버로드된 메소드들 중 어떤 메소드를 사용할 것인지 결정하는 동적 디스패치가 두 번째로 일어납니다.


결과론적으로 복잡한 모델에 대하여, 데이터 구조와 알고리즘을 분리해냄으로 클래스가 더 복잡해지는 것을 방지한 듯 합니다.



3. 제안하는 새로운 방문자


Double Dispatch 를 이용한 방문자 패턴을 통해 클래스를 복잡하게 만드는 것을 방지한 것은 괜찮은 아이디어인 듯 합니다.


하지만, 위에 제시된 방법은 단순하게 원시타입으로 가지고 있어도 되는 필드를 방문자를 사용해야하는 이유로 객체화시키는 것은 매우 불편해 보입니다.


저는 이 점에 착안하여, 새로운 발상을 하게 되었습니다.

기존의 방문자 패턴은 모델이 알고리즘 방문자를 사용하는 방식이라 한다면, 반대로 알고리즘이 모델을 사용하는 방식으로 변경하면 어떨지에 대해 고민했습니다.

알고리즘이 모델로부터 알아서 사용할 데이터를 PULL 방식으로 가져와 수행하는 것이죠..


이를테면, 위의 예제를 아래와 같은 열거형 처럼 변경할 수 있을 듯 합니다.


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
/**
 * 계정의 유효성 정보 체커
 * 
 * <pre>
 *     전략패턴과 유사한 형태의 알고리즘 구현체.
 * </pre>
 *
 * Created by ndh1002 on 2017. 9. 17..
 */
public enum AccountChecker {
 
    NAME {
        @Override
        public void check(Account account) {
            System.out.println("이름에 대한 유효성 검사." + account.getName());
        }
    },
 
    BIRTHDAY {
        @Override
        public void check(Account account) {
            System.out.println("생일에 대한 유효성 검사." + account.getBirthday());
        }
    }
 
    // 이하 생략.
    ;
 
    public abstract void check(Account account);
}
 
cs


기존의 CheckVisitor 처럼 Account 에 존재하던 단일 개체에 대한 유효성 검사 로직을 해당 클래스에 구현하였습니다.


이런 형태의 열거형 사용 형태는 많이 익숙하지 않나요?

네, 이 것은 바로 전략패턴입니다.



이 전략 패턴을 이용하여, 다음과 같이 또 다른 형태의 Account::checkValidType2 를 만들어 볼 수 있을 것 같습니다.


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
/**
 * 계정 클래스 정의
 *
 * Created by ndh1002 on 2017. 9. 10..
 */
public class Account {
    // 회원 기본정보.
    private String name;
    private String birthDay;
 
    // 회원 연락처.
    private String mobile;
    private String mobileCompany;
    private String address;
 
    /**
     * 이름, 생년월일, 연락처 정보의 유효성만 체크하는 메소드.
     *
     * <pre>
     *     각 필드의 유효성 검사 알고리즘은 Account::checkValid 와 동일.
     * </pre>
     *
     * @return
     */
    public Boolean checkValidType2() {
 
        CheckVisitor checkVisitor = new CheckVisitor();
 
        // 유효성 검사를 수행할 객체를 방문자가 순회하며, 알고리즘 수행.
        for (AccountChecker checker : Arrays.asList(
                                            AccountChecker.NAME
                                            , AccountChecker.BIRTHDAY
                                            , AccountChecker.MOBILE_COMPANY
                                            , AccountChecker.MOBILE)) {
            checker.check(this);
        }
 
 
        return false;
    }
 
 
    // SETTER, GETTER 는 생략.
}
 
cs


이 방식은 Double Dispatch 활용을 위해 단일 개체 필드에 대한 모델을 제작할 필요가 없으며, 방문자 패턴의 목적처럼 데이터 구조와 알고리즘을 분리해낼 수도 있습니다.


또한 새로운 기능이 필요하다면, 또 다른 열거형을 제작하면 해결될 일이죠..


이 방식이 제가 새롭게 제안하는 N식 방문자 패턴입니다. @.@

(특별한 것을 바라셨다면, 죄송합니다. ㅜㅡㅜ)



이번에도 정말 긴 글이 된 것 같습니다. @.@


하지만, 이 패턴은 매우 뜻이 깊습니다.

OOP & FP 의 첫 번째 과제였고, 스터디에서 받은 질문을 해결해주며 엄청 기분이 좋았던 것이 기억나에요. 하하하하하..

(아마, 이 과제에 오므라이스가 걸려있었습니다. 잘 해결주셔서 지금도 감사합니다.^^)

또한, 제가 애용하는 전략패턴과 조합한 세 번째 방식은 최근에 개발한 것이라 조금 더 뜻이 깊습니다.


이 주제로 100회를 마무리할 수 있어서 행운입니다. ^^;


이제, Effective OOP 와 관련된 주제로 한 개의 포스팅을 남겨두고 있습니다. (Hello, MVC.)


한번, 마지막까지 파이팅 해보자구용. @.@







반응형
Posted by N'

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'

비지니스 로직을 구현하다보면, 여러 상황에 마주칠 수 있습니다.


그 중 가장 많이 접하는 일은 아마 데이터를 자료구조에 담아 어떤 작업을 수행하도록 하는 것이라 생각합니다.


이와 관련된 내용으로 내부 구현 방법은 노출시키지 않으며 내부 데이터를 순회하는 반복자 패턴에 대해 다룬 적이 있습니다.



이번에 다룰 내용도 살짝 비슷한 내용입니다. @.@


종종 우리의 비지니스 작업 중 Tree 형태로 나타내고 싶은 경우가 종종 있습니다. 

오늘 다뤄볼 Composite 패턴은 객체들 간의 관계를 Tree 구조로 구성하여, 사용자가 단말노드(Leaf : 가장 하위 계층, 자식이 없는 노드)중간노드(Composite : 복합 계층, 자식이 있는 노드)를 동일하게 취급하여 다룰 수 있도록 해줍니다.


언제나 앞써, 개념적으로 나타내는 첫 설명은 어려운 듯 합니다. ㅡㅡ^

그렇다고, 예제를 바로 살펴보기 보다는 UML 을 먼저 살펴보고자 합니다.

 

 

위의 UML을 살펴보면, Tree 구조를 나타내기 위해 단말노드(Leaf)와 중간노드(Composite) 역할을 하는 하위개념를 두고 있으며, 중간노드는 1:N 관계로써 자식에 해당하는 Component 목록을 가지고 있습니다.


[Composite-Component 사이]에 관계가 존재하는 이유는 중간노드의 자식이 단말노드일 수도 있고 또 다른 중간노드일 수도 있기 때문이겠죠?

(즉, 구체적인 것에 의존하지 않게 하기 위함입니다. DIP 기억나나요???)


물론, Component 라는 상위개념을 둠으로써 클라이언트는 두 가지 하위개념을 동일하게 취급할 수 있습니다.


결국 Composite 패턴은 has-a 관계를 이용하여 트리 관계를 구성하고, 이들을 동일하게 취급하여 사용하는 것이 목적입니다.


이번 포스팅은 위에서 언급한 Composite 패턴의 두 목적을 충족할 수 있도록, 예제를 준비했습니다.

아래 내용에서는 회사의 조직도를 나타낼 것이며, 조직도를 나타내는 개념은 조직과 구성원정도로 나눌 생각입니다.

이렇게 나눈 하위개념을 지난 포스팅에서 다룬 반복자 패턴을 이용해서, 대표적인 트리 순회 알고리즘인 DFS(깊이우선탐색)로 순회하도록 해보고자 합니다.



1. 조직과 구성원의 상위 개념 Node. 


첫 번째로 기술할 개념은 조직과 구성원의 상위 개념인 Node 입니다.


두 개념의 상위 클래스이기 때문에, 두 하위 개념이 공통적으로 가져야할 것과 클라이언트가 하위 개념을 동일하게 취급할 수 있도록 추상적인 메소드 서명을 제공할 생각입니다.


여기에서 두 하위개념은 공통적으로 이름을 가진다고 가정할 생각입니다.

또한, 반복자를 적용하기 위해 Iterable 를 구현할 생각입니다.


구현을 해본다면, 아래와 같이 될 것 같습니다.


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
/**
 * 트리 구조의 노드 클래스 명시.
 *
 * Created by Doohyun on 2017. 8. 27..
 */
public abstract class Node implements Iterable<Node>{
 
    private String name;
 
    /**
     * 모든 노드 클래스는 이름을 가지고 있어야함.
     *
     * @param name
     */
    public Node(String name) {
        this.name = name;
    }
 
    /**
     * 이름출력.
     *
     * @return
     */
    public String getName() {
        return name;
    }
 
    /**
     * 노드를 추가.
     *
     * @param node
     */
    public abstract void addChild(Node node);
 
    /**
     * 자식 노드 출력.
     *
     * @return
     */
    public abstract List<Node> getChildList();
}
cs


Iterable 을 구현하도록 하였다면, 해당 반복자를 출력하는 Iterable::iterator 를 필수로 구현해야 합니다. 

하지만, 추상클래스이기 때문에 굳이 언급을 하지 않아도 되는군요.


구체적인 구현은 각 하위개념이 할 예정입니다.



2. 하위개념과 DFS.


이번에는 하위개념인 조직과 구성원, 그리고 DFS 를 수행하는 방법을 구현해볼 생각입니다.


먼저, 조직부터 살펴보죠.

조직은 Composite 역할을 담당하는 클래스로써, 위의 언급된 UML 처럼 트리의 자식인 Node 목록을 has-a 로 유지합니다.


Node 목록을 관리를 위해, Node::addChild 와 Node::getChildList 를 구현할 것이며, 구체화 클래스이기 때문에 Iterable::iterator 역시 구체적으로 구현해야 합니다.


구현된 조직클래스는 아래와 같습니다.


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
/**
 * 조직도를 나타내는 노드.
 *
 * Created by Doohyun on 2017. 8. 27..
 */
public class Organization extends Node {
 
    private ArrayList<Node> nodeList;
 
    /**
     * 조직도 구성.
     *
     * @param name
     */
    public Organization(String name) {
        super(name);
 
        nodeList = new ArrayList<>();
    }
 
    /**
     * 자식 Node 를 추가.
     *
     * @param node
     */
    @Override
    public void addChild(Node node) {
        nodeList.add(node);
    }
 
    /**
     * 자식 Node 목록을 출력.
     *
     * @return
     */
    @Override
    public List<Node> getChildList() {
        return nodeList;
    }
 
    /**
     * 반복자 전략을 출력.
     *
     * <pre>
     *     반복자는 본인을 담은 목록의 반복자 전략을 출력.
     * </pre>
     *
     * @return
     */
    @Override
    public Iterator<Node> iterator() {
 
        ArrayList<Node> arrayList = new ArrayList<>();
        arrayList.add(this);
 
        return new DFSIterator(arrayList.iterator());
    }
}
cs


주목해 볼 것은 반복자의 구현인 Organization::iterator 메소드 입니다. 

이 곳에서 보이는 DFSIterator 는 DFS 방식으로 트리를 순회하기 위해, 따로 제작한 전략 클래스입니다. 


생각하고 있는 꿈은 이 곳의 전략 클래스를 교체하는 방식으로, 언제든 순회방식을 바꾸고자 합니다만,,,,

일단 DFSIterator 부터 구현해보죠...


아래는 DFS 알고리즘을 구현한 전략 Iterator 클래스입니다.


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
/**
 * DFS 로 작동하는 반복자 정의.
 *
 * Created by Doohyun on 2017. 8. 27..
 */
public class DFSIterator implements Iterator<Node>{
 
    private Stack<Iterator<Node>> nodeStack = new Stack<>();
 
    /**
     * 생성자 구현.
     *
     * @param nodeIterator
     */
    public DFSIterator(Iterator<Node> nodeIterator) {
        this.nodeStack.push(nodeIterator);
    }
 
    /**
     * 노드스택이 비어있지 않고, 다음 반복자가 비어있지 않을 때 다음 순회를 할 수 있음!!
     *
     * @return
     */
    @Override
    public boolean hasNext() {
        if (nodeStack.isEmpty()) {
            return false;
        } else {
            return nodeStack.peek().hasNext();
        }
    }
 
    /**
     * 순회 방법을 구현.
     *
     * @return
     */
    @Override
    public Node next() {
 
        Node node;
        {
            // 첫 노드를 뽑는다.
            Iterator<Node> nodeIterator = nodeStack.peek();
 
            node = nodeIterator.next();
 
            if (!nodeIterator.hasNext()) {
                // 다음 데이터가 없다면, 스택에서 제거.
                nodeStack.pop();
            }
 
            if (!node.getChildList().isEmpty()) {
                // 자식이 존재한다면, 자식 목록의 반복자를 노드스택에 넣는다.
                nodeStack.push(node.getChildList().iterator());
            }
        }
 
        return node;
    }
}
 
cs


이번 포스팅에서는 DFS 알고리즘을 포스팅하는 것이 아니기 때문에, 구체적인 알고리즘 전략을 언급하지는 않습니다.

(구글링을 조금 해보면, DFS 알고리즘은 금방 찾을 수 있을 것입니다. @.@)


이 포스팅에서 생각해볼 점은 반복자를 구현함으로써, 반복하는 방법을 숨긴 상태로 클라이언트는 목적에 맞게 순회할 수 있음을 알면 더 좋을 듯 싶습니다.

(다시 한번 언급해보는 반복자 패턴의 목적입니다. @.@)



마지막으로 구현할 것은 Leaf 에 해당하는 구성원입니다.

구성원은 자식을 가지지 않으며, 반복자 또한 필요없어보입니다.


이에 따라, 구현된 구성원 클래스는 다음과 같습니다.


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
/**
 * 구성원을 나타내는 노드.
 *
 * Created by Doohyun on 2017. 8. 27..
 */
public class Member extends Node {
 
    public Member(String name) {
        super(name);
    }
 
    /**
     * 구성원 추가 구현 시, 어떤 것도 하지 않음.
     *
     * @param node
     */
    @Override
    public void addChild(Node node) {
        // NOTING WORK
    }
 
    /**
     * 빈 목록 출력.
     *
     * @return
     */
    @Override
    public List<Node> getChildList() {
        return Collections.emptyList();
    }
 
    /**
     * 반복자를 구현하지 않음.
     *
     * @return
     */
    @Override
    public Iterator<Node> iterator() {
        return null;
    }
}
cs


그러나 한번 고려해볼 점은 구성원 클래스는 Iterable 를 구현하는 클래스이고, 아래와 같은 코드가 가능함을 의미합니다.


1
2
3
for (Node node : new Member("강현지")) {
    System.out.println(node.getName());
}
cs


이 상황에서 Member 클래스의 반복자는 Null 이기 때문에 NullPointerException 이 발생할 것입니다.

즉, 한 단위에 대해서 반복하는 전략 클래스를 제공해야할 것 같습니다.


이로써, 구현한 LeafIterator 는 다음과 같습니다.


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
/**
 * 단말노드 반복자.
 *
 * Created by Doohyun on 2017. 9. 3..
 */
public class LeafIterator implements Iterator<Node> {
 
    private Node node;
    private Boolean hasNext = true;
 
    /**
     * 오직 한 개의 목록을 가짐.
     *
     * @param node
     */
    public LeafIterator(Node node) {
        this.node = node;
    }
 
    /**
     * 한번 사용하고 끝내도록 구현.
     *
     * @return
     */
    @Override
    public boolean hasNext() {
        return hasNext;
    }
 
    /**
     * 다음을 수행한 후, hasNext 를 거짓으로 돌림.
     *
     * @return
     */
    @Override
    public Node next() {
 
        hasNext = false;
 
        return node;
    }
}
cs


오직, 한번만 순회할 수 있도록 플래그를 두어 반복자 클래스를 제작했습니다.

즉, Leaf 인 구성원 클래스는 이 반복자를 사용하도록 해야합니다.


1
2
3
4
5
6
7
8
9
10
11
12
public class Member extends Node {
 
    /**
     * Leaf 반복자를 사용하도록 변경.
     *
     * @return
     */
    @Override
    public Iterator<Node> iterator() {
        return new LeafIterator(this);
    }
}
cs



3. Composite 패턴 사용.


앞써, 만든 조직과 구성원 클래스를 이용하여 트리구조를 구성해 볼 생각입니다.

이렇게 구성된 조직은 for-loop 을 통해, DFS 알고리즘으로 순회하도록 할 것입니다.


테스트 코드는 아래와 같습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Organization 웹솔센터 = new Organization("웹솔센터");
 
// 자식노드로 구성원을 추가하고 있음.
Organization 웹솔2팀 = new Organization("웹솔2팀");
웹솔2팀.addChild(new Member("남두현"));
웹솔2팀.addChild(new Member("유덕형"));
 
// 자식노드로 구성원을 추가하고 있음.
Organization 웹솔3팀 = new Organization("웹솔3팀");
웹솔3팀.addChild(new Member("강현지"));
웹솔3팀.addChild(new Member("유형주"));
 
// 자식노드로 조직을 추가하고 있음. (즉, 구체적인 것에 의존하지 않는 DIP)
웹솔센터.addChild(웹솔2팀);
웹솔센터.addChild(웹솔3팀);
 
// 클라이언트는 단순히 for-loop 만 사용하면 됨)
for (Node node : 웹솔센터) {
    System.out.println(node.getName());
}
cs


작성하고 보니, 결과를 잊었군요.

물론 아래와 같이 깊이우선탐색을 잘 수행하고 있습니다. @.@


1
2
3
4
5
6
7
8
9
// CONSOLE RESULT
//
// 웹솔센터
// 웹솔2팀
// 남두현
// 유덕형
// 웹솔3팀
// 강현지
// 유형주
cs


위의 코드는 첫 번째 목적인 객체들 간의 관계를 Tree 형태로 잘 나타내고 있습니다.

Node 라는 추상적인 것에 Organization 이 의존하도록 하여, 조직이든 구성원이든 자식으로 추가할 수 있습니다.


또한 반복자를 이용함에 있어서, 어떤 하위개념이든 동일하게 사용할 수 있습니다.

단순하게 for-loop 만 이용하면 되죠.


살짝 복잡해보이지만 데이터 구조를 트리 형태로 가진다는 것을 제외하면, 클래스의 목적에 따라 다형적으로 구현하고 있습니다.

UML 역시 다른 디자인패턴과 그리 달라보이지 않습니다. @.@


계속하여 언급되는 다형성의 중요성과 사용사례를 접하면 언젠가 OOP 고수가 될 수 있지 않을까요?

(저는 아직 부족함을 느끼고 있지만, 언제나 공부할 때마다 깨달음은 얻는 것 같습니다.^^;)


이 글이 많은 분들께 도움이 되었으면 좋겠습니다.



반응형
Posted by N'

프로그래머는 일련의 비지니스 문제를 해결하기 위해 프로그램을 개발합니다.

(당연한 이야기지만, ㅡㅡ^)


이 때 비지니스 로직 중 가장 많이 사용하는 것은 아마 자료구조일 것이며, 수많은 목록형 데이터를 처리하기 위해 자료구조를 순회하며 행위를 수행하도록 작성하곤 합니다.


이를테면, 아래와 같이 말이죠?


1
2
3
4
5
6
List<String> nameList = Arrays.asList("강현지""남두현""유덕형""유형주");
        
for (String name : nameList) {
    // 리스트 목록을 순회하며, 데이터를 콘솔로 출력
    System.out.println(name);
}
cs


이와 같이 List 자체의 내부 구현 방법은 노출되어 있지 않지만, 내부의 데이터를 쉽게 순회할 수 있다는 것은 매력적입니다.

(즉 사용자는 ArrayList, LinkedList 의 내부 구현 상태는 알 필요가 없고, for-each 같은 순회 방법만 알면 됩니다.)


이와 같은 패턴을 Iterator 패턴(반복자 패턴)이라 부르며, 이번 포스팅에서는 이에 대한 예제와 함께 자세한 이야기를 다뤄보고자 합니다.


Iterator 패턴은 객체 내부의 특정 목록 데이터 순회하는 것이 목적이며, 흔히 아래와 같은 인터페이스를 구현하도록 하고 있습니다.

(아래 인터페이스는 java.util 패키지에 존재하는 Iterator 인터페이스 입니다.)


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public interface Iterator<E> {
 
    /**
     * 다음 순회하고자 하는 객체가 존재하는가?
     *
     * @return
     */
    boolean hasNext();
 
    /**
     * 순회하고자 하는 다음 객체를 출력.
     * 
     * @return
     */
    E next();
}
cs


해당 인터페이스는 특정 목록 데이터를 순회하는 로직에 사용하는 메소드 서명들을 포함하고 있습니다. 

(다음 순회하는 항목이 있는가? 다음 항목으로 이동 등...)


이를 이용하여, 특정 클래스의 내부 목록을 순회하는 방법을 다음과 같이 서술해 볼 수 있을 것 같군요.


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
/**
 * 심플한 반복자의 구현
 *
 * Created by Doohyun on 2017. 8. 1..
 */
public class SimpleArrayIterator implements Iterator<String>{
 
    private String[] helloArray;
    private int position = 0;        // 현재 위치한 포지션의 index 관리
 
    public SimpleArrayIterator(String[] helloArray) {
        this.helloArray = helloArray;
    }
 
    /**
     * 현재 포지션이 길이 안에 존재하며 해당 배열의 값이 null 이 아닐 때, 다음 항목이 존재한다고 볼 수 있습니다.
     *
     * @return
     */
    @Override
    public boolean hasNext() {
        return position < helloArray.length && helloArray[position] != null;
    }
 
    /**
     * 다음 포지션으로 진행.
     *
     * @return
     */
    @Override
    public String next() {
        ++position;
        return helloArray[position - 1];
    }
}
cs


이렇게 구현한 반복자를 사용하는 방법은 종종 비지니스 로직을 작성하던 중 구현해 본 적이 있을 것입니다.


보통 Iterator 를 사용하여 순회하는 예제는 다음과 같이 제공하곤 합니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 목록 데이터.
String[] nameArray = {"강현지""남두현""유덕형""유형주"};
 
// 반복자 제작.
SimpleArrayIterator simpleArrayIterator = new SimpleArrayIterator(nameArray);
 
// 반복자를 이용하여, 순회.
while (simpleArrayIterator.hasNext()) {
    System.out.println(simpleArrayIterator.next());
}
 
// CONSOLE LOG
// 강현지
// 남두현
// 유덕형
// 유형주
cs


하지만, 보통 우리는 반복자를 이용하여 데이터를 순회하기 보다는 for-loop 를 통해 순회를 하곤 합니다. 


특히, JAVA5 부터 지원하는 새로운 형태의 for문을 종종 이용하곤 하는데요.

이 때 Java.lang.Iterable 인터페이스를 구현해주면, 새롭게 작성된 클래스도 해당 for 문을 사용할 수 있습니다.


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
/**
 * Array 배열을 순회하는 반복가능 클래스 구현.
 *
 * Created by Doohyun on 2017. 8. 3..
 */
public class SimpleArrayIterable implements Iterable<String>{
 
    private String[] nameArray;
 
    public SimpleArrayIterable(String[] nameArray) {
        this.nameArray = nameArray;
    }
 
    /**
     * 반복자를 출력하도록 하고 있습니다.
     *
     * <pre>
     *     새로 작성된 for-loop 에서는 해당 반복자를 사용하도록 하고 있습니다.
     * </pre>
     *
     * @return
     */
    @Override
    public Iterator<String> iterator() {
        return new SimpleArrayIterator(nameArray);
    }
}
 
// 반복 가능 클래스 테스트 로직..
{
 
    // 목록 데이터.
    String[] nameArray = {"강현지""남두현""유덕형""유형주"};
 
    // 반복 가능 클래스의 인스턴스 제작.
    SimpleArrayIterable simpleArrayIterator = new SimpleArrayIterable(nameArray);
 
    // 새로운 형태의 for-loop 이용..
    for (String name : simpleArrayIterator) {
        System.out.println(name);
    }
 
}
cs


이로써, 예상할 수 있는 것은 모든 Collection 은 Iterable 을 구현하고 있다는 것을 알 수 있습니다.

모든 Collection 은 새로운 형태의 for-loop 을 사용할 수 있으니까요?


이와 같이, 알게 모르게 많은 사용하고 있던 Iterator 패턴은 [순회하는 방법을 캡슐화]하고 있습니다.

이는 Collection 의 내부 구현과 별도로 일관된 균일한 접근을 할 수 있도록 도와줄 수 있을 것 같습니다.

(꼭 Collection 이 아닌, Tree Graph 등 여러 자료구조를 탐색하는 것에도 사용할 수 있겠죠?)


이를 통해 꽤 괜찮은 컨셉의 비지니스 로직을 만들 수 있겠죠? ^^

한 번 이 글을 보고 실습해보면 나쁘지 않을 것 같습니다..... 



또한, 언제나 마무리 인사로,


"이 글을 보는 분들에게 도움이 되길 바랍니다. :-)"





반응형
Posted by N'

컴퓨터로 처리하는 데이터는 계속해서 복잡해져 왔고, 복잡한 프로세스를 가진 프로그램을 제작하기 위해 꽤 많은 기술과 방법들이 등장했습니다.


그 중 한가지는 OOP 로, 복잡한 데이터와 행위를 하나의 단위로 결합하여 관리함으로써 프로그램의 구조화 및 유지보수성을 키웠을 것이라 생각합니다.


이러한 가운데, 오늘 포스팅에서는 제작한 클래스의 내부 데이터(멤버 변수)들이 많고 이를 초기화(생성자 및 Setter 등)하는 과정이 생각보다 복잡할 때 사용하기 좋은 빌더 패턴(Builder Pattern)에 대해 다뤄보고자 합니다.


빌더 패턴을 이용하면, 객체 생성 시 초기화하는 과정을 보다 직관적이며 편리하게 이용할 수 있습니다.


예를들어, 아래와 같이 멤버변수가 많은 클래스가 있습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
 * 빌더 패턴 테스트를 위한 VO
 *
 * Created by Doohyun on 2017. 7. 20..
 */
public class TestModelVo {
 
    private Integer field1;
    private Integer field2;
    private Integer field3;
    private Integer field4;
    private Integer field5;
    private Integer field6;
    private Integer field7;
    
    // SETTER, GETTER 생략
}
cs


이 클래스를 이용하여 객체를 제작해야할 경우, 각 멤버변수에 데이터를 넣는 방법은 다음과 같이 두 가지 방법을 생각해볼 수 있습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// setter 를 이용하는 방법.
{
    TestModelVo testModelVo = new TestModelVo();
    testModelVo.setField1(1);
    testModelVo.setField2(2);
    testModelVo.setField3(3);
    testModelVo.setField4(4);
    testModelVo.setField5(5);
    testModelVo.setField6(6);
    testModelVo.setField7(7);
}
 
// 한 번에 초기화할 수 있는 생성자를 제공하는 방법.
{
    TestModelVo testModelVo = new TestModelVo(1,2,3,4,5,6,7);
}
cs


모두 많이 사용하는 방법일 것이라 생각합니다.


첫 번째 방법은 Setter 를 이용하여 부분적으로 초기화를 수행하고 있습니다. 

보통 이렇게 많이 사용하는 편이지만, 초기화 과정이 한 단위 씩 끊어서 일어나고 있습니다.

(즉 객체는 이미 생성되었고 생성된 객체에 데이터를 넣는 과정. 일반적이며 나쁘지 않은 방법)


두 번째 방법은 데이터를 미리 받아, 객체를 생성할 수 있도록 생성자를 정의하는 방법입니다.

이 방법도 많이 사용하는 편이지만, 위의 예제와 같이 기화 값이 많을 경우 가독성 및 사용성이 떨어질 수 있습니다. 

(각 파라미터가 무엇인지, 제대로 값은 넣고 있는지, 등의 확인이 필요할 듯 합니다.)


또한 부분적으로 멤버변수를 초기화를 하고 싶은 경우, 그만큼의 생성자를 만들어야할 수 있습니다.



빌더패턴을 이용하면 복잡한 객체 생성 과정을 한 단위의 작업으로 다양하게 처리할 수 있으며, 이는 꽤 많이 사용될 여지를 줄 수 있습니다.


빌더패턴의 목적은 '복잡한 특정 객체의 생성 및 초기화할 수 있는 클래스의 제작'이며, 각 초기화 과정을 파이프라인 형식으로 사용할 수 있는 메소드를 제공하여 한 단위로 다양한 초기화 과정을 만들 수 있습니다.


파이프라인 형식은 일련의 행위를 수행한 후 자기 자신을 출력의 결과로 넘김으로써, 메소드를 파이프 연결 하듯이 계속 이어 사용할 수 있도록 하는 방법입니다.


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
/**
 * TestModelVo 를 생성할 수 있는 빌더.
 *
 * Created by Doohyun on 2017. 7. 20..
 */
public class TestModelVoBuilder {
 
    private TestModelVo testModelVo;
 
    private TestModelVoBuilder() {
        testModelVo = new TestModelVo();
    }
 
    /**
     * 빌더 인스턴스를 생성하는 정적 메소드.
     *
     * @return
     */
    public static TestModelVoBuilder Builder() {
        TestModelVoBuilder instance = new TestModelVoBuilder();
 
        return instance;
    }
 
    /**
     * 특정 초기화 작업 후, 자기자신을 결과를 넘김으로 파이프라인식의 메소드로 사용 가능.
     *
     * @param field1
     * @return
     */
    public TestModelVoBuilder setField1(Integer field1) {
        testModelVo.setField1(field1);
 
        return this;
    }
 
    /**
     * 필드2 초기화.
     *
     * @param field2
     * @return
     */
    public TestModelVoBuilder setField2(Integer field2) {
        testModelVo.setField1(field2);
 
        return this;
    }
 
    /**
     * 제작완료 후, 결과 VO 출력.
     *
     * @return
     */
    public TestModelVo build() {
        return testModelVo;
    }
}
 
// 한 단위로 복잡한 생성과정을 다양하게 만들 수 있어보임. (메소드를 이어 사용하는 파이프라인)
TestModelVo testModelVo = TestModelVoBuilder.Builder().setField1(1).setField2(2).build();
cs


이 방법을 이용하면, 한 단위로 다양한 생성자를 제작하는 것과 비슷한 효과를 볼 수 있습니다.

또한 파이프라인 형식으로 이용하기 때문에 편리하며, 메소드를 이용하기 때문에 초기화하는 방법이 직관적입니다.


이 방법은 설정과 관련된 객체를 생성하거나, 함수형 프로그래밍에서 일련의 쿼리를 만드는 등 여러 목적으로 많이 사용하고 있습니다.



예를들어, JAVA8 에서 등장한 Comparator::thenCompare 는 다음과 같은 원리로 만들어볼 수 있습니다.

(Comparator::thenCompare 는 정렬의 조건을 추가할 수 있는 메소드입니다.)


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
/**
 * thenCompare 기능을 지원하는 Comparator Builder
 *
 * Created by Doohyun on 2017. 7. 20..
 */
public class SimpleComparatorBuilder<T> {
    private Comparator<T> comparator;
 
    private SimpleComparatorBuilder() {
    }
 
    /**
     * 첫 초기화 시, 무조건 정렬조건을 받도록 함.
     *
     * - 적어도 한 조건을 받고, 다양한 비교조건을 제작.
     *
     * @param comparator
     * @param <T>
     * @return
     */
    public static <T> SimpleComparatorBuilder<T> Create(Comparator<T> comparator) {
        SimpleComparatorBuilder<T> simpleComparator = new SimpleComparatorBuilder<>();
 
        simpleComparator.comparator = comparator;
 
        return simpleComparator;
    }
 
    /**
     * 정렬의 조건을 추가하는 메소드.
     * 
     * @param inputComparator
     * @return
     */
    public SimpleComparatorBuilder<T> thenCompare(Comparator<T> inputComparator) {
        SimpleComparatorBuilder<T> simpleComparator = new SimpleComparatorBuilder<>();
 
        // 동적인 비교자 생성. 일련의 동적파라미터화.
        simpleComparator.comparator = (a, b) -> {
            // 기존 존재하는 비교자를 이용하여 비교.
            int compareResult = this.comparator.compare(a, b);
 
            if (compareResult == 0) {
                // 비교 결과, 두 객체가 동일할 경우 새로 입력된 비교 연산자를 이용하여 비교.
                return inputComparator.compare(a, b);
            } else {
                // 비교 결과가 다를 경우, 결과를 출력.
                return compareResult;
            }
        };
 
        // 새로 제작된 빌더를 출력.
        return simpleComparator;
    }
 
    /**
     * 조건에 대한 추가가 끝났을 경우, 내부의 비교자를 출력.
     * 
     * @return
     */
    public Comparator<T> build() {
        return comparator;
    }
}
cs


SimpleComparatorBuilder 의 목적은 비교자(Comparator)의 생성에 있어서, 비교하는 과정을 파이프라인 형식으로 다양하게 만들 수 있도록 thenCompare 기능을 제공하는 것입니다.


이를 이용하여, 다음과 같이 다양한 조건에 따라 비교하는 비교쿼리를 수행할 수 있습니다.


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
/**
 * 비교구문을 테스트하는 메소드.
 */
public static void 비교_구문_테스트() {
    // 테스트를 위한 목록
    List<MemberVo> memberVos = Arrays.asList(
                                        new MemberVo("강XX"25)
                                        , new MemberVo("유XX"27)
                                        , new MemberVo("유XX"21));
 
 
    /**
     * 이름으로 내림차순
     * 나이로 오름차순.
     */
    Collections.sort(memberVos
                , SimpleComparatorBuilder.<MemberVo>Create((a, b) -> b.getName().compareTo(a.getName())).
                     thenCompare((a, b) -> a.getAge().compareTo(b.getAge())).build()
    );
 
    System.out.println(memberVos);
}
 
// CONSOLE LOG
// [{유XX, 21세}, {유XX, 27세}, {강XX, 25세}]
cs


다행히, 잘 작동하는 모듈이 만들어졌습니다. :-)

(시간이 괜찮다면, 이 글을 보고 직접 만들어보는 것을 추천합니다.)


이와 같이, 빌더패턴을 이용하면 단순히 객체 초기화 뿐만이 아닌 어떤 원하는 다양한 목적을 쉽게 구현할 수 있을 것입니다.

(Stream API 가 대표적이며, 이를 이용하여 Collection 에 대해 다양한 쿼리를 제작할 수 있습니다.)



"간단하지만, 쓸모가 적절한 이 패턴을 잘 알아두면 정말 좋지 않을까요?" 

라는 생각을 해보며, 이번 포스팅을 마칩니다.



이 글을 읽는 모두가 도움이 되었으면 좋겠습니다. :-)






반응형
Posted by N'

OOP 를 접할 때, 처음 다루는 내용 중 한 가지는 데이터와 기능을 한 단위로 관리하자는 Object(객체)의 가장 중요하고 기본이 되는 패러다임입니다.


이 때 객체 내부에서 관리하는 데이터를 객체의 상태라고 말할 수 있으며, 객체의 상태에 따라 분기처리를 이용하여 해야할 기능을 각각 구현 하는 경우는 많았을 것입니다.


이를테면, 아래와 같이 말이죠..


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class TestObject {
    
    // 내부 상태.
    private Integer status;
 
    /**
     * 내부 상태에 따라, 특정 기능을 수행.
     */
    public void func() {
        // 종종 사용해볼 수 있는 분기 처리
        
        switch (status) {
            case 1:
                // 액션1 수행
                break;
            case 2:
                // 액션2 수행
                break;
            default:
                // 액션3 수행
        }
    }
}
cs


심플한 구현이지만, OOP 의 철학에서는 이와 같은 분기처리에 대해 꽤 경계를 하는 듯 합니다.

(OOP 철학에서는 분기처리를 다형성(polymorphism)으로 대체할 수 있음을 강조하고 있습니다.)


이전의 다뤘던 OOP 관련 많은 포스팅에서 간접적으로 분기를 대체하는 방법론들을 다뤄왔지만, 

이번 포스팅에서는 조금 더 본질적인 '왜 분기처리를 경계 해야하는가?' 에 대한 이유와 함께 객체의 내부상태를 OOP 관점에서 관리하고자한 상태 패턴(State-Pattern)에 대해 다뤄보려고 합니다.


오늘도 역시 요구사항으로 시작합니다.



1. 요구사항


RPG 의 성공적인 신화를 거둔 N사에 취직한 당신은, 그 희망처럼 신규로 제작협의가 된 프로젝트로 발령이 났습니다.


큰 기대에 부푼 당신!


메인 디렉터는 당신의 열정을 알아보고 첫 번째 미션을 맡겼습니다.


그것은 바로 "NPC 의 호감도" 구현입니다.

Player 는 NPC 에게 특정 액션을 취할 수 있고, NPC 들은 Player 의 호감상태에 따라 반응이 달라집니다.

(예를들면, NPC 들이 대화주제에 개인사가 더 포함하거나, 비밀상점을 열어주는 등)


Player 가 NPC 에게 할 수 있는 액션은 정해져 있으며(대화하기, 선물주기 등), 수행한 액션수에 따라 NPC 의 호감도가 결정되며 그에 따라 액션을 수행합니다.


일단, 너무 많은 수를 고려하지 않고 NPC 한 명에 대한 프로토 타입을 목표로 하려 합니다.



2. 객체 내부 변수와 그에 따른 행동.


요구사항 명세를 먼저 살펴볼 때, 명확한 한가지 사실은 Player 가 NPC 에게 취했던 액션의 횟수를 기록해야 한다는 것입니다.


그렇기 때문에, NPC 클래스 내부의 멤버변수로 액션의 횟수를 다음과 같이 기록하고자 합니다. 

내부 멤버변수를 변화시킬 수 있는 메소드도 같이 말이죠.


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
/**
 * NPC 객체 구현, 이름은 "Doohyun"
 * <p>
 * Created by Doohyun on 2017. 7. 12..
 */
public class NPC {
 
    private String name = "Doohyun";        // 이름 
 
    // 내부 상태.
    private Integer talkCount = 0;          // 대화횟수
    private Integer giftCount = 0;          // 구매횟수
 
    /**
     * 대화하기
     *
     * @param playerName
     */
    public void talkWithHim(String playerName) {
        ++talkCount;
    }
 
    /**
     * 선물하기
     *
     * @param playerName
     */
    public void giftToHim(String playerName) {
        ++giftCount;
    }
}
cs


이제, 각 액션에 대한 구체적인 행위(NPC::talkWithHim, NPC::giftToHim)를 구현해 보죠.


조금 더 구체적인 요구사항인 행위 횟수에 따른 호감도는 다음과 같습니다. 


- 대화횟수 20 초과 일 때

- 선물횟수 10 초과 : 호감

- 선물횟수 10 이하 : 비호감


- 대화횟수 20 이하일 때 : 기본


이에 따라, 구현은 아래와 같습니다.


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
/**
 * 대화하기
 *
 * @param playerName
 */
public void talkWithHim(String playerName) {
 
    if (talkCount > 20) {
        if (giftCount > 10) {
            // 호감 상태.
            System.out.printf("%s씨와 대화는 언제나 재미있어요. (%s는 나를 좋아하는 것 같다.)\n", playerName, name);
        } else {
            // 비호감 상태
            System.out.printf("%s씨, 죄송하지만 지금 좀 바빠서요. (%s는 나와의 대화가 지루해보인다.)\n", playerName, name);
        }
    } else {
        // 기본 상태.
        System.out.printf("%s씨 다음에 봐요. (%s와의 대화를 마쳤다.)\n", playerName, name);
    }
 
    ++talkCount;
}
 
/**
 * 선물하기
 *
 * @param playerName
 */
public void giftToHim(String playerName) {
 
    if (talkCount > 20) {
        if (giftCount > 10) {
            // 호감 상태.
            System.out.printf("%s씨 감사해요. 이건 제가 사는 커피입니다. (%s 에게서 '아메리카노'를 획득했다.)\n", playerName, name);
        } else {
            // 비호감 상태
            System.out.printf("이런 선물은 조금 부담이 느껴지네요.. (%s는 내 선물을 부담스러워하는 것 같다.)\n", name);
        }
    } else {
        // 기본 상태.
        System.out.printf("%s씨 담에 커피 한잔해용. (%s와의 대화를 마쳤다.)\n", playerName, name);
    }
 
    ++giftCount;
}
cs


행위 횟수에 따라 잘 구현이 되었지만, 코드 중복이 다수 보입니다.


아직은 NPC 에게 할 수 있는 액션이 별로 없기 때문에 상관이 없지만, 요구사항이 추가되었을 때 그만큼 코드중복이 생길 것입니다.


코드 중복이 생기는 만큼, 추 후 수정을 할 때 모두 찾아 고쳐야 함을 의미합니다.

이는 유지보수에 쉽지 않다는 것을 의미합니다. ㅡㅡ^


이를 리팩토링하기 위하여, 크게 생각하지 말고 복잡한 if-else 관계부터 캡슐화해보죠. 



3. 코드(CODE)를 이용한 리팩토링


복잡한 if-else 관계이지만, 우리는 이를 살짝 리팩토링할 수 있을 것 같습니다.


NPC 는 호감도라는 일종의 상태를 따로 가지고 있으며, 해당 상태에 따라 적절한 행위를 하도록 하는 것이죠.


추상적인 개념인 호감도는 코드 로써 정의를 할 것입니다.

(현재 구축할 코드는 호감, 비호감, 기본상태 정도인 듯 합니다.)


코드 개념을 도입해서 진행한 리팩토링은 다음과 같습니다.


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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
/**
 * NPC 객체 구현, 이름은 "Doohyun"
 * <p>
 * Created by Doohyun on 2017. 7. 12..
 */
public class NPC {
 
    private String name = "Doohyun";
 
    /**
     * 호감상태 코드 정의.
     */
    public static class CODE_FEELING {
        public static final String GOOD = "GOOD";           // 호감
        public static final String NOT_GOOD = "NOT_GOOD";   // 비호감
        public static final String NORMAL = "NORMAL";       // 기본상태.
    }
 
    private String feeling = CODE_FEELING.NORMAL;           // 호감도.
 
    /**
     * 현재 호감도를 정의하는 메소드 정의.
     */
    private void generateFeeling() {
        if (talkCount > 20) {
            if (giftCount > 10) {
                // 호감 상태.
                feeling = CODE_FEELING.GOOD;
            } else {
                // 비호감 상태
                feeling = CODE_FEELING.NOT_GOOD;
            }
        } else {
            // 기본 상태.
            feeling = CODE_FEELING.NORMAL;
        }
    }
 
    /**
     * 대화하기
     *
     * @param playerName
     */
    public void talkWithHim(String playerName) {
        // 호감상태 변경.
        generateFeeling();
 
        // 상태에 따른 분기처리.
        switch (feeling) {
            case CODE_FEELING.GOOD:
                System.out.printf("%s씨와 대화는 언제나 재미있어요. (%s는 나를 좋아하는 것 같다.)\n", playerName, name);
                break;
            case CODE_FEELING.NOT_GOOD:
                System.out.printf("%s씨, 죄송하지만 지금 좀 바빠서요. (%s는 나와의 대화가 지루해보인다.)\n", playerName, name);
                break;
            case CODE_FEELING.NORMAL:
                System.out.printf("%s씨 다음에 봐요. (%s와의 대화를 마쳤다.)\n", playerName, name);
                break;
        }
 
        ++talkCount;
    }
 
    /**
     * 선물하기
     *
     * @param playerName
     */
    public void giftToHim(String playerName) {
        // 호감상태 변경.
        generateFeeling();
 
        // 상태에 따른 분기처리.
        switch (feeling) {
            case CODE_FEELING.GOOD:
                System.out.printf("%s씨 감사해요. 이건 제가 사는 커피입니다. (%s 에게서 '아메리카노'를 획득했다.)\n", playerName, name);
                break;
            case CODE_FEELING.NOT_GOOD:
                System.out.printf("이런 선물은 조금 부담이 느껴지네요.. (%s는 내 선물을 부담스러워하는 것 같다.)\n", name);
                break;
            case CODE_FEELING.NORMAL:
                System.out.printf("%s씨 담에 커피 한잔해용. (%s와의 대화를 마쳤다.)\n", playerName, name);
                break;
        }
 
        ++giftCount;
    }
}
 
cs


이 리팩토링으로 인해, 오히려 NPC 클래스의 라인은 늘어났습니다. 

그렇다고 코드 중복이 없어진 것도 아닙니다. 


하지만 복잡한 if-else 상태에 대한 유연함을 가져올 수 있습니다.

오직 NPC::generateConfig 만 수정을 함으로써, 분기구문을 관리할 수 있습니다.

즉, 코드를 이용하여 일종의 캡슐화를 했다고 할 수 있습니다.


또한 코드 사용 부분의 가독성이 좋아졌습니다.

호감도 상태에 따라 무슨 일을 해야한다는 것이 명확하게 보이지 않나요? @.@


하지만, 여전히 코드를 사용하는 곳은 거슬립니다.

복잡한 코드의 사용을 단순화한 것은 좋아보이지만, 호감도 상태코드의 수정은 쉽지 않을 것 같습니다. 

(호감도 추가, 삭제를 위해서는 코드를 사용하고 있는 switch 상태를 점검해야할 것입니다.)


이 구문에서 제기한 문제는 단순히 코드 중복으로 인해 중복된 부분을 모두 고쳐야 하는 유지보수성의 문제만을 이야기한 것은 아닙니다.

오직 코드를 사용하는 곳이 한 곳이라도, 코드종류의 변경은 이미 작성된 로직 수정을 불러옵니다.

그 이유는 "코드라는 자체 조건"에 의해 분기하여 행위를 하기 때문이죠.


즉 분기의 사용은 '확장 개발을 해야할 경우, 기존 로직의 수정은 필수' 라는 의미를 내포하고 있으며, 이는 프로그램 확장성에 부담을 줄 수 밖에 없습니다.


SOLID 규칙 중, '수정에는 닫혀있고, 확장에는 열려있어야 한다.(OCP)' 는 언제나 이런 사실을 견제합니다.


OOP 에서는 이러한 분기로직을 대체할 개념으로 다형성을 말합니다. 

하지만 우리는 이를 이용한 개념은 전혀 생소하지 않을 것입니다. 이와 비슷한 문제를 풀어본 적이 있기 때문이죠.


바로 아래 내용에서 말이죠. :-)



'전략패턴' 에서 코드 사용문제를 다뤄본 것과 같이, 상태에 따른 행위를 위임하는 것으로 이 문제를 해결해 볼 수 있을 것 같습니다.



4. 상태에 따른 행위 캡슐화


상태에 따라, 해야할 일은 일단 두 가지 정도 되는 것 같습니다. (대화하기, 선물주기)

이에 따라 저는 다음과 같은 추상적인 클래스를 제작하였습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
 * 추상적인 호감도 클래스
 *
 * Created by Doohyun on 2017. 7. 15..
 */
public abstract class AbstractFeeling {
 
    /**
     * 감정에 따라 대화하는 액션 추상화.
     *
     * @param playerName
     * @param npcName
     */
    public abstract void talkAction(String playerName, String npcName);
 
    /**
     * 감정에 따라 선물에 대한 액션을 추상화
     *
     * @param playerName
     * @param npcName
     */
    public abstract void giftAction(String playerName, String npcName);
}
cs


이를 이용하여, 각 호감도에 대한 구체화 클래스를 제작할 수 있을 것 같습니다.


아래는 추상적인 호감상태를 구현한 구체화 클래스들입니다.

기존, 코드의 상태따라 switch 로 분기되어 처리되었던 행위를 캡슐화하였습니다.


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
/**
 * 호감상태 구체화 클래스
 *
 * Created by Doohyun on 2017. 7. 15..
 */
public class GoodFeeling extends AbstractFeeling {
 
    @Override
    public void talkAction(String playerName, String npcName) {
        System.out.printf("%s씨와 대화는 언제나 재미있어요. (%s는 나를 좋아하는 것 같다.)\n", playerName, npcName);
    }
 
    @Override
    public void giftAction(String playerName, String npcName) {
        System.out.printf("%s씨 감사해요. 이건 제가 사는 커피입니다. (%s 에게서 '아메리카노'를 획득했다.)\n", playerName, npcName);
    }
}
 
/**
 * 비호감 상태의 호감도
 *
 * Created by Doohyun on 2017. 7. 15..
 */
public class NotGoodFeeling extends AbstractFeeling {
 
    @Override
    public void talkAction(String playerName, String npcName) {
        System.out.printf("%s씨, 죄송하지만 지금 좀 바빠서요. (%s는 나와의 대화가 지루해보인다.)\n", playerName, npcName);
    }
 
    @Override
    public void giftAction(String playerName, String npcName) {
        System.out.printf("이런 선물은 조금 부담이 느껴지네요.. (%s는 내 선물을 부담스러워하는 것 같다.)\n", npcName);
    }
}
 
/**
 * 기본상태 호감도.
 *
 * Created by Doohyun on 2017. 7. 15..
 */
public class NormalFeeling extends AbstractFeeling{
 
    @Override
    public void talkAction(String playerName, String npcName) {
        System.out.printf("%s씨 다음에 봐요. (%s와의 대화를 마쳤다.)\n", playerName, npcName);
    }
 
    @Override
    public void giftAction(String playerName, String npcName) {
        System.out.printf("%s씨 담에 커피 한잔해용. (%s와의 대화를 마쳤다.)\n", playerName, npcName);
    }
}
cs


이제 NPC 클래스들이 캡슐화된 상태 클래스들을 사용하도록 리팩토링해보죠.


일단, 기존에 관리하던 코드는 제거할 생각입니다.

구체화된 상태클래스들이 그 역할을 대신할 수 있을 것 같습니다.


저는 아래와 같이 구현을 해보았습니다.


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
public class NPC {
 
    private String name = "Doohyun";
 
    // 내부 상태.
    private Integer talkCount = 0;          // 대화횟수
    private Integer giftCount = 0;          // 구매횟수
 
    // 상태 인스턴스 생성. (오직 한개만 있을 것이면, 싱글톤도 좋을 듯.)
    private final GoodFeeling goodFeeling = new GoodFeeling();
    private final NotGoodFeeling notGoodFeeling = new NotGoodFeeling();
    private final NormalFeeling normalFeeling = new NormalFeeling();
 
    private AbstractFeeling feeling = normalFeeling;           // 호감도.
 
    /**
     * 현재 호감도를 정의하는 메소드 정의.
     */
    public void generateFeeling() {
        if (talkCount > 20) {
            if (giftCount > 10) {
                // 호감 상태.
                feeling = goodFeeling;
            } else {
                // 비호감 상태
                feeling = notGoodFeeling;
            }
        } else {
            // 기본 상태.
            feeling = normalFeeling;
        }
    }
 
    /**
     * 대화하기
     *
     * @param playerName
     */
    public void talkWithHim(String playerName) {
        // 호감상태 변경.
        generateFeeling();
 
        // 다형성을 이용한 상태에 따른 행위 위임 처리.
        // 이는 상태가 추가 하더라도 이 부분을 고치지 않아도 됨을 의미 (OCP).
        feeling.talkAction(playerName, name);
 
        ++talkCount;
    }
 
    /**
     * 선물하기
     *
     * @param playerName
     */
    public void giftToHim(String playerName) {
        // 호감상태 변경.
        generateFeeling();
        
        feeling.giftAction(playerName, name);
 
        ++giftCount;
    }
}
cs


예상대로, 코드가 대체 되었고 상태에 따라 행위를 수행할 수 있도록 하였습니다.


이는 상수를 이용하여 코드를 사용하는 것에 비해, 보다 명백하게 상태를 나타낼 수 있습니다. 


또한, 상태에 따라 다른 행위를 위임할 수 있습니다. 

즉 상태에 따른 분기를 제거했으며, 이는 추 후 상태 추가에 대한 부담이 적음을 의미할 수 있습니다.


이 방식을 OOP 에서 말하는 상태패턴에서는 Context-driven 이라고 합니다.

주제(NPC)가 주체적으로 상태(feeling)를 변경하는 것을 의미하죠. 전략패턴과 매우 유사합니다.


물론, 상태가 스스로 변경하는 방식 역시 존재합니다. 



4. 상태의 전이 (Context-driven vs State-driven)


이번 절에서는 상태의 전이의 두 가지 방법에 대해 자세히 다뤄볼 예정입니다.


3절에서는 주체(Context)인 NPC 클래스가 특정조건에 대하여 상태를 변경하는 방식인 Context-driven 을 구현하였습니다. 


하지만, 현재 호감도 상태에 따라 전이 방식이 달라진다는 요구사항을 받았다고 가정해 봅시다.

예를들면, 현재 구현된 NPC::generateConfig 를 다음과 같이 변경해 볼 수 있습니다.


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 void generateFeeling() {
    if (talkCount > 20) {
        if (giftCount > 10) {
 
            if (feeling.getClass().equals(NormalFeeling.class)) {
                // 현재 상태가 기본일 때, 호감으로 변경.
                feeling = goodFeeling;
            } else if (feeling.getClass().equals(NotGoodFeeling.class)) {
                // 현재 상태가 비호감일 때, 기본으로 변경.
                feeling = normalFeeling;
            }
        } else {
            // 비호감 상태
            feeling = notGoodFeeling;
        }
    } else {
 
        if (feeling.getClass().equals(GoodFeeling.class)) {
            // 현재 상태가 호감일 때, 기본으로 변경.
            feeling = normalFeeling;
        }
    }
}
cs


즉 NPC 의 행위 횟수에 고정적으로 상태가 변경되는 것이 아닌, 동적인 현재의 호감도에 따라 전이방식이 달라질 수 있다는 요구사항입니다.


복잡한 과정을 NPC::generateConfig 로 몰았으나, [호감도 방식이 추가,삭제] 될 때 이 복잡한 로직은 꽤 골칫덩이가 될 수 있습니다.


State-driven 방식은 이 문제를 해결할 수 있는 체크메이트(checkmate)가 될 수 있을 것 같습니다.

Context-driven 이 주체가 특정 조건에 따라 상태를 변경을 했던 반면, State-driven 은 구체화 된 상태 클래스에서 특정 상황에 따라 스스로 상태를 전이합니다. 즉 이 방식은 상태 변경 행위 역시도 구체화된 각 상태 클래스에게 위임하겠다는 것을 의미합니다.


상태 클래스가 주제의 현재상태를 변경하기 위해서는, 주제에 대한 정보를 알고 있어야 합니다. 

그렇기 때문에 각 상태 클래스들이 주제를 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
/**
 * 추상적인 호감도 클래스
 *
 * Created by Doohyun on 2017. 7. 15..
 */
public abstract class AbstractFeeling {
 
    private NPC npc;
 
    /**
     * 상태를 사용하는 주제(NPC) 객체를 알도록 처리.
     *
     * <pre>
     *     - 주제와 상태가 서로 사용하는 관계 (Deep-coupling).
     *     - 바람직한 관계가 맞을까?
     * </pre>
     *
     * @param npc
     */
    public AbstractFeeling(NPC npc) {
        this.npc = npc;
    }
 
    /**
     * 주제 출력.
     *
     * @return
     */
    protected NPC getNpc() {
        return npc;
    }
 
    /**
     * 감정에 따라 대화하는 액션 추상화.
     *
     * @param playerName
     * @param npcName      // (NPC name 을 파라미터로 받을 필요가 없음. - NPC 인스턴스를 알고 있기 때문)
     */
    public abstract void talkAction(String playerName);
 
    /**
     * 감정에 따라 선물에 대한 액션을 추상화
     *
     * @param playerName
     */
    public abstract void giftAction(String playerName);
}
 
cs


추상 클래스의 생성자 및 각 메소드들의 서명이 변경 되었으니, 그에 따라 상태 클래스의 서명 역시 수정할 필요가 있습니다.


아래의 예제는 변경된 추상클래스의 필수조건을 반영하고, 상태 전이 기능을 담은 기본 상태의 구체화 클래스 입니다.


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
public class NormalFeeling extends AbstractFeeling{
 
    /**
     * 생성자 서명 변경.
     *
     * @param npc
     */
    public NormalFeeling(NPC npc) {
        super(npc);
    }
 
    @Override
    public void talkAction(String playerName) {
 
        // 상태 변경 처리.
        {
            if (getNpc().getTalkCount() > 20 && getNpc().getGiftCount() > 10) {
                // 대화 횟수가 20 초과 선물 횟수가 10 초과일 때,
                // 기본 상태이면 호감상태로 변경하는 로직 구현
                getNpc().setFeeling(getNpc().getGoodFeeling()); // -> 이 방식이 불편해보임.
            }
        }
 
        // NPC 의 이름은 이미 has-a 로 포함하는 NPC 객체에서 가져올 수 있다.
        System.out.printf("%s씨 다음에 봐요. (%s와의 대화를 마쳤다.)\n", playerName, getNpc().getName());
    }
 
    @Override
    public void giftAction(String playerName) {
        System.out.printf("%s씨 담에 커피 한잔해용. (%s와의 대화를 마쳤다.)\n", playerName, getNpc().getName());
    }
}
cs


NPC 에 대한 정보를 구체화 상태 클래스들이 모두 알고 있기 때문에, 자유롭게 상태전이 및 정보 이용을 할 수 있는 것을 확인할 수 있습니다.


하지만, 현재 작성된 코드에서 조금 거슬리는 부분이 있습니다.

상태전이를 수행하는 아래 부분말이죠.. 


1
getNpc().setFeeling(getNpc().getGoodFeeling()); // -> 이 방식이 불편해보임.
cs


NPC 객체를 변경하기 위해 사용하는 setter, getter 사용이 귀찮고 부적절해보입니다.

이 방법은 단지 불편함뿐만이 아닌, 구체화된 상태 클래스들 사이에서도 의존관계를 높게 할 여지를 줍니다.


차라리, NPC 객체에서 상태전이 메소드를 제공해주고 상태에서 사용하는 것은 어떨까요? 

이렇게 말이죠..


1
2
3
4
5
6
7
8
9
10
11
12
13
public class NPC {
 
    /**
     * 호감도를 "호감" 상태로 변경.
     */
    public void toChangeGoodFeeling() {
        setFeeling(goodFeeling);
    }
}
 
// NPC 클래스에서 제공해준 메소드를 사용하는 방식. 편리함..
// getNpc().setFeeling(getNpc().getGoodFeeling()); 
getNpc().toChangeGoodFeeling(); 
cs


어느정도 정리가 된 듯합니다.


이제 나머지 상태까지 서명을 변경한 뒤, NPC 메소드를 다음과 같이 수정해 봤습니다.

setter, getter, 상태전이 메소드(toChangeXXXX) 등은 생략했습니다. :-)


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
/**
 * NPC 객체 구현, 이름은 "Doohyun"
 * <p>
 * Created by Doohyun on 2017. 7. 12..
 */
public class NPC {
 
    /**
     * 대화하기
     *
     * @param playerName
     */
    public void talkWithHim(String playerName) {
 
         /**
         * 호감상태 변경 
         * 호감상태 변경 메소드는 더이상 필요하지 않음.
         * 상태가 행위를 수행할 때, 다형성에 의해 적절한 전이가 발생.
         * 즉, 상태를 전이하는 행위 역시 주제가 상태클래스들에게 위임.
         *
         * generateFeeling();
         **/
 
        // 다형성을 이용한 상태에 따른 행위 처리.
        // 이는 행위가 추가한다하더라도 이 부분을 고치지 않아도 됨을 의미.
        feeling.talkAction(playerName);
 
        ++talkCount;
    }
 
    /**
     * 선물하기
     *
     * @param playerName
     */
    public void giftToHim(String playerName) {
        feeling.giftAction(playerName);
 
        ++giftCount;
    }
}
cs


State-driven 방식을 이용함으로써, 주제에서 조건에 따라 상태를 전이하던 NPC::generateFeeling 이 필요가 없어졌습니다.


상태들이 스스로 조건에 따라 상태전이를 수행하며, 이는 복잡한 if-else 관계가 사라졌다는 점과 함께 상태들의 추가 삭제에 대한 유연성이 보다 높아졌음을 의미합니다.


예를들어 상태가 삭제될지라도, NPC에서 toChangeXXXX 메소드를 제공했기 때문에 해당 메소드를 빈 메소드로 만들어 사장(Deprecated)시키는 방법 생각해 볼 수 있습니다.

단순히 이 방법만으로, 삭제된 상태를 의존하고 있던 다른 형제 상태들을 수정하지 않아도 됩니다.

(아, 물론 나중에 추 후 리팩토링할 경우 사장된 메소드는 지워주면 좋겠죠...)



State-driven 은 Context-driven 에 보다 더 유연한 것처럼 보입니다.

결과적으로 보면, OOP 에서 지양하는 분기처리를 대부분 다형성으로 처리할 수 있지 않았나요?


하지만 상태전이에 대한 요구사항이 일정 값에 의해 고정적으로 전이되는 구조 였다면, State-driven 은 좋은 선택이 되지 못했을 것입니다.

고정적으로 전이한다는 것은 NPC::generateFeeling 와 같은 구현내용이 어딘가에 꼭 존재해야함을 의미합니다. 

그렇기 때문에 Context 인 NPC 클래스에서 주체적으로 상태를 변경했었죠..


즉, 어떤 특정방식이 좋다는 것이 아닌 프로그래머가 요구사항 따라 적절한 방법을 사용해야합니다.



5. 열거형을 통한 Bird-Eyes-View 확보.


자바의 열거형(Enum)을 이용하면, 꽤 재미있는 것을 많이 해볼 수 있는 것 같습니다.


전략패턴 역시, 열거형을 이용하여 구현을 할 수 있었죠.

상태패턴은 구조적으로 전략패턴과 비슷하기 때문에 이 역시 열거형을 이용하여 구현할 수 있습니다.


이번 절에서는 앞서 구현했던 상태들을 열거형으로 구현해보는 것을 해볼 생각입니다.

물론 열거형으로 구현하는데 있어, [Context-driven, State-driven] 모두 가능합니다. 



- Context-driven 구현


먼저 구현해볼만한 사항은 Context-driven 입니다.

Context-driven 방식은 Context 가 상태를 전이하기 때문에 이에 대해 고려를 하지 않아도 됩니다.

이 곳에서 해야할 일은 상태에 대하여, 위임받게 될 행위를 적절하게 구현하는 것이죠.


이에 대한 구현 사항은 아래와 같습니다.


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
/**
 * 호감도에 대한 상태 열거형 정의
 *
 * Created by Doohyun on 2017. 7. 16..
 */
public enum Feeling {
 
    GOOD {
        @Override
        public void talkAction(String playerName, String name) {
            System.out.printf("%s씨와 대화는 언제나 재미있어요. (%s는 나를 좋아하는 것 같다.)\n", playerName, name);
        }
 
        @Override
        public void giftAction(String playerName, String name) {
            System.out.printf("%s씨 감사해요. 이건 제가 사는 커피입니다. (%s 에게서 '아메리카노'를 획득했다.)\n", playerName, name);
        }
    },
 
    NOT_GOOD {
        @Override
        public void talkAction(String playerName, String name) {
            System.out.printf("%s씨, 죄송하지만 지금 좀 바빠서요. (%s는 나와의 대화가 지루해보인다.)\n", playerName, name);
        }
 
        @Override
        public void giftAction(String playerName, String name) {
            System.out.printf("이런 선물은 조금 부담이 느껴지네요.. (%s는 내 선물을 부담스러워하는 것 같다.)\n", name);
        }
    };
 
    /**
     * 감정에 따라 대화하는 액션 추상화.
     *
     * @param playerName
     * @param name
     */
    public abstract void talkAction(String playerName, String name);
 
    /**
     * 감정에 따라 선물에 대한 액션을 추상화
     *
     * @param playerName
     * @param name
     */
    public abstract void giftAction(String playerName, String name);
}
cs


전략패턴에서 다뤘던 열거형을 이용한 방식과 크게 다르지 않습니다.

Context 에서 사용할 메소드의 서명에 위임할 행위를 상태마다 적절하게 구현하였습니다.



- State-driven 구현


State-driven 의 경우 구체화된 상태 클래스에서 Context 의 상태를 변경해야 합니다.

기존 구현 내용의 경우 Context 를 has-a 관계로 가지고 있었지만, 열거형에서는 아쉽게도 동적인 상태를 가질 수는 없습니다.


하지만, 안되는 것은 없습니다! :-)

각 열거형의 메소드 서명에는 Context 를 파라미터로 받는 방식으로 우회하면 되죠.


이를 반영한 열거형은 다음과 같습니다.


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
/**
 * 호감도에 대한 상태 열거형 정의
 *
 * Created by Doohyun on 2017. 7. 16..
 */
public enum Feeling {
 
    Normal {
        @Override
        public void talkAction(NPC npc, String playerName) {
            // 상태 변경 처리.
            {
                if (npc.getTalkCount() > 20 && npc.getGiftCount() > 10) {
                    // 대화 횟수가 20 초과 선물 횟수가 10 초과일 때,
                    // 기본 상태이면 호감상태로 변경하는 로직 구현
                    npc.toChangeGoodFeeling();
                }
            }
 
            // NPC 의 이름은 이미 has-a 로 포함하는 NPC 객체에서 가져올 수 있다.
            System.out.printf("%s씨 다음에 봐요. (%s와의 대화를 마쳤다.)\n", playerName, npc.getName());
        }
 
        @Override
        public void giftAction(NPC npc, String playerName) {
            System.out.printf("%s씨 담에 커피 한잔해용. (%s와의 대화를 마쳤다.)\n", playerName, npc.getName());
        }
    };
 
    /**
     * 감정에 따라 대화하는 액션 추상화.
     *
     * @param npc
     * @param playerName
     */
    public abstract void talkAction(NPC npc, String playerName);
 
    /**
     * 감정에 따라 선물에 대한 액션을 추상화
     *
     * @param npc
     * @param playerName
     */
    public abstract void giftAction(NPC npc, String playerName);
}
 
cs



이로써 열거형으로 상태패턴을 구현하는 방법을 알아 보았는데요.

열거형을 이용하여 상태패턴을 구현하면, 제 생각에는 두 가지 정도의 장점이 있는 듯 합니다.


- 생산성


열거형을 이용하면, 굳이 클래스들을 많이 만들지 않아도 됩니다.

열거 타입만 추가해주면 되죠.


이는 꽤 생각보다 생산성을 올려줍니다.


- Bird-Eyes-View(조감도법) 확보


상태패턴의 각 상태는 다른 형제 상태들 사이의 관계에 있어서 꽤 의존적일 수 있습니다.


물론, 앞써 살펴본 예제에는 물리적으로 서로의 영역을 침범하지 않도록 Context 에서 상태전이를 직접적으로 수행하는 메소드들 제공 했었지만, 논리적인 의존성은 어쩔 수 없는 듯 합니다.


클래스의 개수가 많아졌다면, 그만큼 상태간의 논리적인 관계를 보기가 쉽지 않을 것입니다.

하지만, 열거형의 한 파일안에 모든 상태가 있고 이를 관리한다면 상태들의 관계를 살펴보기 훨씬 좋을 듯 합니다.


이는 단지 열거형뿐 아니라, 복잡한 VO 를 제작함에 있어서 각 변수들의 클래스를 내부 클래스로 선언함으로써 같은 효과를 볼 수 있습니다.


복잡한 관계들일 수록 조감도법은 꽤 유지보수에 도움이 될 것입니다.



6. 패턴 비교 (전략패턴 vs 상태패턴)


이번 리뷰에서는 계속하여 상태패턴 외에 전략패턴을 언급 했었습니다.


상태패턴과 전략패턴 모두 특정 주제가 어떤 조건에 의해 해야할 행위를 다형성을 이용하여 위임한다는 공통점을 가지고 있습니다.


즉, UML 을 비교한다면 두 패턴은 크게 다르지 않을 것입니다.


하지만 사용성 면에서 차이가 존재합니다.


전략패턴의 경우 '특정 메소드의 전략을 동적으로 변경하길 바라며, 이는 외부요인에 의해서 원할 때 변경'합니다.


상태패턴의 경우는 종종 한 주체(Context)가 제공하는 꽤 많은 기능들에 영향을 주며, 외부요인보다는 상태 스스로 변경되는 경우가 많습니다. 또한 전략 패턴에 비해 꽤 잦은 전이가 일어납니다.


그렇다고 '이 것은 상태패턴이야, 저 것은 전략패턴이야' 라고 외울 필요는 없을 듯 합니다. 


공부의 목적은 선대로부터 OOP 적인 사고를 배우며 더 좋은 기술을 만들기 위해서지, 코드 평론가가 되기 위한 것은 아니니까요..



생각보다 꽤 긴 포스팅이 된 것 같습니다.


그 이유는 이번 포스팅은 꽤 작성하기 어려웠고, 그에 따라 생각이 많아졌기 때문인 듯 합니다.


특히, 이번에는 상태패턴 자체보다는 언제나 후배들에게 강조하던 분기처리에 대한 입장을 다뤄보고 싶었습니다.

언제나 [분기처리에 대한 경계]를 말하곤 했지만 그 이유에 대해서는 깊게 생각해보지 못했었고, 이번 포스팅을 계기로 그에 대한 고민을 많이 해보는 기회가 되었던 것 같습니다.

하지만, 글에 담겨져 있는지는 모르겠습니다. ^^;


마지막으로 상태패턴에 대한 재미있는 예제를 생각하지 못해 고민을 하던 중, 적절한 아이디어를 제공해준 '부사수 형오리' 에게 감사를 표하며 이번 포스팅을 마칩니다.


이 글이 읽는 분들에게 도움이 되었으면 좋겠습니다.

반응형
Posted by N'

[사내 선배 개발자]분들 모두 잘하시지만(저는 언제쯤 잘할 수 있을까요? ㅜㅡㅜ), 

그 중 저에게 있어서 가장 믿을 수 있는 파트너였고, 간접적인 스승(직접적으로 뭔가 교육 등을 받지는 않았으니...)이신 선배님이 한 분 계십니다.


그 분과 디자인패턴 이야기가 종종 나올 때면, 언제나 끝에는 이 말로 종결을 짓곤 합니다.


"모든 디자인패턴은 Command 패턴을 구현하기 위해 존재할 뿐이야..."


이번 포스팅은 이 선배님께서 가장 강조하던 Command(명령)패턴에 관하여 다뤄보려고 합니다.


명령 패턴의 주 목적은 이름 그대로, 요청(명령)에 대한 캡슐화입니다.


이 패턴은 실제 요청자(Invoker)와 그 연산을 실제로 실행하는 자(Receiver) 사이에 일종의 중계자(Controller)를 두고 중계자가 캡슐화한 요청(명령)을 관리-실행을 하는 구조(has-a)로, 이는 요청자와 실행자 사이의 관계를 느슨하게 하는 것을 목표로 합니다.


언제나처럼 요구사항을 풀어가며 예제를 해결해보죠.



1. 요구사항


귀사에서 현재 제작하고 관리하는 어플리케이션 중 하나는 Cafe 관리 솔루션입니다.

Cafe 관리 솔루션은 "Cafe 내에서 사용하는 많은 infra 의 작동상태를 관리" 하는 것을 목적으로 하였으며, 꽤 많은 회사에서 이 솔루션을 사용하고 있습니다.


그렇기 때문에, Cafe 관리 솔루션의 요구사항은 끊기지 않는 것 같습니다. @.@


[욕심많은 기획자]는 다양한 기기를 관리할 수 있는 만능 리모콘 제작을 의뢰했습니다.

만능 리모콘에는 오직 한개의 버튼밖에 없지만, Cafe 에서 사용하는 많은 infra 의 on/off 를 오직 이 버튼만으로 관리하길 기대합니다.


여기서 중요한 것은 아직 우리는 관리할 모든 infra 를 받지 못했습니다. ㅜㅡㅜ



2. 다양한 infra, 더욱 다양한 서명들.


먼저 관리요청을 받은 infra 의 상태를 먼저 체크해 볼 필요가 있습니다.

각 infra 들은 많은 사람들에 의해서 제작이 되었고, 그렇기 때문에 당연하게도 메소드의 서명들이 모두 다릅니다.


또한, 버튼을 누를 때마다 해야할 일이 한 개는 아닌 것 같습니다.

Ex. MusicPlayer 의 경우, 버튼에 의해 활성화할 시 해야할 일은 아마도 MusicPlayer::on, MusicPlayer::startMusic 정도 일 것입니다.


하지만 더 안타까운 것은 추가 될 infra 역시 다양한 메소드 서명과 다양한 룰이 등장할 것이란거죠.



3. 요청의 캡슐화 및 컨트롤


메소드 서명과 사용 방법이 모든 infra 마다 다르다는 것은 꽤 골치 아픈 문제처럼 보입니다.

또한 만능 리모콘은 요청을 처리하기 위해서 infra 의 많은 정보를 알아야하는 것은 좋아 보이지 않습니다.

(만능 리모콘이 infra 가 추가될 때마다 해당 정보를 알기 위해 수정을 해야합니다.)


하지만 요청에 대한 일련의 복잡한 과정을 만능 리모콘에 맞게 '버튼'이라는 하나의 인터페이스로 단순화한다면 간단한 문제가 될 수 있을지도 모릅니다. 

단순화는 만능 리모콘이 요청을 처리하기 위해서 infra 의 정보를 몰라도 됨을 의미합니다.


우리는 이와같은 문제를 한 번 풀어본 적이 있습니다.

기억이 나나요? 맞습니다. 바로 Facade 의 개념을 도입해보죠.


Facade 패턴이 궁금하다면 아래 포스팅을 참고! :-)



만능 리모콘의 요청에 맞춰 각 infra가 해야할 일을 다음과 같이 단순화를 하고자 합니다.


1
2
3
4
5
6
7
8
9
10
11
/**
 * 리모콘이 할 수 있는 인터페이스 정의
 *
 * Created by Doohyun on 2017. 7. 6..
 */
public interface ICommand {
    /**
     * 실행
     */
    void execute();
}
cs



이에 따라 요청에 따라 infra 가 해야할 동작을 담을 ConcreteCommand 클래스를 만들어보죠.


아래는 MusicPlayer 와 MusicPlayer 에 대한 on/off 요청을 캡슐화한 클래스입니다.

요청을 캡슐화한 클래스에서는 요청전략에 따라 해야할 일련의 과정을 수행하도록 하였습니다.


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
/**
 * 음악 플레이어
 *
 * <pre>
 *     실제 액션을 하는 객체를 의미
 *     receiver.
 * </pre>
 *
 * Created by Doohyun on 2017. 7. 6..
 */
public class MusicPlayer {
    
    public void on() {
        System.out.println("음악플레이어 전원을 올린다.");
    }
 
    public void startMusic() {
        System.out.println("음악 재생");
    }
 
    public void endMusic() {
        System.out.println("음악 끄기");
    }
    
    public void off() {
        System.out.println("음악플레이어 끄기");
    }
}
 
/**
 * 음악플레이어 스위치를 켜는 명령 정의.
 *
 * Created by Doohyun on 2017. 7. 6..
 */
public class MusicPlayerOnCommand implements ICommand {
 
    private MusicPlayer musicPlayer;
 
    public MusicPlayerOnCommand(MusicPlayer musicPlayer) {
        this.musicPlayer = musicPlayer;
    }
 
    /**
     * 음악 전원을 올리고 재생한다.
     */
    @Override
    public void execute() {
        musicPlayer.on();
        musicPlayer.startMusic();
    }
}
 
/**
 * 음악플레이어 스위치를 끄는 명령 정의.
 * 
 * Created by Doohyun on 2017. 7. 6..
 */
public class MusicPlayerOffCommand implements ICommand{
    private MusicPlayer musicPlayer;
 
    public MusicPlayerOffCommand(MusicPlayer musicPlayer) {
        this.musicPlayer = musicPlayer;
    }
 
    /**
     * 음악을 끄고, 전원을 내린다.
     */
    @Override
    public void execute() {
        musicPlayer.endMusic();
        musicPlayer.off();
    }
}
cs



이제 앞써 구현한 명령(ICommand) 객체를 사용할 만능리모콘은 다음과 같이 제작해보겠습니다.


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
/**
 * 명령 객체를 관리하는 중계자.
 *
 * Created by Doohyun on 2017. 7. 6..
 */
public class RemoteController {
 
    private ICommand command;
 
    /**
     * 명령객체 세팅.
     *
     * @param command
     */
    public void setCommand(ICommand command) {
        this.command = command;
    }
 
    /**
     * 실행
     */
    public void execute() {
        Optional.ofNullable(command).ifPresent(ICommand::execute);
    }
}
cs


RemoteController 는 ICommand 를 has-a 관계로 취하고 있으며, 이는 infra 를 실행함에 있어서 어떤 정보도 알 필요가 없음을 의미합니다.


즉, 캡슐화된 요청은 일종의 전략이라고 볼 수 있으며 RemoteController 는 전략을 사용하고 있는 형태로 볼 수 있을 것 같습니다.


전략 패턴에 대한 내용은 아래에서 확인할 수 있습니다.



이제 구현된 내용을 테스트하는 코드를 간단히 작성해보겠습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Receiver
MusicPlayer musicPlayer = new MusicPlayer();
 
// Invoker 와 Receiver 사이의 Controller
RemoteController controller = new RemoteController();
 
// 음악플레이어 켜기. (invoke type 1)
controller.setCommand(new MusicPlayerOnCommand(musicPlayer));
controller.execute();
 
// 음악플레이어 끄기. (invoke type 2)
controller.setCommand(new MusicPlayerOffCommand(musicPlayer));
controller.execute();
 
// CONSOLE LOG
// 음악플레이어 전원을 올린다.
// 음악 재생
// 음악 끄기
// 음악플레이어 끄기
cs


이 구조에서는 만약 새로운 infra 가 생긴다 하더라도, 

ICommand 를 구현하는 요청객체를 만들어 RemoteController 에 세팅하면 특별히 다른 부분을 수정할 필요가 없어 보입니다. (OCP)


현재 구조를 UML 로 표현해보면 다음과 같습니다.



이 구조를 많이 보지 않았나요?

상위 모듈인 Controller 와 하위모듈인 ConcreteCommand 들은 모두 추상적인 ICommand 에 의존하고 있습니다. (DIP)


이 것에 대해 존경하는 또 다른 선배님의 말씀을 빌리면,


모든 패턴의 UML 을 그려보면 다 똑같아....


라고 하고 싶군요.



3. 명령 패턴 응용.


명령 패턴의 또 다른 묘미는 명령 객체를 관리할 수 있다는 것입니다.


Controller 에서 명령 객체를 has-a 관계로 유지하며 리하는 방식을 목적에 맞게 구현함으로써, undo/redo, macro 등을 개발해 볼 수 있습니다.


이러한 기능들은 특히, invoker 입장에서 특정 행위를 receiver 를 이용해 하기 위해 정보를 유지해야하는 불편함을 덜어줄 수 있을 것입니다.


이번 절에서는 명령 패턴의 대표적인 활용 예시인 UNDO/REDO 를 구현해보겠습니다.


이를 위해 앞써, 구현한 interface 의 명세를 조금 수정할 생각입니다.

특정 요청에 대한 행위를 취소하는 기능인 undo 를 추가하기 위해서죠..


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
 * undo 를 지원하는 명령인터페이스
 *
 * Created by Doohyun on 2017. 7. 6..
 */
public interface ICommand {
 
    /**
     * 실행취소
     */
    void undo();
 
    /**
     * 실행
     */
    void execute();
}
cs



이를 구현하는 ConcreteCommand 객체는 다음과 같습니다.

이번에는 특별하게 상태가 있는 요청입니다.


POSS 를 켜기 위해서는 id, pwd 가 필요하며, 요청객체에서 상태를 유지하도록 하였습니다.


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
/**
 * 포스를 작동시키는 커맨드 구현.
 *
 * Created by Doohyun on 2017. 7. 7..
 */
public class POSSOnCommand implements ICommand {
 
    private String id;
    private String pwd;
    private POSS poss;
 
    /**
     * 포스를 켜는 커맨드 구현.
     *
     * <pre>
     *     상태를 유지함으로써, 
     *     invoker 는 요청을 한번 할 때를 제외하고는 해당 정보를 유지할 필요가 없음.
     * </pre>
     *
     * @param poss
     * @param id
     * @param pwd
     */
    public POSSOnCommand(POSS poss, String id, String pwd) {
        this.poss = poss;
        this.id = id;
        this.pwd = pwd;
    }
 
    @Override
    public void undo() {
        poss.logout();
        poss.closeSystem();
    }
 
    @Override
    public void execute() {
        poss.pushStartButton();
        poss.login(id, pwd);
    }
}
cs



다음 수정을 해볼 부분은 RemoteController 입니다. 

Stack 두 개를 목적에 따라 분류하여 명령 객체를 관리하고 있으며, RemoteController::execute, RemoteController::undo 가 실행될 때마다 적절하게 명령들을 이동시키고 있습니다.


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
/**
 * 컨트롤러 undo/redo 지원
 *
 * Created by Doohyun on 2017. 7. 6..
 */
public class RemoteController {
 
    // 일반 명령을 위한 스택
    private Stack<ICommand> commandStack = new Stack<>();
    // UNDO 명령을 위한 스택
    private Stack<ICommand> undoStack = new Stack<>();
 
    // 명령을 추가
    public void setCommand(ICommand commandWithUndoable) {
        commandStack.push(commandWithUndoable);
    }
 
    /**
     * 일반 적인 실행. (REDO 포함)
     */
    public void execute() {
        if (!commandStack.isEmpty()) {
            // [일반명령 스택]에서 가장 마지막에 담긴 명령객체를 추출 후 실행.
            ICommand command = commandStack.pop();
            command.execute();
 
            // 해당 명령을 UNDO 스택에 삽입.
            undoStack.push(command);
        }
    }
 
    /**
     * 작업 취소 (Undo)
     */
    public void undo() {
        if (!undoStack.isEmpty()) {
            // [UNDO 명령 스택]에서 가장 마지막에 담긴 명령객체를 추출 후 실행.
            ICommand command = undoStack.pop();
            command.undo();
 
            // 일반 실행 스택에 데이터 삽입.
            commandStack.push(command);
        }
    }
}
 
cs



자, 코드가 수정 되었으니 마찬가지로 테스트 코드를 또 만들어보죠.

포스팅에서 제공은 안하지만 [커피머신 작동 명령] 까지 동시에 같이 세팅해 보도록 하였습니다.


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
CoffeeMachine coffeeMachine = new CoffeeMachine();
POSS poss = new POSS();
 
RemoteController controller2 = new RemoteController();
 
// 포스 작동 명령 세팅.
controller2.setCommand(new POSSOnCommand(poss, "Doohyun","486"));
 
controller2.execute();
controller2.undo();
controller2.execute();
 
// 커피머신 작동 명령 세팅
controller2.setCommand(new CoffeeMachineOnCommand(coffeeMachine));
controller2.execute();
controller2.undo();
controller2.undo();
controller2.execute();
 
// CONSOLE LOG
// 포스 켜기
// 로그인 : Doohyun, 486
// 로그아웃
// 포스 끄기
// 포스 켜기
// 로그인 : Doohyun, 486
// 커피기계 스위치 켜기
// 커피기계 스위치 끄기
// 로그아웃
// 포스 끄기
// 포스 켜기
// 로그인 : Doohyun, 486
cs


다행히, 실행/실행취소가 적절하게 잘 작동하는 것처럼 보입니다.

특히 주목할 점은 POSS 의 재실행 시, 상태인 id/pwd 를 다시 입력할 필요가 없다는 것입니다.

이는 invoker 입장에서 이와 같은 요구사항 처리 시, 정보를 계속 유지할 필요가 없음을 의미합니다.



이번 포스팅에서 다룬 명령 패턴은 '요청의 캡슐화' 라는 특정 목적을 가지고 있지만, 사실 여태 살펴본 다른 패턴들과 크게 다르지는 않은 듯 합니다.


특정 요청에 대한 복잡한 일련의 과정을 단순화한 전략에 따른 행위를 하도록 다형성을 이용했으며, 각 컴포넌트간의 관계를 느슨하게 위해 SOLID 의 두-세가지(SRP, OCP, DIP) 정도를 충족하도록 적용한 구조입니다.


패턴에 대한 이해도 중요하지만, 여러 패턴들 속에서 반복적으로 나타나는 이러한 특징들을 계속 접해보는 것도 도움이 되지 않을까 생각이 듭니다. :-)

감사합니다.



- 추가 사항 (2017.09. 10)


명령 패턴에 대한 실습자료는 아래와 같습니다. 감사합니다. ^^


CommandHomeWork.zip


반응형
Posted by N'

최근 사내의 선배에게 소프트웨어 개발에 대한 이야기를 하던 중 다음과 같은 이야기가 있었습니다.


"개발은 혼자할 수 있지만, 소프트웨어는 다수에 의해 만들 수 밖에 없는 듯...."


작은 규모의 프로젝트라면 슈퍼 개발자 혼자서 요구사항의 범위를 모두 충족할 수 있지만,

어느정도 중소 이상 급의 규모의 프로젝트라면 협업은 불가피해 보입니다.

(사실, 개발 뿐 아니라 모든 일이 그런 듯.... ㅡㅡ^)


그렇기 때문에, 개발자들은 타인에 의해서 작성된 모듈을 많이 사용하며 또한 본인이 만든 모듈 역시 배포를 해야할 것입니다.


앞써, 우리는 타에 의해서 배포된 모듈을 본인이 작성한 인터페이스 형식으로 변경하기 위한 패턴을 배웠습니다. 

네, 맞습니다. 바로 Adapter 패턴입니다. :-)



이번에는 반대로 본인이 만든 어떤 소프트웨어의 복잡한 모듈에 대하여 [간략화 된 인터페이스] 를 제공함으로써, 라이브러리 사용자들이 쉽게 사용하며 이해할 수 있도록 도움을 주는 Facade 패턴에 대해 다뤄보려고 합니다.

(Facade 는 "건물의 정면"을 뜻합니다.)


Facade 패턴의 목표는 자주 사용하는 일련의 공통적인 작업에 대한 편의 메소드를 제공해주는 것이며, 이번 리뷰에서는 이해를 위해 간단한 코드를 제공할 생각입니다.


언제나처럼 첫 시작은 요구사항입니다.


1. 요구사항


신입 IT 기술 매니저인 당신은 Cafe 의 인프라를 관리하는 부서로 발령을 받았습니다.


이 부서에서는 Cafe 의 여러 시스템들을 관리하는 프로그램을 배포하고 있으며, 

주 목적은 Cafe 개업 시작과 종료 시 [전등, 영업기계(POSS), 커피머신 세팅, 음악볼륨 조절] 등의 인프라를 끄고 키는 것을 관리하는 시스템을 목적으로 합니다.


이 과정에는 일련의 방법 및 매뉴얼(예를들어 커피머신은 영업 시작 1시간 전에는 미리 켜야 합니다.)이 존재하며, 매번 고객사에 판매할 때마다 이 작업을 교육 시켜야 합니다.


OOP 지식의 전문가인 당신은 반복적인 룰(Rule)을 모든 모듈 사용자가 숙지할 필요가 없다고 생각하고 있으며, 개선방법을 고려해보고자 기존 모듈들을 살펴보고 적절한 방법을 찾고자 합니다.



2. 이미 만들어진 복잡한 API


모듈을 확인해본 결과,


모든 인프라(전등, POSS, 커피머신 등)들은 단순하게 개업 시작, 종료에만 사용하는 것이 아닌 여러 상황(Event)의 변화에 따라 해야할 행위들이 존재했습니다.

(개업 시작, 종료 역시 큰 범위에서는 Event 의 한 종류라고 생각할 수 있겠군요.)


예를들어, 살펴본 CoffeeMachine 인프라 클래스입니다.


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
/**
 * 커피 머신.
 *
 * Created by Doohyun on 2017. 7. 2..
 */
public class CoffeeMachine {
    /**
     * 커피 머신 스위치 올리기.
     */
    public void clickOnSwitch(){
        System.out.println("커피머신 스위치 올리기");
    }
 
    /**
     * 커피 머신 스위치 내리기.
     */
    public void clickOffSwitch(){
        System.out.println("커피머신 스위치 내리기");
    }
 
    /**
     * 스팀 기계 체크.
     */
    public void checkSteam() {
        System.out.println("스팀 체크");
    }
 
    /**
     * 에스프레소 필터 체크
     */
    public void checkEspressoShopFilter() {
        System.out.println("에스프레소 필터 체크");
    }
 
    /**
     * 커피콩 추가.
     */
    public void addCoffeeBean() {
        System.out.println("커피콩 추가");
    }
    
    // 이하 생략...  이 클래스의 행위는 너무 많음 ㅡㅡ^
}
 
cs



문제는 모든 인프라 클래스들이 행위가 너무 많다는 것이며, 

모든 기능이 해당 클래스가 책임져야 할 문제이기 때문에 메소드 추출 및 이동과 같은 리팩토링을 하는 것은 오히려 API 의 혼란을 가져올 여지가 있어 보입니다. 


이미 모든 고객사들은 특정 상황에 따라 문서 및 교육에 따라 아래처럼 모듈을 사용하고 있습니다.

 

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
/**
 * 스타벅스 카페
 *
 * Created by Doohyun on 2017. 7. 2..
 */
public class StarbucksCafe {
 
    private CoffeeMachine coffeeMachine;
    private MusicPlayer musicPlayer;
    private Poss poss;
 
    private String masterId = "MASTER_ID";
    private String masterPwd = "MASTER_PWD";
 
    public StarbucksCafe() {
        this.coffeeMachine = new CoffeeMachine();
        this.musicPlayer = new MusicPlayer();
        this.poss = new Poss();
    }
 
    /**
     * Event 1 : 카페 오픈.
     */
    public void open() {
        
        // STEP1 커피 머신 켜기.
        {
            this.coffeeMachine.clickOnSwitch();                     // 커피머신 스위치 켜기
            this.coffeeMachine.checkSteam();                        // 스팀 체크
            this.coffeeMachine.checkEspressoShopFilter();           // 에스프레소 샷 필터 체크
            this.coffeeMachine.addCoffeeBean();                     // 커피콩 추가
        }
 
        // STEP2 포스 켜기
        {
            this.poss.clickOnSwitch();                              // 포스켜기
            this.poss.inputIdentity(masterId, masterPwd);           // 포스 로그인
        }
 
        // STEP3 음악 켜기
        {
            this.musicPlayer.clickOnSwitch();                       // 음악플레이어 켜기
            this.musicPlayer.loginToMelon(masterId, masterPwd);     // 멜론 로그인
            this.musicPlayer.runMusic();                            // 음악켜기
        }
    }
 
    /**
     * Event 2 : 카페 종료.
     */
    public void close() {
        
        // STEP1 커피 머신 정리
        {
            this.coffeeMachine.checkEspressoShopFilter();           // 에스프레소 샷 필터 체크
            this.coffeeMachine.checkSteam();                        // 스팀 체크
            this.coffeeMachine.clickOffSwitch();                    // 커피머신 스위치 끄기
        }
 
        // STEP2 음악 끄기
        {
            this.musicPlayer.logoutFromMelon();                     // 멜론 로그아웃
            this.musicPlayer.clickOffSwitch();                      // 음악플레이어 끄기
        }
        
        // STEP3 포스 끄기
        {
            this.poss.logout();                                     // 포스 정산 및 로그아웃
            this.poss.clickOffSwitch();                             // 포스 종료
        }
    }
}
 
cs


STARBUCKS 카페의 경우 문서에 따라, 잘 정리하여 운영을 하고 있습니다.

문서가 잘 정리되어 있고, 고객사의 개발자들이 그에 따라 잘 개발해준다면 문제가 없어보입니다.

(위와 동일한 코드를 작성해주면 되니, 아무 문제가 없을수도 있습니다.)


하지만 위와 같은 복잡한 API 를 모든 개발사의 개발자가 알고 있어야 한다는 것은, 그만큼의 API 에 대한 문서 및 교육이 필요함을 의미합니다. 


또한, 우리가 제공하는 API 가 기능추가를 의도할 때(예를들어 새로운 infra 가 추가 되었고, 이는 모든 Cafe 의 카페 오픈 하는 비지니스에 포함이 되어야할 경우), 모든 고객사에게 이를 알려야 하며 잘 사용하고 있는지에 대한 검토 또한 필요할 수 있습니다.

(A/S 가 부실하면, 고객사를 잃을 수도 있겠죠? ㅡㅡ^)


즉, 고객사의 편의를 위한 새로운 방법이 필요할 수 있어 보입니다.

 


3. 포장(wrapping) 을 통한 편리한 API 사용.


프로그래밍 기법 중에 우리는 포장(Wrapping)을 하는 경우는 흔히 볼 수 있습니다.


대표적인 것이 Java 의 Number 관련(Integer, Double...) Wrapper 클래스들입니다. 

이 Wrapper 클래스들은 원시타입인 int, double 등에 대하여 Boxing 과정을 통해 클래스로써의 특성을 가질 수 있도록 해줍니다.


즉, 기존 어떤 상태를 특정 목적에 따라 포장하는 것을 Wrapping 이라고 칭할 수 있을 것 같으며, 우리는 이 과정을 알게 모르게 많이 배웠습니다.


- Adapter 패턴


이미 "제공되는 것" 과 "필요한 것" 의 차이를 극복하기 위해, 타모듈의 객체를 본인에게 유리한 인터페이스로 Wrapping 합니다.


- Decorator 패턴


주어진 상황에 따라 기존 객체에게 책임이나 속성을 덧붙이기 위해, 장식으로 대상객체를 Wrapping 합니다. 

이는 상속을 통해서만 책임을 확장할 수 있던 방법의 대안이 되었습니다.


Decorator 패턴에 대한 리뷰는 아래에서 참고! :-)



Facade 패턴 역시 Wrapping 을 사용하는 방법으로, 

복잡하게 제공할 수 밖에 없는 API 들을 일종의 매뉴얼화 시킨 객체를 제공함으로써 라이브러리 사용을 극대화시키도록 합니다.


즉, 매뉴얼을 코드화 시켰다고 생각하는 것이 좋겠군요.

StarbucksCafe 에서 제작하도록 지시했던 메뉴얼을 다음과 같은 Wrapping 클래스로 제공해보죠.


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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
/**
 * 카페 운영을 위한 파사드 정의.
 *
 * Created by Doohyun on 2017. 7. 2..
 */
public class CafeOperateFacade {
    private CoffeeMachine coffeeMachine;
    private MusicPlayer musicPlayer;
    private Poss poss;
 
    private String masterId;
    private String masterPwd;
 
    public CafeOperateFacade(String masterId, String masterPwd) {
        this.coffeeMachine = new CoffeeMachine();
        this.musicPlayer = new MusicPlayer();
        this.poss = new Poss();
 
        this.masterId = masterId;
        this.masterPwd = masterPwd;
    }
    
    //  직접적인 컴포넌트 사용을 위해, GETTER 를 제공.
 
    /**
     * 카페 머신 출력.
     * 
     * @return
     */
    public CoffeeMachine getCoffeeMachine() {
        return coffeeMachine;
    }
 
    /**
     * 음악 플레이어 출력.
     * 
     * @return
     */
    public MusicPlayer getMusicPlayer() {
        return musicPlayer;
    }
 
    /**
     * 포스 출력.
     * 
     * @return
     */
    public Poss getPoss() {
        return poss;
    }
 
    /**
     * Id 출력.
     * 
     * @return
     */
    public String getMasterId() {
        return masterId;
    }
 
    /**
     * 패스워드 출력.
     * 
     * @return
     */
    public String getMasterPwd() {
        return masterPwd;
    }
 
    /**
     * Event 1 : 카페 오픈.
     */
    public void open() {
 
        // STEP1 커피 머신 켜기.
        {
            this.coffeeMachine.clickOnSwitch();                     // 커피머신 스위치 켜기
            this.coffeeMachine.checkSteam();                        // 스팀 체크
            this.coffeeMachine.checkEspressoShopFilter();           // 에스프레소 샷 필터 체크
            this.coffeeMachine.addCoffeeBean();                     // 커피콩 추가
        }
 
        // STEP2 포스 켜기
        {
            this.poss.clickOnSwitch();                              // 포스켜기
            this.poss.inputIdentity(masterId, masterPwd);           // 포스 로그인
        }
 
        // STEP3 음악 켜기
        {
            this.musicPlayer.clickOnSwitch();                       // 음악플레이어 켜기
            this.musicPlayer.loginToMelon(masterId, masterPwd);     // 멜론 로그인
            this.musicPlayer.runMusic();                            // 음악켜기
        }
    }
 
    /**
     * Event 2 : 카페 종료.
     */
    public void close() {
 
        // STEP1 커피 머신 정리
        {
            this.coffeeMachine.checkEspressoShopFilter();           // 에스프레소 샷 필터 체크
            this.coffeeMachine.checkSteam();                        // 스팀 체크
            this.coffeeMachine.clickOffSwitch();                    // 커피머신 스위치 끄기
        }
 
        // STEP2 음악 끄기
        {
            this.musicPlayer.logoutFromMelon();                     // 멜론 로그아웃
            this.musicPlayer.clickOffSwitch();                      // 음악플레이어 끄기
        }
 
        // STEP3 포스 끄기
        {
            this.poss.logout();                                     // 포스 정산 및 로그아웃
            this.poss.clickOffSwitch();                             // 포스 종료
        }
    }
}
 
cs


위와 같이, 공통적으로 사용할 법한 기능들을 간편한 API 로 제공하였습니다.

또한, 각 인프라 역시 직접적으로 사용할 수 있도록 Getter 메소드를 제공하고 있습니다.


공통적으로 사용할 법한 기능들은 각 Cafe 클래스에서 사용하고, 특수한 기능들은 알아서 직접적으로 모듈을 사용하라고 열어둔 셈입니다.


자, Facade 를 제공함으로써 각 고객사 모듈이 어떻게 변했는지 확인해볼까요?


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
/**
 * 스타벅스 카페
 *
 * Created by Doohyun on 2017. 7. 2..
 */
public class StarbucksCafe {
 
    private CafeOperateFacade cafeOperateFacade;
 
    private String masterId = "MASTER_ID";
    private String masterPwd = "MASTER_PWD";
 
    public StarbucksCafe() {
        this.cafeOperateFacade = new CafeOperateFacade(masterId, masterPwd);
    }
 
    /**
     * 카페를 오픈한다.
     */
    public void open() {
        cafeOperateFacade.open();
    }
 
    /**
     * 카페를 닫는다.
     */
    public void close() {
        cafeOperateFacade.close();
    }
}
cs


복잡했던, 모듈이 매우 심플(Simple) 해졌군요. :-)

복잡한 API 문서를 정성들여 제공하는 것보다, 쓰기 좋은 라이브러리를 제공해주는 편이 고객사 입장에서는 더 좋겠죠?


또한, 우리는 고객사의 코드를 관리할 수 있습니다.

예를들어 우리는 라이브러리의 CafeOperateFacade::open 을 수정하면, 모든 고객사들의 소스에 수정사항에 대한 영향을 줄 수 있습니다.


그렇지만 이는 사실 단순하게 좋다고만 볼 수는 없습니다. 

미치는 영향이 잘못하면 고객사들에게는 큰 피해가 갈 수도 있겠죠?



이번 리뷰에서 다룬, Facade 의 개념은 우리가 사용하는 많은 라이브러리에 적용되어 있습니다.


복잡한 Javascript 문법을 편리하게 하기 위하여 jQuery 가 생겼으며, JDBC 의 불편한 Mapping 과정 때문에 iBatis, Hibernate 등의 ORM 이 많이 사용되고 있습니다.


즉, 복잡한 라이브러리를 Wrapping 하여 편리한 API 만을 볼 수 있게함으로써 많은 개발자들의 개발기간을 감축(FACADE 의 최종 목적)시켰으며, 이는 OOP 의 개념에서 가장 큰 목적이 아닐까 생각을 합니다.


이 글이 도움이 되길 바랍니다. :-)











반응형
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'