Stream API 에 대한 공부는 잘하고 있나요?


조금만 익숙해지더라도, 비지니스 로직 작성에 있어서 꽤 큰 편리함을 느낄 수 있을 것입니다. 


Java8 의 핵심인 강력한 Stream API 의 두 번째 review 는 아래 글들을 바탕으로 합니다.

(물론, 지난 시간에 진행한 과제도 같이 보며 복습을 해보죠. ^^;)



언제나 그랬지만 기본서인 JAVA8 in Action 을 읽고 오는 것을 추천합니다. :-)



또한, (^^)


이번 절은 Stream API 의 꽃이었기 때문에, 실습이 있습니다. :-)


이번 숙제는 좀 특별합니다. 

실무와 아마 직접적인 연관이 있을 수도....


뭐, 실습에서 못하면 종종 숙제가 될지도? ㅜㅡㅜ


STUDY_OOP_FP_15.zip



반응형
Posted by N'

짧지만, 생각보다 별 것이 있었던 "메소드를 넘기는 방법" 에 대한 내용이 끝이 났습니다.

 

다음 스터디에서는

 

앞써 배운 이 방법을 적극적으로 이용하며,

JAVA8 부터 등장하여 함수형 프로그래밍을 할 수 있도록 해준, 강력한 Stream API 에 대해 리뷰해봅니다.

 

이 주제와 관련한 내용을 읽기 전에, 복습과 관련된 내용에 대한 글을 먼저 남깁니당. :-)

 

 

 

또한, 다음 시간에 다룰 내용에 대한 글 역시 남깁니당. :-)

 

 

 

이 포스팅을 읽는 것도 중요하지만, 언제나 기본서인 [JAVA8 in Action] 을 읽고 오는 것을 추천합니다.

(바쁘면 어쩔 수 없지만.....)


또한, 이번 실습 후 하게 될 일입니다.

언제나 배웠으면 해보는게 중요하겠죠? (숙제가 되지 않기를 바랍니다. @.@)


STUDY_OOP_FP_14.zip


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

함수형 프로그래밍을 시작하는 첫 장에서는 


- 함수란 무엇인가?


- 다형성(Polymorphism)의 필요성과 이를 이용하기 위한 OOP 의 한계


- 이를 극복하기 위한, 기존 Java8 이전의 방법과 편리해진 람다와 메소드 레퍼런스


정도의 내용을 다루었습니다. (위의 개념에 대해 생각이 안난다면, 꼭 복습을 권장합니다.)


이에 대한 포스팅 정보는 아래에서 확인 :-)



그리고 이를 실습해보기 위한 과제가 있었고, 이번 포스팅에서는 과제에 대한 리뷰를 해보고자 합니다.


1. SwitchAndSwitch


첫 번째 숙제는 람다에 대한 튜토리얼 진행을 해보고자 했던 내용이었습니다.


요구사항은 아래의 소스를 리팩토링하는 것이었습니다.

메소드 내부에 존재하는 코드들은 일련의 공통적인 작업들이 많이 보입니다.


보통은 이런 작업에 대해서 따로 메소드 추출 등의 리팩토링을 하겠지만, 이 로직이 오직 switchAndSwitch 메소드에서만 사용될 것 같았기 때문에 다른 방법을 찾아보자는 것이었습니다.

(추 후, 다른 로직에서도 사용이 된다면 그 때 캡슐화하는 것으로...)


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
public void switchAndSwitch() {
 
    List<GenerateModel1> generateModel1List = Model1Dao.GetInstance().selectList(Arrays.asList(1,2,3));
 
    // something work1
    {
        List<Integer> memberSnList = Arrays.asList(2,3);
 
        for (GenerateModel1 model1 : generateModel1List) {
             if (memberSnList.contains(model1.getMemberSubjectSn())) {
                String name = model1.getName();
 
                switch (name) {
                case "강현지":
                    System.out.println("IF 란 사치임을 증명한 " + name);
                    break;
                
                case "유덕형":
                    System.out.println("한 수에 버그를 말살하는 " + name);
                    break;
                case "유형주":
                    System.out.println("한 메소드에 5줄 이면 충분한 " + name);
                    break;
                }
            }
        }
    }
 
    // something work2
    {
        List<String> filterNameList = Arrays.asList("강현지""유덕형");
 
        for (GenerateModel1 model1 : generateModel1List) {
            if (filterNameList.contains(name)) {
                String name = model1.getName();
            
                switch (name) {
                case "강현지":
                    System.out.println("IF 란 사치임을 증명한 " + name);
                    break;
                
                case "유덕형":
                    System.out.println("한 수에 버그를 말살하는 " + name);
                    break;
 
                case "유형주":
                    System.out.println("한 메소드에 5줄 이면 충분한 " + name);
                    break;
                }
            }
        }
    }
}
cs


우리는 일련의 동작 즉 함수를 값으로 가질 수 있다는 개념을 알았고, 이를 이용해서 굳이 메소드 추출을 안하고 이 문제를 해결할 수 있었습니다.


네, 람다를 이용해보는 것이죠.


그 전에 프로그래밍 원칙 중 중요한 원칙이 한 개 있습니다.


"변하는 부분과 변하지 않는 부분을 분리하라!"


이 법칙에 근거했을 때, 저는 해당 로직에 대해 다음과 같이 정의를 했습니다.


- 변하는 부분 : GenerateModel1 에 대한 필터 로직

- 변하지 않는 부분 : Loop 를 돌며, 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
public void switchAndSwitch() {
 
    List<GenerateModel1> generateModel1List = Model1Dao.GetInstance().selectList(Arrays.asList(1,2,3));
 
    /**
     *  Predicate 를 받아, 일련의 공통작업을 수행하는 소비자.
     */
    Consumer<Predicate<GenerateModel1>> switchConsumer =
        (predicate) -> {
            for (GenerateModel1 model1 : generateModel1List) {
                
                if(predicate.test(model1)) {
 
                    String name = model1.getName();
                    
                    switch (name) {
                    case "강현지":
                        System.out.println("IF 란 사치임을 증명한 " + name);
                        break;
 
                    case "유덕형":
                        System.out.println("한 수에 버그를 말살하는 " + name);
                        break;
 
                    case "유형주":
                        System.out.println("한 메소드에 5줄 이면 충분한 " + name);
                        break;
                    }
                }
            }
        };
 
    // something work1
    {
        List<Integer> memberSnList = Arrays.asList(2,3);
 
        // 프리디케이트가 정의되는 부분이 변하는 부분.            
        switchConsumer.accept(model1 -> memberSnList.contains(model1.getMemberSubjectSn()));    
    }
 
    // something work2
    {
        List<String> filterNameList = Arrays.asList("강현지""유덕형");
 
        // 프리디케이트가 정의되는 부분이 변하는 부분.            
        switchConsumer.accept(model1 -> filterNameList.contains(model1.getName()));    
    }
}
cs


람다에 대한 첫 튜토리얼로 나름 나쁘지 않았다고 생각합니다. :-)


사실 개인적으로 람다표현식 보다도 위의 규칙이 더 중요하며, 저 규칙만 잘지켜도 좋은 프로그래머가 될 수 있을 것이란 생각이 드는군요.



2. Template-Method 의 극복? 이 방법은 정말로 좋은가?


동적 파라미터화를 이용하면, 굳이 클래스를 상속받아 구현하지 않아도 다형성을 사용할 수 있음을 알았습니다.


또한 JAVA8 에서는 기본 함수형 인터페이스까지 제공해주기 때문에 동적파라미터화를 하기 위해 따로 인터페이스를 제작하지 않아도 될 것 같아 보이는데요.


기본형 함수형 인터페이스에 대한 내용은 아래 포스팅에서 참고.



이 숙제는, 정말로 이 방법이 좋은 지에 대해 다뤄보는 내용이었습니다.


요구사항은 다음과 같았습니다.


ORM 으로 제작된 클래스들의 필드는 종종 비슷한 경우가 많지만, 아쉽게도 제너레이터에 의해 제작되기 때문에 이 곳을 수정하는 것은 문제가 있어보입니다.

(즉 상속 등 클래스 관계를 지어줄 수 없으며, 이는 꽤 골치아픈 문제가 될 수 있습니다.)


즉 이러한 이유로 다형성 등의 개념을 이용할 수 없는 것처럼 보이며, 이는 비슷한 로직도 재활용이 쉽지 않음을 의미합니다.


이를 극복하기 위한 여러가지 방법을 다뤘으며, 대표적인 방법 중 한 가지는 Template-Method 패턴을 이용해보는 것이었습니다.



하지만, 굳이 한 메소드 제작을 위해서 복잡한 클래스 구조를 가질 수 있어 보이는 Template-Method 를 사용하는 것은 부담이라 생각하였습니다.

(이런 생각은 귀차니즘에서 보통 비롯하곤 합니다. ㅡㅡ^)


그러던 중, 동적 파라미터화 및 기존 제공 함수형 인터페이스가 있는 것을 배웠고 이를 이용해 Template-Method 와 비슷한 효과를 낼 수 있을 것 같았습니다.


즉, 어떠한 인터페이스나 추상클래스를 만들지 않고 Template-Method 를 흉내내는 것이 이 과제의 목적이었습니다.


아래 소스는 과제로 제출해준 한 분의 소스입니다.

(제작해주셔서 감사합니다.^^)


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
/**
 * 제너레이트 된 모델에 대한 비지니스 로직 정의.
 *
 *
 * @param <G>
 * @param addTargetMemberSnList
 * @param excludeTargetMemberSnList
 */
public <G> void createByGenerateModel(
    List<Integer> addTargetMemberSnList
    , List<Integer> excludeTargetMemberSnList
    , Function<Integer, G> toGenerateModel
    , Function<List<Integer>, List<G>> selectList
    , Function<G, Integer> toMemberSubjectSn
    , Consumer<G> insertData
    , Consumer<List<Integer>> deleteByMemberSnList) {
        
    final HashMap<Integer, G> groupByMemberSnMemberMap = new HashMap<>();
    {    
        for (Integer memberSn : addTargetMemberSnList) {
            // 일단은 MemberSn 만 넣는다고 가정.
                
            G generateModel = toGenerateModel.apply(memberSn);
            groupByMemberSnMemberMap.put(memberSn, generateModel);
        }
    }
 
    // 이미 존재하는 구성원이거나 제외대상자는 입력 대상에서 제외.
    {
        // 이미 존재하는 구성원순번 또는 제외 타겟 순번 집합.
        HashSet<Integer> excludeTargetMemberSnSet = new HashSet<>();
        {
            // 이미 존재하는 구성원 순번 목록 삽입.
            List<G> existList = selectList.apply(groupByMemberSnMemberMap.keySet().stream().collect(Collectors.toList()));
            for (G model : existList) {
                excludeTargetMemberSnSet.add(toMemberSubjectSn.apply(model));
            }
 
            // 제외 대상 파라미터도 추가.
            excludeTargetMemberSnSet.addAll(excludeTargetMemberSnList);
        }
 
        // 추가대상 그룹에서 제외 대상 집합을 삭제한다.
        groupByMemberSnMemberMap.keySet().removeAll(excludeTargetMemberSnSet);
    }
 
    // 데이터 트랜잭션
    {
        // 데이터 삽입.
        for (G model : groupByMemberSnMemberMap.values()) {
            insertData.accept(model);
        }
 
        // 제외대상 삭제.
        deleteByMemberSnList.accept(excludeTargetMemberSnList);
    }
}
 
// 메소드 사용 예
// Model1 에 대한 데이터 처리.
ModelSampleService.GetInstance().createByGenerateModel(Arrays.asList(12), Arrays.asList(3),
                    (Integer memberSn) -> {
                        GenerateModel1 generateModel1 = new GenerateModel1();
                        generateModel1.setMemberSubjectSn(memberSn);
                        return generateModel1;
                    },
                    (List<Integer> memberSnList) -> {
                        List<GenerateModel1> list = Model1Dao.GetInstance().selectList(memberSnList);
                        return list;
                    },
                    (GenerateModel1 generateModel1) -> {
                        Integer memberSn = generateModel1.getMemberSubjectSn();
                        return memberSn;
                    },
                    (GenerateModel1 generateModel1) -> {
                        Model1Dao.GetInstance().create(generateModel1);
                        return;
                    },
                    (List<Integer> targetMemberSnList) -> {
                        Model1Dao.GetInstance().deleteByMemberSnList(targetMemberSnList);
                        return;
                    }
);
cs


의도 했던 바와 같이 어떠한 [인터페이스, 추상메소드] 없이, OCP 를 지킨 코드는 나왔습니다.

추 후, GenerateModel 이 또 등장하였고, 비슷한 로직을 사용한다면 각 함수형 인터페이스를 구현해주면 됩니다.


하지만, 일단 메소드 사용성 면에서 많은 불편함을 느꼈을 것입니다.

특정 프로토콜로 묶인 추상메소드의 구현이 아니기 때문에, 각 람다를 구현할 때마다 무슨 기능을 했었는지 살펴봐야 합니다.

이는 가독성도 떨어진다고 볼 수 있을 것 같네요..

(인터페이스나 추상클래스로 구현 했다면, 보다 동적인 구현에 있어서 무슨 일을 하는지 명확했을 수 있습니다.)  


이 과제의 의도는 새로운 지식을 맹신하지 말라는 일종의 견제를 해주고 싶었고(패턴병과 유사한 함수병), 요구사항과 현재 상황에 따라 적절한 대처가 필요함을 느낄 수 있도록 하는 것이 목적이었습니다.

(요구사항이 기껏 한 두개의 함수형 인터페이스만 사용할 정도라면, 깔끔할 수 있었습니다.)



3. 계속 존재했던 동적 파라미터화


마지막 과제는 명령패턴을 이용해, undo/redo 를 구현해보고자 하였습니다.


이와 관련된 내용은 아래 포스팅을 참고.



과제 자체는 사실 이 패턴에 대한 이해도 중요했지만, 꼭 동적 파라미터화 같은 방법이 JAVA8 에서 등장한 것은 아니었다는 것에 대한 실습이었습니다.


명령패턴은 일종의 요청을 캡슐화하고 컨트롤러가 이를 관리하게 함으로써, 실제 요청자와 실행자 사이의 관계를 느슨하게 하게 하는 것을 목적으로 합니다.


대부분 이 패턴의 예제에서는 명령을 캡슐화하기 위해 인터페이스를 구현하는 구현클래스를 제작하지만, 이 과제에서는 굳이 클래스를 제작하지 않고 동적 파라미터화를 이용하여 즉시 구현하는 것을 목적으로 하였습니다.

(다양한 요청에 대해서, 재활용 안 할 구현클래스를 만드는 것은 일종의 낭비이지 않을까요?)


일단, 이 패턴을 구현하기 위한 interface 는 다음과 같이 제작했습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
 * 커맨드 인터페이스 정의.
 *
 * Created by Doohyun on 2017. 7. 10..
 */
public interface ICommand {
 
    /**
     * 어떤기능이든 undo 구현.
     */
    void undo();
 
    /**
     * 어떤기능이든 실행 구현.
     */
    void execute();
}
cs


이를 실행하기 위한 실행자(Receiver)와 컨트롤러(Controller) 는 다음과 같습니다.

(대부분의 내용은 앞써 언급한 포스팅에서 복사했습니다. 이 패턴이 궁금하다면 해당 링크를 참고하세용.)


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
/**
 * 명령을 관리하는 컨트롤러
 */
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);
        }
    }
}
 
/**
 * 글씨를 입력하는 데모 클래스.
 *
 * Created by Doohyun on 2017. 7. 10..
 */
public class TextWatcherDemo {
 
    private StringBuilder builder = new StringBuilder("");
 
    /**
     * 텍스트 입력.
     *
     * <pre>
     *     텍스트를 입력하고, 현재 상태를 표시한다.
     * </pre>
     *
     * @param text
     */
    public void addText(String text) {
        builder.append(text);
        System.out.println(builder.toString());
    }
 
    /**
     * 텍스트 삭제.
     *
     * <pre>
     *     텍스트를 삭제하고, 현재 상태를 표시한다.
     * </pre>
     */
    public void deleteText() {
        builder.deleteCharAt(builder.length() - 1);
        System.out.println(builder.toString());
    }
}
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
38
39
40
41
42
43
44
45
46
47
RemoteController remoteController = new RemoteController();
TextWatcherDemo textWatcherDemo = new TextWatcherDemo();
 
        
 
// 텍스트를 입력받아, 컨트롤러에 명령을 세팅하고 실행하는 소비자 정의.
Consumer<String> InputText = (text) -> {
        // 명령 세팅.
        // 동적으로 행위를 정의. 즉 동적파라미터화
        remoteController.setCommand(
            new ICommand(){
        
                @Override                                                    
                public void undo(){
                    textWatcherDemo.deleteText();
                }
 
                @Override
                public void execute(){
                    textWatcherDemo.addText(text);
                }
            }
        );
 
        // 실행
        remoteController.execute();
};
 
// 메소드 레퍼런스
// undo 실행을 위한 함수 정의.
Runnable undo = remoteController::undo;
 
// 람다 버전의 REDO 함수.
// redo 실행을 위한 함수 정의.
Runnable redo = () -> remoteController.execute();
        
InputText.accept("안");
InputText.accept("녕");
 
undo.run();
redo.run();
 
// CONSOLE LOG
// 안
// 안녕
// 안
// 안녕
cs


이런 방식은 사실 이벤트 처리 기반 시스템을 만들다보면, 꽤 많이 해봤을 것입니다.

결국 [JAVA8 in Action] 에서 메소드를 파라미터로 넘긴다는 이야기는 이전부터 있던 개념이며, 람다나 메소드 레퍼런스는 이를 보다 쉽게 사용할 수 있는 개념이라 볼 수 있을 것 같네요.

(추 후 공부하게 될, Stream API 사용을 위해서는 이를 적극적으로 사용해야 합니다.)



이로써, 지난 주에 실습한 내용에 대한 리뷰가 끝났습니다.

첫 시간이지만, 꽤 많은 내용을 다뤘던 듯 합니다. 그에 따라 과제도 좀 많이 있었죠. ^^;


이 과제를 언급할 때, 최근에 시청한 한 프로그램의 출연자가 했던 대화 중 하나를 같이 말했습니다.


 Knowing is nothing, Doing is the best. 

(아는 것은 중요하지 않다. 하는 것이 가장 좋다.)


단순히 듣기만 하는 것이 아니라, 한번 해보는 것은 정말 중요한 듯 합니다.

(이런 행동들은 조금씩 우아한 방법을 찾아 보는 것에 도움이 되겠죠?.. 아마도...)


어쨌든 개인시간을 투자하며, 계속 지금과 같은 시간을 같이 보내준 여러분들께 감사합니다.

(맥주를 한 병 더 먹었다면 아마 더 감성글이 됐을지도...)



반응형
Posted by N'

다음 스터디에서는 


지난 스터디에서 진행한 간략한 Lambda 의 심화과정을 공부해보려 합니다.


선행으로 미리 읽어볼 자료를 다음과 같이 리스트 업하겠습니다.



또한, 과제 역시 같이 리뷰해보도록 하죠. @.@


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

해당 포스팅에서 언급된 내용은 Ndroid 에서 제공합니다.

https://github.com/skaengus2012/Ndroid


최근, 사내에서 개발하는 프로젝트에서 주로 다루게 되었던 요구사항 중 하나는 특정 범위에 대하여 등급을 부여하는 것이었습니다.


예를들어, [80.01~100 이면 "S", 70.01~80.00 이면 "A"] 이런 식으로 특정 구간에 대한 점수를 구합니다. 


이를 구현하는 문제는 생각보다 어려운 문제는 아니었지만, SWIFT 와 같은 최신언어에서는 범위에 관한 TYPE 을 제공하고 있는 것을 확인했던 차, JAVA 에도 이런 클래스가 있으면 좋겠다는 생각을 하게 되었습니다.


JAVA8 에서는 범위와 비슷한 문제에 대하여, [날짜 클래스 관련 Duration, Between 과 같은 클래스들이 배포]되었지만 아쉽게도 여러 형식을 호환하기 위한 구간 클래스는 아니었습니다.


그래서, N`s 오픈소스 프로젝트에서는 구간 관련 요구사항을 담을 수 있는 모듈을 제공하고자 하였습니다. 

해당 모듈의 주목적은 특정 Generic Type 에 대한 구간을 객체로써 가지는 것을 목적으로 하며, 이를 이용할 수 있는 추가 모듈을 제공하는 것입니다.


사용법은 아래와 같습니다.



1. 구간 객체 생성 및 활용.


구간을 생성하고, 특정 값이 구간에 포함되는가에 대한 표현입니다.


1
2
3
4
5
6
7
// 3 에서 7 사이의 구간 객체 생성.
Between<Integer> between3to7 = Between.Just(37);
 
System.out.println("3에서 7 사이, 4 는 구간에 포함되는가? -> " + between3to7.contains(4+ "\n");
 
// CONSOLE LOG
// 3에서 7 사이, 4 는 구간에 포함되는가? -> true
cs


객체 생성 방식은 RxJava 의 Maybe 와 비슷한 경험을 제공하고자 하였으며, 

기존 집합개념의 Set 의 Set::contains 와 개념적으로 비슷하기 때문에 같은 서명을 사용하고자 했습니다. 



2. 구간 객체 관리 Map


Between 객체를 관리할 수 있는 자료구조가 있다면, 꽤 멋진 활용을 할 수 있을 것이라 생각했었습니다. 

가장 먼저 생각한 것은 구간을 key 로 가지며, 구간에 포함되는 값을 매칭하면 해당 구간에 대한 value 를 얻을 수 있는 Map 을 구현하는 것이었습니다.


자료구조적으로 HashMap 은 아니지만, Map 과 비슷한 메소드 서명을 취할 수 있도록 Wrapping 하여 위의 요구사항을 충족하고자 했습니다. :-)


사용방법은 다음과 같습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
BetweenMap<Between<BigDecimal>String> betweenStringBetweenMap = new BetweenMap<>();
 
// 
betweenStringBetweenMap.put(Between.Just(BigDecimal.valueOf(0.0), BigDecimal.valueOf(5.0)), "A");
betweenStringBetweenMap.put(Between.Just(BigDecimal.valueOf(5.1), BigDecimal.valueOf(10.0)), "S");
betweenStringBetweenMap.put(Between.Just(BigDecimal.valueOf(6.1), BigDecimal.valueOf(13.0)), "S++");
 
// BigDecimal map getter.
// Return type -> Rx.Maybe
betweenStringBetweenMap.getFirst(BigDecimal.valueOf(3.87)).subscribe(grade -> System.out.println("Maybe Type 결과 출력 ([3.87] 에 대한 등급) : " + grade));
 
// List Type Return.
System.out.println("List Type 결과 출력 ([8.7] 에 대한 등급) : " + betweenStringBetweenMap.getToList(BigDecimal.valueOf(8.7)));
 
// CONSOLE LOG
// Maybe Type 결과 출력 ([3.87에 대한 등급) : A
// List Type 결과 출력 ([8.7] 에 대한 등급) : [S, S++]
cs



해당 모듈과 관련된 원본 주소는 아래에서 확인하실 수 있습니다.



이 포스팅이 보다 간결한 코딩을 하는 것에 도움이 되길 바래요 ~ @.@ ~


반응형
Posted by N'

FP 의 첫 스터디는 왜 "함수형프로그래밍" 인지 알아보는 시간을 가져보고자 합니다.


아래 글들을 미리 읽어오시면 도움이 될듯 합니다.

포스팅의 개수가 많지만, 짧기 때문에 금방 읽어볼 수 있을 듯 합니다. :-)



물론, 주교재인 [JAVA8 IN ACTION] 을 미리 읽어오면 더 좋습니다.

각 포스팅 제목이 책의 목차입니다.... @.@


감사합니다. 


- 추가내용


이번, 실습시간에 수행할 프로젝트 폴더를 첨부합니다.

실습시간 내에 해결을 못한다면 아마도 숙제가 되겠죠.. (종종 숙제가 될지도.... @.@)


STUDY_OOP_FP_11.zip


반응형
Posted by N'

8. 함수형 프로그래밍(오리엔테이션).pdf




두번째 주제인 FP 에 대한 오리엔테이션 자료입니다.


감사합니다. :-)

반응형
Posted by N'