2017년 9월 26일 기획 일지


제품을 출시하기 위해 해야할 가장 첫 번째는 역시 아이디어 검토인 듯 합니다.

잘 팔릴 제품이나 사용성에 좋은 제품을 찾아내야, 실제로 소프트웨어를 제작 후 "상처뿐인 영광"이 되는 상황을 방지할 수 있겠죠..


위의 취지에 맞춰, 4-GRAM(팀 이름)에서는 한달동안 진행할 프로젝트로 마라톤과 SNS 를 융합시킨 어플리케이션을 제작하려 하고 있습니다.

사내에서 진행하는 대회이기 때문에 사내에 도움이 되는 프로젝트가 좋지 않을지에 대한 생각이 들었고, "나눔" 활동을 지향하는 귀사에서 매년 2회 진행하는 마라톤 행사가 저희의 타겟이 되었습니다.


마라톤 행사에 대해 많은 것을 생각해보니 대회 전 연습 행사부터 본 행사까지 "익숙해진 불편함" 들이 많이 있었으며, 꽤 많은 기능을 제공해 줄 수 있을 것이란 생각이 들었습니다.


아이디어의 실효성에 대한 많은 토의가 있었으며, PPT 를 아래와 같이 브레인 스토밍에서 나온 모든 것을 정리하고 있습니다. 

 

하지만 이를 아무렇게나 정리하기 보다는 정해진 양식에 따라 한 번 작성을 해보자는 생각이 들었습니다.

특정하게 정해진 목차를 기술하고, 그에 따라 살을 붙이면 정리가 낙서가 되는 것을 방지할 수 있지 않을까 에 대한 생각이 들더군요.


이를 위해 아이디어 실효성에 대한 면이 부각, 기술적인 스택, 어필할 수 있는 요소를 찾아보며, 우리가 개발해야할 최종 이 무엇인지를 정리할 필요가 있다는 생각이 들었습니다.


이렇게 정해진 목차는 다음과 같았습니다.


[선정배경] -> [개발목적] -> [기술동향] -> [개발목표] -> [프로젝트 어필 홍보] -> [개발내용] -> [피드백] 


각 목차는 기획 단계에서 꽤 괜찮은 정리를 할 수 있었습니다.



1-2. 선정배경 & 개발목적


앞써, 언급한 이 어플리케이션 의 당위성, 실효성을 검증하기 위한 여러 생각을 할 수 있게 생각했습니다.

선정배경이 제대로 작성 되야 "상처뿐인 영광"이 되지 않을 것 같네요...


3. 기술 동향


벤치마킹 단계였습니다. 


마라톤 앱을 많이 안써봤기 때문에 여러 어플에 대한 조사를 많이 해봤으며, 특히 디자인면에서 많은 참고가 되었습니다.

이 단계에서는 아마 경험없는 저희같은 스타트업의 많은 교보재가 될 것으로 예상 됩니다.


(마라톤 앱이 보통 지도를 많이 사용하는데, 지도를 사용하는 어플도 이쁜 UI 를 작성할 수 있다는 것을 깨닫게 되었네요. ㅎㅎ)

 

 


4. 개발 목표


저희가 제작할 어플리케이션의 꼴입니다.


이 있는 것과 없는 것은 소프트웨어 제작에서 정말 큰 차이를 둔다고 생각하고 있으며, 1순위로 제작해야 하는 필수적인 기능 들을 정리해볼 수 있는 기회였습니다.

 

 


5. 프로젝트 어필 홍보


전략적인 사고로 어떻게 하면, 상을 받을 수 있을지에 대한 고민입니다.

사실상 저희에게 가장 중요한 면인데, 실무라고 한다면 어떻게 물건을 팔지에 대한 고민이 되겠군요.

 

 

 


7. 피드백


여러 사람이 회의를 하기 때문에, 각 사람들이 생각지 못한 결론은 많이 등장 하는 것 같습니다.

특히, 기존 목차에서는 "기능 소개" 라는 부분이 없었는데, 이에 대한 지적이 매우 적절 했었습니다.


언제나 마지막 목차에는 각 회의록에 대한 피드백이 있어야 할 것 같습니다. ^^;

 

 

1회 차 회의의 정리 내용은 이와 같았습니다.

정리해준 모든 자료들에 대한 내용인 [ESC 개발기획서 - 가칭 마이톤 (버전 0.2)] 를 이 곳에 첨부합니다.


첨부 : [ESC 개발기획서 - 가칭 마이톤 (버전 0.2)]






반응형
Posted by N'

안녕하세요. 블로그 주인장 Ndroid 입니다.

새로운 게시판을 신설하게 되어 글을 쓰게 되었습니다.


이번 게시판의 이름은 

"ESC 개발일지!!"


이 게시판은 사내에서 진행하는 개발자 페스티벌에서 진행하는 개발 활동에 대한 기록을 남기기 위해 만들게 되었습니다.


행사 카테고리의 이름은 ESC 로 해커톤 행사라고 합니다만, 미리 제작해도 된다고 하니 해커톤보다는 공개소프트웨어 공모전 같은 느낌이 납니다.... @.@

ESC 뜻은 (Eet Sleep Code) 의 약자인데, 예전에 몸담고 있던 SW 멤버십 단체티의 로고라 조금 더 뜻 깊네요. ㅎㅎ


이 카테고리의 작성 이유는 일지를 쓴다는 면에서 추억을 기억하기 위한 것도 있지만, 개발 일지를 작성함으로써 다음에 비슷한 일을 할 때 더 좋은 결과를 도출하기 위한 면으로 작성하고자 합니다.

(작성이 잘 되어야 하는데, 말이죠.... @.@)



아마, 작성하는 내용은 다음과 같을 것 같습니다.


1. 기획 일지


만들고자 하는 제품의 아이디어 및 도출 과정, 상품을 홍보하기 위한 방법 등의 모든 이야기를 기술해보고자 합니다.

(물론 아이디어는 노출되면 안되니, 행사가 종료될 때까지 비공개!)

아이디어가 주최측에서 공개를 했기 때문에 그냥 공개처리로 하겠습니다. ㅜㅡㅜ


이 과정이 굉장히 어려울 것 같습니다만, 잘 작성해보는 것으로......



2. 개발 일지


제작하는 소프트웨어에 담기는 많은 기술에 대해 작성해보고자 합니다.

많은 스타트 업 회사에서는 실제로 기술 스택 문서를 블로그로 많이 운영하는 것을 확인했고, 현재 개발에서도 많이 참고하고 있습니다.


이번에 만드는 제품에는 재미있는 기술을 많이 담아볼 생각입니다.

기술 뿐만 아니라, 현재 스터디(Effective OOP & FP)의 메인 주제인 개발 방법론 역시 담아볼 수 있으면 담아보는 것으로.....



한 달동안 참여하는 팀원(스터디 참여하는 분)들과 뜻깊은 경험을 공유하며 자신감을 주고 싶고, 그 경험을 잘 담았으면 좋겠습니다.


이번 게시판도 성공적으로 마무리 되길 기도합니다. 하하하하 

 

 





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

세 번째 복습 리뷰는 FP 마지막 스터디 에서 진행했던 새로운 코딩 패턴에 대해 다뤄보려고 합니다.


관련 내용은 아래 포스팅을 참고하세요 :-)



한 번 진행했던 실습 내용에 대해, 기능별로 나눠 포스팅을 진행해보겠습니다.


디폴트 메소드와 Optional 의 경우 관련 포스팅에 내용이 더욱 자세히 나와있습니다.

그렇기 때문에 따로 진행하지는 않겠습니다.



1. 조건부 실행


FP 첫 시간에 배웠던 것 중 한 가지는 함수를 파라미터로 넘길 수 있다는 것이었습니다. 


조건부 실행은 이 메커니즘을 응용한 방법으로 특정 조건이 되었을 때, 어떤 행위를 수행할지 파라미터를 넘기는 것을 의미합니다.


이런 행위를 왜 하는가에 대해 생각해볼 때 다음과 같은 요구사항을 생각해볼 수 있습니다. 


1
2
3
4
5
6
7
8
9
10
/**
  * 특정 행위를 수행하는 함수1.
  * 
  * <pre>
  *     실패했을 때 errorValue 를 이용해서 뭔가를 수행하는 듯.
  * </pre>
  * 
  * @param errorValue
  */
public void func1(String errorValue);
cs


그래서, 이 메소드를 이용하는 곳을 살펴보니 아래와 같았습니다.


1
2
3
4
5
6
7
// 에러 값을 미리 생산.
// errorValueCreator 의 실행시간은 3초.
// 실행시간을 줄일 수는 없어보임.
String errorValue = errorValueCreator();
 
// 결국, 실패하나 성공하나 무조건 errorValueCreator 가 실행되는 상황..
func1(errorValue);
cs


func1 을 실행하기 위해서는 errorValue 를 구해야하는 상황이며, 이는 func1 의 결과가 성공하나 실패하나 무조건 errorValueCreator 를 실행해야함을 의미합니다.


물론 이 방법을 우회할 수 있는 방법은 많으나, 조건부 실행에서는 다음과 같은 방법을 제시합니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
  * 특정 행위를 수행하는 함수1.
  * 
  * <pre>
  *     실패했을 때 errorValue 을 생산하는 함수를 인자로 받음.
  * </pre>
  * 
  * @param errorValue
  */
public void func1(Supplier<String> errorValueSupplier);
 
// 동적 파라미터화 이용.
// errorValueCreator 는 내부적으로 실패했을 때만 실행함을 보장.
func1(() -> errorValueCreator());
cs


즉 실패 시 어떤 행위를 할지를 동적 파라미터화 형태로 받음으로, 실패할 때만 errorValueCreator 가 실행되도록 조정할 수 있어 보입니다.


또한, 동적 파라미터화는 다형성을 내포하고 있기 때문에 다양한 errorValueCreator 를 제작하여 파라미터로 넘길 수 있을 것 처럼 보이네요. 


이러한 조건부 실행을 사용하는 예제는 Optional::orElseGet 입니다.

Optional 이 비어있을 때, orElse 에 디폴트 값을 미리 넣는 반면, orElseGet 는 데이터가 비어있을 때 디폴트 값을 생성하는 함수를 실행하도록 합니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// orElse 사용.
String name1 = Optional.ofNullable(testVO).
                flatMap(TestVO::toTestVO2Optional).
                flatMap(TestVO.TestVO2::toTestVO3Optional).
                flatMap(TestVO.TestVO3::toTestVO4Optional).
                flatMap(TestVO.TestVO4::toNameOptional).
                orElse(GetDefaultResult());
                
// orElseGet 사용.
String name2 = Optional.ofNullable(testVO).
                flatMap(TestVO::toTestVO2Optional).
                flatMap(TestVO.TestVO2::toTestVO3Optional).
                flatMap(TestVO.TestVO3::toTestVO4Optional).
                flatMap(TestVO.TestVO4::toNameOptional).
                orElseGet(() ->GetDefaultResult());
cs


두 메소드는 미묘하지만, 실행이 다릅니다.



2. 커링 함수 


커링 함수는 함수를 완성시키지 않고, 파라미터를 받아 또 다른 함수를 만드는 기법입니다.

조금 더 쉽게 표현하면, 메소드의 결과가 함수임을 말합니다.


이를테면, 이런거?


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
/**
 * 이름에 타이틀을 붙이는 함수 제작
 *
 * @param title
 * @return
 */
public static UnaryOperator<String> GetTitleFunction(
            String title) {
    return (value) -> String.format("%s %s", title, value);
}
 
// 멋있는 이라는 타이틀을 붙여주는 function..
UnaryOperator<String> awesome_func = GetTitleFunction("Awesome Guy");
// 아름다운 이라는 타이틀을 붙여주는 function..
UnaryOperator<String> beautiful_func = GetTitleFunction("Beautiful Lady");
 
System.out.println( beautiful_func.apply("강현지"));
System.out.println( awesome_func.apply("남두현"));
System.out.println( awesome_func.apply("유덕형"));
System.out.println( awesome_func.apply("유형주"));
 
// CONSOLE LOG
// Beautiful Lady 강현지
// Awesome Guy 남두현
// Awesome Guy 유덕형
// Awesome Guy 유형주
cs


동작을 값으로 가진다는 메커니즘은 이런 아이디어를 또 만들어내는군요. @.@


이를 보고, 쓸모 없어보인다고 생각하면 오산!


커링함수들의 출력이 함수형 인터페이스란 것을 생각한다면,

Stream API 의 파이프라인에 들어갈 함수들에 대한 모듈화를 해줄 수 있음을 의미합니다.


1
2
3
Arrays.asList("강현지""남두현""유덕형""유형주").
    stream().
    map(GetTitleFunction("More Developer style person"));
cs


이렇게 말이죠!!!!!!



이 것으로 FP 스터디에 다루고자 했던 모든 내용은 끝났으며, 처음 4월에 생각했던 OOP & FP 의 목표는 마무리되었습니다.


여기까지 달려온 여러분 모두 수고하셨습니다.



이 곳까지 왔을 때, 


4월의 그 때보다 성장 했기를 바라며,  


 Effective 시리즈로 지금 이 시점보다 진보하길


기도합니다.



Good-Bye [OOP & FP].

반응형
Posted by N'

두 번째 복습 리뷰는 Stream API 의 조금 더 스마트한 사용을 위한 아래 포스팅에 대한 추가 내용입니다.



Stream API 의 첫 번째 리뷰가 데이터를 필터하거나 맵핑하는 등의 중간과정을 알아보는 것이었다면, 두 번째 리뷰에서는 중간 연산의 결과를 어떻게 사용할 것인지를 다루는 결과과정을 알아보았습니다.


또한, 원시타입 Stream 역시 알아봤었습니다.


그런 의미에서 이 번 포스팅 역시, 이 시기에 진행했던 실습을 바탕으로 리뷰를 진행하도록 하겠습니다.



Q1. IntStream 실습.


원시타입 Stream 중 하나인 IntStream 을 사용해보는 실습이었습니다.

IntStream 을 이용하면, 숫자 관련된 연산들을 쉽게 처리할 수 있음을 배웠었는데요.

(이를테면, 총합, 개수, 평균, 최대값, 최소값 등등...)


그에 대한 실습으로 아래 코드를 리팩토링 해보았습니다.


1
2
3
4
5
6
7
8
9
10
int sum = 0;
 
// 짝수만 덧셈하는 로직 구현.
for (int i = 1; i <= 1000++i) {
    if (i % 2 == 0) {
        sum += i;
    }
}
 
System.out.println(sum);

cs


1~1000 사이의 짝수의 합을 구하는 간단한 로직입니다.

이 간단한 로직은 조금 더 간단해질 수 있을 것 같아 보입니다.


1
2
3
4
// IntStream 에서 제공하는 sum 메소드 사용.
int sumResult2 = IntStream.rangeClosed(1,1000).filter(i -> i % 2 == 0).sum();
 
System.out.println(sumResult2);
cs


많은 로직을 함수형 프로그래밍으로 리팩토링할 수 있지만, 특히 함수라는 이름답게 수학적인 로직은 보다 더욱 쉽게 리팩토링할 수 있지 않을까요? ㅎㅎ


아, 물론 알아둘 것은 기존 객체형 Stream 에서 원시타입 스트림으로 변경할 수 있다는 것 역시 알아두세요.


1
2
3
4
5
6
List<MemberVO> dataList = new MemberVO();
 
// mapToInt 사용.
dataList.stream().
    mapToInt(MemberVO::getMemberSubjectSn).
    sum();
cs


mapToInt, mapToDouble 등의 메소드는 일반 객체를 원시타입으로 변경하도록 하는 Function 을 받도록 하고 있습니다.


물론, 원시타입 stream 역시 객체 stream 으로 변경할 수 있습니다.


1
2
3
4
5
// box 형 stream 으로 변환. int -> Integer
IntStream.rangeClosed(11000).boxed();
            
// 기타 객체로 변환. int -> MemberVo
IntStream.rangeClosed(11000).mapToObj(number -> new MemberVo());
cs


이와 관련된 내용은 아래 포스팅에서 참고하세용 :-)




Q2. 특정 조건의 객체 찾기 (break 문 리팩토링)


일종의 흐름의 탈출이라 불릴 만한 break, continue, return 등은 goto 문과 비슷해보입니다. 


일련의 절차적 흐름에 따라 과정을 마무리 짓는 것이 아닌, 원하는 위치로 이동시키는 목적을 가진 이 keyword 들은 편의성과 별도로 유지보수에 좋지 않은 특성 때문에 사용을 지양하라고 많이 배웠었을 것입니다.


JAVA8 in Action 에서는 이런 흐름의 탈출 역시 모두 리팩토링할 수 있음을 언급했으며, 이에 대한 실습을 간단히 해보았습니다.


1
2
3
4
5
6
for (int i = 0; i < 100++i) {
    if (i > 50 && i % 2 == 1) {
        System.out.println(i);
        break;
    }
}
cs


위의 예제는 특정조건에 맞는 데이터가 나타나면, 데이터를 출력하고 반복을 탈출하라는 break 를 사용하고 있습니다.


이 로직은 Stream API 에서 제공하는 메소드로 리팩토링이 가능해보입니다.


1
2
3
4
5
IntStream.range(1,100).
    filter(i -> i >50).
    filter(i -> i % 2 == 1).
    findFirst().
    ifPresent(System.out::println);
cs


맞습니다. Stream API 에는 findFirst 라는 적절한 메소드가 존재하네요.

이를 이용해서, break 문을 사용하지 않으며 선언형으로 멋지게 작업을 했군요.


다른 나머지 흐름탈출 keyword 들은 어떻게 우회할 수 있을까요?


아래 포스팅을 읽어보며, 한번 익혀보세용.

또한, 이와 관련된 글들을 아래에 모두 남깁니다.




Q3. 그룹핑을 이용한 직관적인 데이터 정리


귀사에서 주로 진행하는 웹 프로젝트내에서 종단 작업을 하는 개발자들은 유지보수적인 면이나 앞단 작업자를 위해서 인터페이스 위주의 프로그래밍을 해야한다고 언급했던 적이 있었습니다.


이에 대한 글은 아래에서 참고하세용 :-)



이에 대한 철학에 따라, 여러분은 아마 DB 에서 데이터를 조회 후 적절한 인터페이스를 설계하여 앞단 개발자에게 나머지 일을 맡길 것입니다. 


하지만, 복잡한 많은 데이터들을 엮는 것은 사실상 쉽지 않은 일입니다.


예를들면, 아래와 같은 데이터VO 들이 있다고 가정합시다.

(편의상, getter, 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
/**
 * 구성원 정보
 *
 * Created by Doohyun on 2017. 7. 30..
 */
public class MemberVO {
    private String name;                    // 이름
    private Integer memberSubjectSn;        // 구성원순번
}
 
/**
 * 인사평가 단계 정보
 *
 * Created by Doohyun on 2017. 7. 30..
 */
public class HrAppraisalStepVO {
    private Integer hrAppraisalStepSn;     // 인사평가단계순번
    private String stepName;               // 단계명
    private Boolean useYn;                 // 사용여부
}
 
/**
 * 인사평가 종류
 *
 * Created by Doohyun on 2017. 7. 30..
 */
public class HrAppraisalKindVO {
    private Integer hrAppraisalKindSn;  // 인사평가종류순번
    private String appraisalKindName;   // 평가종류명
    private Integer displayPriority;    // 표시순서
}
 
/**
 * 인사평가 단계 종류별 점수 정보.
 *
 * Created by Doohyun on 2017. 7. 30..
 */
public class HrAppraisalStepKindResultVO {
    private Integer hrAppraisalKindSn;      // 인사평가종류순번
    private Integer memberSubjectSn;        // 구성원주체순번
    private Integer hrAppraisalStepSn;      // 인사평가단계순번
    private double score;                   // 점수
}
cs



이 네 종류의 VO 가지고 조합하여 만들어야하는 것은 아래의 VO 입니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
 * 취합 결과.
 *
 * Created by Doohyun on 2017. 7. 30..
 */
public class StepAverageResultVO {
    private String name;
 
    private List<CapabilityAverageVO> capabilityAverageVOList;
 
     /**
     * 평균점수 VO
     */
    public static final class CapabilityAverageVO {
        private String kindName;
        private Double averageScore;
        private Integer displayPriority;
 
        @Override
        public String toString() {
            return String.format("%s : %f", kindName, averageScore);
        }
    }
}
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
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
// 인사평가단계 목록
List<HrAppraisalStepVO> hrAppraisalStepVOList = new ArrayList<>();
{
    for (HrAppraisalStepVO hrAppraisalStepVO : CompleteDao.GetInstance().getHrAppraisalStepList()) {
        // 
        if (hrAppraisalStepVO.getUseYn()) {
            hrAppraisalStepVOList.add(hrAppraisalStepVO);
        }
    }
}
 
// 인사평가종류 목록
List<HrAppraisalKindVO> hrAppraisalKindVOList = CompleteDao.GetInstance().getHrAppraisalKindList();
 
// 구성원 정보 목록.
List<MemberVO> memberVOList = CompleteDao.GetInstance().getMemberList();
 
// 인사평가단계별 종류 결과.
List<HrAppraisalStepKindResultVO> hrAppraisalStepKindResultVOList = CompleteDao.GetInstance().getHrAppraisalStepKindResultVoList();
 
 
// 평균결과 목록.
ArrayList<StepAverageResultVO> stepAverageResultVOList = new ArrayList<>();
 
// 결과목록 제작 알고리즘 정의
for (MemberVO memberVO : memberVOList) {
 
    // 데이터 삽입
    StepAverageResultVO stepAverageResultVO = new StepAverageResultVO();
    {
        stepAverageResultVO.setName(memberVO.getName());
        stepAverageResultVOList.add(stepAverageResultVO);
    }
 
    // 구성원의 역량점수 평균 목록.
    final List<CapabilityAverageVO> capabilityAverageVOList = new ArrayList<>();
 
    for (HrAppraisalKindVO hrAppraisalKindVO : hrAppraisalKindVOList) {
 
    // 단계별 종류 점수 목록.
    final List<HrAppraisalStepKindResultVO> scoreList = new ArrayList<>();
    {
        for (HrAppraisalStepVO hrAppraisalStepVO : hrAppraisalStepVOList) {
            for (HrAppraisalStepKindResultVO hrAppraisalStepKindResultVO : hrAppraisalStepKindResultVOList) {
 
                // 평가종류 잡기.
                if (memberVO.getMemberSubjectSn().intValue() == hrAppraisalStepKindResultVO.getMemberSubjectSn().intValue()
                        && hrAppraisalStepKindResultVO.getHrAppraisalKindSn().intValue() == hrAppraisalKindVO.getHrAppraisalKindSn().intValue()
                        && hrAppraisalStepVO.getHrAppraisalStepSn().intValue() == hrAppraisalStepKindResultVO.getHrAppraisalStepSn().intValue()) {
                                scoreList.add(hrAppraisalStepKindResultVO);
                        }
                }
            }
        }
 
        // 특정 종류가 모든 단계의 점수가 있어야함.
        if (scoreList.size() == hrAppraisalStepVOList.size()) {
            CapabilityAverageVO capabilityAverageVO = new CapabilityAverageVO();
            capabilityAverageVO.setKindName(hrAppraisalKindVO.getAppraisalKindName());
            capabilityAverageVO.setDisplayPriority(hrAppraisalKindVO.getDisplayPriority());
            
            // 평균점수.            
            Double average = 0.0;
            {
                for (HrAppraisalStepKindResultVO resultVO : scoreList) {
                    average += resultVO.getScore();
                }
            }
            
            average /= scoreList.size();
            capabilityAverageVO.setAverageScore(average);
            capabilityAverageVOList.add(capabilityAverageVO);
        }
    }
        
    // 역량에 따른 정렬 수행.
    Collections.sort(capabilityAverageVOList, new Comparator<CapabilityAverageVO>() {
        @Override
        public int compare(CapabilityAverageVO o1, CapabilityAverageVO o2) {
            return o1.getDisplayPriority().compareTo(o2.getDisplayPriority());
        }
    });
    
    stepAverageResultVO.setCapabilityAverageVOList(capabilityAverageVOList);
}
    
// 데이터 프린트.
{
    for (StepAverageResultVO stepAverageResultVO : stepAverageResultVOList) {
        System.out.println(String.format("%s의 점수 정보", stepAverageResultVO.getName()));
            
        for (CapabilityAverageVO capabilityAverageVO : stepAverageResultVO.getCapabilityAverageVOList()) {
            System.out.println(capabilityAverageVO);
        }
 
        System.out.println();
    }
}
 
cs


우와... 그냥, 복잡해보이네요. 

(보지마세요.. 그냥 복잡하다는 것을 보여주기 위한 것...)


여러 데이터 종류들 간의 일종의 외래키 관계 따라 데이터를 엮기 위해, n중 루프를 돌고 있습니다. 

(반복문 내의 복잡한 조건은 덤이군요....)


만약 여러 DB Model 간의 외래키의 관계로 엮어 무슨 일을 하고 싶다면, n중 반복문으로 일일이 찾아 내부에 조건을 만들기보다는 자료구조인 Map 을 이용하는 것을 추천해드립니다.


Map 에 특정 key 에 대한 그룹핑 결과를 모은 뒤, 조합 부분에서 key 기반으로 데이터를 찾아 로직에서 사용하는 것이죠...


이를테면, 특정 데이터를 찾는 for-loop 은 Map 이용하여 다음과 같이 단순화했다고 볼 수 있습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
// for-loop 이용.
// 반복을 통해, 특정 데이터를 찾은 뒤 행위 실행..
for (MemberVo memberVo : memberList) {
    if (memberVo.getMemberSubjectSn() == 2) {
        // 찾았다. 데이터..
        // 다음 로직 수행..
    }
}
 
// Map 을 이용.
// key 기반으로 데이터를 찾음. 단계자체가 없어져 단순화.
MemberVo memberMap = memberMap.get(2);
// 찾았다. 데이터..
// 다음 로직 수행..
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
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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
    /**
     * N식 백엔드 조합의 전략은 다음과 같습니다.
     *
     * - STEP1 DB 데이터 조회한 목록을 특정 사용할 key 기반 맵으로 제작.
     * - STEP2 FP 제공 메소드, 특히 filter, map 등 을 이용하여, 데이터 정리 처리. (구현 부분에서 상세하게 기술.)
     * - STEP2 를 조금 더 간략화할 수 있는 전략으로 어댑터 패턴등이 있음. (Ex. .toXXXX, fromXXX)
     *
     * 해당 방법을 꼭 확인하여, 좋은 성능의 유지보수 좋은 코드를 구현할 수 있는 역량을 키울 수 있길 바랍니다.
     * - 백엔드의 모든 서비스 로직은 아래와 같이 구현할 수 있으며, 해당 문제해결능력을 습득하면 꽤 레벨업한 자바프로그래머가 될 수 있을 것이라 생각합니다.
     *   (자료구조를 효과적으로 이용한 정보처리, 특히 Map 을 이용한 방법은 많은 것을 단순화시킬 수 있습니다.)
     */
    public void N_피드백_결과() {
 
        /**
         * 인사평가 단계 그룹
         *
         * key : 인사평가단계순번
         * value : 인사평가 단계
         */
        final Map<Integer, HrAppraisalStepVO> hrAppraisalStepVOMap = CompleteDao.GetInstance().
                                                                        getHrAppraisalStepList().
                                                                        stream().
                                                                        filter(HrAppraisalStepVO::getUseYn).
                                                                        collect(Collectors.toMap(HrAppraisalStepVO::getHrAppraisalStepSn, Function.identity()));
 
        /**
         * 인사평가 종류 그룹
         *
         * key : 인사평가종류순번
         * value : 인사평가 종류
         */
        final Map<Integer, HrAppraisalKindVO> hrAppraisalKindVOMap =  CompleteDao.GetInstance().
                                                                        getHrAppraisalKindList().
                                                                        stream().
                                                                        collect(Collectors.toMap(HrAppraisalKindVO::getHrAppraisalKindSn, Function.identity()));
 
        // 구성원 정보 목록.
        final List<MemberVO> memberVOList = CompleteDao.GetInstance().getMemberList();
 
 
        /**
         * 구성원의 단계별 종류별 점수 그룹
         *
         * - 사용하지 않는 단계는 필터되었음이 보장.
         * - 추 후, 데이터 조합에서 사용하기 쉬운 형태 순으로 그룹핑을 함.
         * - 아래의 조합 알고리즘에서는 구성원 순번, 인사평가 종류순번에 따라 점수를 분류하고, 모든 단계에 대해 점수가 존재하는지 확인한 후 평균을 구하도록 하고 있음.
         * - 그에 따라, 구성원순번 > 인사평가종류순번 > 인사평가단계순번 > 점수 순으로 그룹핑함..
         * - 이로써, 필요에 따라 원하는 부분의 그룹을 쉽게 가져올 수 있어 보임 (가져오는 부분을 STEP 이라 분류하고 주석으로 작성할 예정. 잘따라오길 바람.)
         *
         * key : 구성원 순번
         * value : {key : 인사평가종류순번, value : {key : 인사평가단계순번, value : 점수 }}
         *
         */
        final Map<Integer, Map<Integer, Map<Integer, Double>>> hrAppraisalStepKindResultMap
                = CompleteDao.GetInstance().getHrAppraisalStepKindResultVoList().
                    stream().
                    filter(vo -> hrAppraisalStepVOMap.containsKey(vo.getHrAppraisalStepSn())).
                    collect(Collectors.groupingBy(HrAppraisalStepKindResultVO::getMemberSubjectSn
                            , Collectors.groupingBy(HrAppraisalStepKindResultVO::getHrAppraisalKindSn,
                                    Collectors.toMap(HrAppraisalStepKindResultVO::getHrAppraisalStepSn, HrAppraisalStepKindResultVO::getScore))));
 
 
        // 데이터 엮기.
        final List<StepAverageResultVO> stepAverageResultVOList;
        {
            // STEP1 구성원 정보 목록에 따라 순회하며, 조합된 결과를 작성할 예정.
            stepAverageResultVOList = memberVOList.stream().
                                        map(memberVO -> {
                                            StepAverageResultVO resultVO = new StepAverageResultVO();
 
                                            // 구성원 정보 추가.
                                            {
                                                resultVO.setName(memberVO.getName());
                                            }
 
                                            // 정렬된 역량 평균 점수 목록.
                                            final List<CapabilityAverageVO> capabilityAverageVOList;
                                            {
                                                /**
                                                 * STEP2 : 구성원 순번에 대한, 점수 그룹을 가져옴.
                                                 *.        아래의 맵은 앞써 그룹핑한 결과에 따라 구성원 점수만 있음이 보장이 됨..
                                                 *
                                                 * 구성원이 받은 단계별 종류별 점수 그룹
                                                 *
                                                 * - Optional 을 이용한 안전한 데이터 파싱.
                                                 *
                                                 * key : 인사평가종류순번
                                                 * value : {key : 인사평가단계순번, value : 점수 }
                                                 */
                                                final Map<Integer, Map<Integer, Double>> subMap1
                                                        = Optional.ofNullable(hrAppraisalStepKindResultMap.get(memberVO.getMemberSubjectSn())).orElse(Collections.emptyMap());
 
 
                                                /**
                                                 * 맵을 이용한 key 기반 조합 방법.
                                                 *
                                                 * - key 기반 조합이기 때문에, 복잡한 알고리즘을 구현하지 않아도 됨. (필요한 정보는 맵에서 직접 추출)
                                                 * - n^m 알고리즘을 최소화할 수 있음 (성능면에서 우위)
                                                 * - filter, map 등 "유효성 체크부분"과 "정리 부분" 의 구분이 확실. (추 후, 유지보수에 괜찮은 듯)
                                                 */
                                                
                                                // 이 곳에서 할 일..
                                                // 인사평가종류에 따라 순회하며, 평균 점수 목록을 구함.
                                                capabilityAverageVOList = subMap1.keySet().stream().
                                                                            filter(hrAppraisalKindVOMap::containsKey).
                                                                            filter(hrAppraisalKindSn -> {
                                                                                // 유효성 체크.
                                                                                // 등장하면 안되는 과제에 대한 유효성 검사 부분을 이 곳에서만 구현.
 
                                                                                // STEP3 : 앞써, 구한 STEP2 에서 특정 인사평가종류의 데이터만 조회.
                                                                                //         마찬가지로, 해당 맵에는 정 구성원의 원하는 종류의 점수만 있음이 보장..
                                                                                //.        결국 아래 맵에는 원하는 특정 종류의 단계별 점수가 있다고 볼 수 있음.
                                                                                
                                                                                /**
                                                                                 * 종류별 단계의 점수.
                                                                                 *
                                                                                 * key : 인사평가단계순번
                                                                                 * value : 점수
                                                                                 */
                                                                                Map<Integer, Double> subMap2 = subMap1.get(hrAppraisalKindSn);
 
                                                                                // 필터는 이 곳에서만 처리.
                                                                                // 점수가 비어있지 않고, 모든 단계의 점수가 있음이 보장이 되어야함.
                                                                                // 즉 위의 로직 중 조건에 맞냐는 여부를 이 곳에서만 구현함으로써 좋은 가독성을 생각해봄.
                                                                                final Boolean validYn;
                                                                                {
                                                                                    // 점수가 비어있는가?
                                                                                    Boolean isEmptyScore = subMap2.isEmpty();
 
                                                                                    // 동일한 사이즈인가?
                                                                                    Boolean isEqualsSize = (subMap2.size() == hrAppraisalStepVOMap.size());
 
                                                                                    // 모든 점수의 단계의 순번이 조회된 단계에 모두 포함되는가?
                                                                                    Boolean isContainsAllKey = subMap2.keySet().stream().allMatch(hrAppraisalStepVOMap::containsKey);
 
                                                                                    validYn = isEqualsSize && isContainsAllKey && !isEmptyScore;
                                                                                }
 
                                                                                return validYn;
                                                                            }).
                                                                            map(hrAppraisalKindSn -> {
 
                                                                                // 데이터 정리.
                                                                                // 유효성 검사를 바탕으로 나온 모든 데이터는 신뢰할 수 있음.
                                                                                // 이 곳에서는 맵을 이용한 key 기반 조합을 할 수 있음.
 
                                                                                final CapabilityAverageVO capabilityAverageVO = new CapabilityAverageVO();
                                                                                {
                                                                                    // 필터에서 구한 것과 마찬가지.
                                                                                    // 특정 단계의 평균점수를 구해야하기 때문에 다시한번 특정 구성원의 특정 종류 결과만 조회
                                                                                    //
                                                                                    // STEP3 : 앞써, 구한 STEP2 에서 특정 인사평가종류의 데이터만 조회.
                                                                                    //         마찬가지로, 해당 맵에는 특정 구성원의 원하는 종류의 점수만 있음이 보장..
                                                                                    //.        결국 아래 맵에는 원하는 특정 종류의 단계별 점수가 있다고 볼 수 있음.
                                                                                    
                                                                                    /**
                                                                                     * 종류별 단계의 점수.
                                                                                     *
                                                                                     * key : 인사평가단계순번
                                                                                     * value : 점수
                                                                                     */
                                                                                    Map<Integer, Double> subMap2 = subMap1.get(hrAppraisalKindSn);
 
 
                                                                                    HrAppraisalKindVO hrAppraisalKindVO = hrAppraisalKindVOMap.get(hrAppraisalKindSn);
                                                                                    capabilityAverageVO.setKindName(hrAppraisalKindVO.getAppraisalKindName());
                                                                                    capabilityAverageVO.setDisplayPriority(hrAppraisalKindVO.getDisplayPriority());
 
                                                                                    // 평균점수 입력.
                                                                                    {
                                                                                        subMap2.values().stream().
                                                                                                mapToDouble(Double::doubleValue).
                                                                                                average().
                                                                                                ifPresent(capabilityAverageVO::setAverageScore);
                                                                                    }
                                                                                }
 
                                                                                return capabilityAverageVO;
                                                                            }).
                                                                            sorted(Comparator.comparing(CapabilityAverageVO::getDisplayPriority)).
                                                                            collect(Collectors.toList());
                                            }
 
                                            // 역량평균 입력.
                                            resultVO.setCapabilityAverageVOList(capabilityAverageVOList);
 
                                            return resultVO;
                                        }).
                                        collect(Collectors.toList());
        }
 
        // 데이터 프린트.
        {
            stepAverageResultVOList.forEach(stepAverageResultVO -> {
                System.out.println(String.format("%s의 점수 정보", stepAverageResultVO.getName()));
                stepAverageResultVO.getCapabilityAverageVOList().forEach(v -> System.out.println(v));
                System.out.println();
            });
        }
    }
cs


즉, 위의 로직에서는 다중 for 문의 목적이 특정 외래키에 따라 원하는 데이터를 찾는 것이었습니다. 

루프의 현재 데이터에서 특정 key 에 대한 데이터를 찾기 위해 for-loop 를 돌고 있죠.


이는 for 문 내부로 들어갈 수록 챙겨야 하는 외래키의 종류는 늘어나는 로직이 되어버렸고, 코드의 복잡성을 높혔습니다.


하지만, Map 을 이용해 이 과정을 단순화함으로써 불필요하게 깊숙한 단계로 들어가지 않아도 되는 것처럼 보이네요.



하지만, Map 기반 데이터 찾기는 어느정도 약점을 가지고 있습니다.

꼼꼼하게, 해당 key가 들어있나 확인을 해줘야하는 것 처럼 보입니다.


하지만, 우리에게는 Optional 이 있으니 마음껏 데이터를 파싱할 수 있고, 대부분 로직을 비어있다는 가정하에 작성하면 강건한 프로그램이 나올 수 있습니다.

(Null 우회, Optional 의 가장 큰 혜택은 아마도 Map 이 아닐지....)


추가적으로 살펴보면, 함수형 문법을 이용하여 필터할 부분과 맵핑할 부분도 적절히 나눠져 있어, 처음 로직보다는 가독성이 좋아보입니다.


내부 주석도 잘 살펴보며, 얻어갈 수 있는 부분을 얻길 바랍니다.


이번 내용과 관련있는 포스팅은 다음과 같습니다.




Q4. Collectors 응용. 목록의 깊은 복사.


Stream API 에서 다양한 결과를 내보낼 수 있는 Collector 는 매우 강력합니다.

이런 Collector 를 배웠으니, 한번 이를 응용해보는 실습을 해보는 것이 좋을 것 같다는 생각이 들었죠...


요구사항은 다음과 같았죠.


Q3의 실습을 통해 Map 이 굉장히 좋은 자료구조라는 것을 느꼈다면, 종종 복잡한 Map 을 복사하고 싶은 욕구가 있을 수 있습니다.


하지만, 복잡한 Map 을 깊은복사하기 위해서는 아래와 같은 로직이 필요할 수 있습니다.


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
// 현재 제공한 targetMap 을 깊은복사하고 싶음.
// 단순하게 맵을 새로 제작하는 것으로는 내부의 맵을 복사할 수 없음. (얕은 복사)
// 즉 하나씩 맵을 순회하면서 복사해야함을 의미.
Map<Integer, Map<Integer, Map<Integer, HrAppraisalStepKindResultVO>>> targetMap =
            CompleteDao.GetInstance().getHrAppraisalStepKindResultVoList().
                            stream().collect(
                                    Collectors.groupingBy(HrAppraisalStepKindResultVO::getMemberSubjectSn,
                                            Collectors.groupingBy(HrAppraisalStepKindResultVO::getHrAppraisalStepSn
                                            , Collectors.toMap(HrAppraisalStepKindResultVO::getHrAppraisalKindSn, Function.identity())))
);
 
        
Map<Integer, Map<Integer, Map<Integer, HrAppraisalStepKindResultVO>>> copyMap = new HashMap<>();
{
 
    for (Integer memberSubjectSn : targetMap.keySet()) {
 
        Map<Integer, Map<Integer, HrAppraisalStepKindResultVO>> subMap1 = targetMap.get(memberSubjectSn);
 
        final Map<Integer, Map<Integer, HrAppraisalStepKindResultVO>> newCopySubMap1 = new HashMap<>();
        {
            for (Integer hrAppraisalStepSn : subMap1.keySet()) {
                final HashMap<Integer, HrAppraisalStepKindResultVO> newCopySubMap2 = new HashMap<>(subMap1.get(hrAppraisalStepSn));
 
                newCopySubMap1.put(hrAppraisalStepSn, newCopySubMap2);
            }
        }
 
        copyMap.put(memberSubjectSn, newCopySubMap1);
    }
}
cs


이런 로직을 저는 Collect 메소드를 이용하여, 아래와 같이 리팩토링을 해보았죠...


자기 자신을 key 로 가지며, value 로 하위항목을 복사하는 상향식 방법이라고 할 수 있을까요? ㅎㅎ

(위에서 부터 아래를 구체화하고 있습니다.)


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
// Collector 를 이용한 깊은 복사.
final Map<Integer, Map<Integer, Map<Integer, HrAppraisalStepKindResultVO>>> copyMap;
{
    /**
     * 로직 설명.
     *
     * Collector.toMap 을 이용한 copy.
     *
     * Collector 의 toMap 제작 시,
     *
     * key 를 자기자신 (Function.identity(), 람다로 표현하면, n->n) 으로 나타내고,
     * value 를 카피할 다음 맵으로 지정합니다.
     *
     * 다음 맵에서도 마찬가지로 Collector 의 toMap 으로 자기자신을 key 로 지정하여 카피하는 전략을 이용합니다.
     *
     */
    copyMap = targetMap.keySet().
                stream().
                collect(Collectors.toMap(
                        Function.identity()
                        , memberSubjectSn -> {
                                    Map<Integer, Map<Integer, HrAppraisalStepKindResultVO>> subMap1 = targetMap.get(memberSubjectSn);
 
                                    return subMap1.keySet().
                                            stream().
                                            collect(Collectors.toMap(
                                                    Function.identity()
                                                    , hrAppraisalStepSn -> new HashMap<>(subMap1.get(hrAppraisalStepSn))
                                            ));
                                })
                        );
}
cs


이번 내용에서 가장 중요한 것은 아마 Collector 겠죠?




이번 장에서 사실 가장 알려주고 싶었던 것은 Q3 였습니다.


Map 은 여러모로 좋은 자료구조이며, 이를 이용해서 앞서 설명한 것보다 더 다양한 것들을 해볼 수 있습니다. 

(응용할만한 것이 많죠... 위는 그냥 응용 중 하나!)


기존에 쓰던 방식을 고수하는 것도 좋지만, 이런 방법도 있구나 를 알면 꽤 좋을 듯 합니다.


무기를 택함에 있어 검만 사용해서 검을 택하는 것과,


여러 무기를 다 사용해보고 검을 택하는 것은 다르겠죠.


그럼, 이쯤에서 두 번째 복습 포스팅을 마칩니다.


복습하는 그대여, 파이팅!!! @.@

반응형
Posted by N'

안녕하세요. :-) 블로그 주인장인 "성숙한 개발을 지향하는 Ndroid" 입니다.

(잘 모르겠지만, 이 것이 필명입니다.)


드디어 함수형 프로그래밍(FP) 챕터까지 마무리함으로써, 기본적으로 목표했던 내용까지 모두 끝냈습니다.


짧았지만 진행했던 시간동안, 개인적으로 당연하게 생각했던 지식들을 다시 한번 되돌아 볼 수 있는 계기가 되었고, 이러한 여러가지 생각들을 공유할 수 있는 자리를 만들 수 있던 것은 행운이었던 것 같습니다.


진행했던 시간동안 도움이 되었고, 실제로 잘 사용만 해준다면 더할나위없이 좋은 것은 없겠죠? ㅎㅎ


그리고! 스터디를 끝냈던 주차에 [전혀, 생각 못한 기분좋은 이벤트?] 도 있어서 매우 행복했습니다. 

(하하하하하 @.@)



그렇다고, 후기같은 감성글을 남기고자 할 생각은 아닙니다!!!!


이번 포스팅 부터 세 차례동안 지난 내용에 대한 리뷰글을 남기고자 합니다.

혹시나 복습을 하고자 하는 분들이 있을지도 모르니(할지는 의문?), 그에 대한 이정표를 제공하고자 합니다.


첫번째 주제Stream API (1) 에서 진행했던 과제 리뷰입니다.


이 글에 대한 자료는 아래 포스팅에서 참고!



과제에서는 JAVA8 이전에 작성된 간단한 코드를 Stream API 를 이용하여 리팩토링하는 시간을 가졌었습니다.


단계별로 하나씩 보도록 하죠.



Q1. Filter 를 이용한 if 절 리팩토링.


아래 코드는 컬렉션을 순회하며, 특정 조건에 맞는 자료를 출력하는 로직입니다.


1
2
3
4
5
6
7
8
9
// 구성원 정보 출력.
List<MemberVo> memberVoList = MemberService.GetInstance().selectMemberList();
 
for (MemberVo memberVo : memberVoList) {
    // 구성원 순번이 3 이하인 정보만 출력.
    if (memberVo.getMemberSubjectSn() < 3) {
        System.out.println(String.format("이름 : %s, 나이 : %d", memberVo.getName(), memberVo.getAge()));
    }
}
cs

비교적 간단해보이는 이 로직은 내부반복을 이용하는 Stream API 에서 filter 키워드를 이용하여 리팩토링할 수 있었습니다.

1
2
3
4
 MemberService.GetInstance().selectMemberList().
     stream().
     filter(memberVo -> memberVo.getMemberSubjectSn() < 3).
     forEach(memberVo ->  System.out.println(String.format("이름 : %s, 나이 : %d", memberVo.getName(), memberVo.getAge())));
cs


위의 코드나 아래 코드나 사실 큰 차이는 없어보이지만, 

함수형으로 작성된 코드는 분기조건(filter)과 실행행위(foreach)의 로직을 분리하고 있습니다.


이는 꽤 복잡한 로직에서 생각보다 매력적으로 다가올 수 있을 것처럼 보입니다.


관련 내용은 아래 포스팅에서 확인할 수 있습니다. :-)





Q2. 정렬과 결과의 개수 제한 로직 리팩토링.


이번 문제에서는 Stream API 의 조금 더 기능을 써보고자 진행했던 내용입니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 구성원 정보 출력.
List<MemberVo> memberVoList = MemberService.GetInstance().selectMemberList();
 
// 나이 순으로 내림차순 정렬,
Collections.sort(memberVoList, new Comparator<MemberVo>() {
    @Override
    public int compare(MemberVo o1, MemberVo o2) {
        return o2.getAge().compareTo(o1.getAge());
    }
});
 
// 구성원 목록이 존재할 경우, 나이가 가장 많은 사람의 이름을 출력.
if (!memberVoList.isEmpty()) {
    MemberVo memberVo = memberVoList.get(0);
 
    System.out.println(String.format("나이가 가장 많은 사람은 ? %s", memberVo.getName()));
}
cs


Q1 과 비교하여, 특정 조건에 맞게 정렬도 해야하며, 정렬된 결과에 따라 딱 한개만 결과가 나오도록 해야하는군요.


하지만, Stream API 에는 다양한 파이프라인 메소드들이 존재하니 이 것 역시 쉽게 해결할 수 있을 것입니다.


1
2
3
4
5
MemberService.GetInstance().selectMemberList().
    stream().
    sorted((o1, o2) -> o2.getAge().compareTo(o1.getAge())).
    findFirst().
    ifPresent(memberVo -> System.out.println(String.format("나이가 가장 많은 사람은 ? %s", memberVo.getName())));
cs


Stream API 에서 지원하는 sort 메소드를 이용하여 정렬하였고, 첫 번째 데이터를 찾기 위해 findFirst 를 사용했습니다.


findFirst 의 결과는 있을 수도 없을 수도 있기 때문에, Optional 형태로 출력함을 잊지마세용.

(현재 케이스에서는 MemberService::selectMemberList 의 결과가 빈 리스트일 수 있음.)


여기서 또한 주목할 점은 의무체인 Lambda 를 이용해볼 수 있다는 것입니다.

sort 의 비교구문은 현재 Lambda 로 되어있지만, Comparator 에서 제공하는 팩토리 메소드를 이용하면 조금 더 직관적으로 변경할 수 있습니다.


1
2
3
4
5
6
 // 나이를 이용하여, 내림차순 정렬.
Comparator<MemberVo> comparator1 = (MemberVo o1, MemberVo o2) -> o2.getAge().compareTo(o1.getAge());
            
// Comparator 의 comparing 으로 어떤 값을 이용하여 정렬할지 지정.
// reversed 메소드를 이용하여 내림차순.
Comparator<MemberVo> comparator2 = Comparator.comparing(MemberVo::getAge).reversed();
cs


어때요? 보다 직관적이고 수정하기 쉽겠죠?


해당 내용은 아래 포스팅에서 확인할 수 있습니다. :-)




Q3. 맵핑 튜토리얼.


Stream API 에서 가장 많이 사용한다고 볼 수 있는 mapping(사상화)에 대한 튜토리얼입니다.

map 을 이용하면, 간단하게 컬렉션의 제네릭 타입을 목적에 맞게 변경시킬 수 있습니다.


예제를 한번 보죠. 아래 코드는 간단합니다. 

구성원목록에서 지역을 추출한 뒤, 오름차순에 따라 정렬 후 콘솔 출력을 하고 있습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 구성원 정보 출력.
List<MemberVo> memberVoList = MemberService.GetInstance().selectMemberList();
 
// red-black tree 알고리즘 따라, 이 곳에 넣은 정보는 자동으로 정렬이 됨.
final TreeSet<String> treeSet = new TreeSet<>();
{
    for (MemberVo memberVo : memberVoList) {
        treeSet.add(memberVo.getLocation());
    }
}
 
for (String location : treeSet) {
    System.out.println(location);
}
cs


아래 코드는 map 메소드를 이용하여, 아래와 같이 리팩토링할 수 있습니다.


1
2
3
4
MemberService.GetInstance().selectMemberList().stream().
    map(MemberVo::getLocation).
    sorted().
    forEach(System.out::println);
cs


정말 간단해서 좋군요.

map 에 치환 Function 만 적절(MemberVo -> String)하게 넣어주면, 모든 것이 해결! ^^


해당 내용은 아래에서 확인할 수 있습니다.




Q4. FlatMap 을 이용한 Inner Join 패턴 구현.


특정 Stream 을 다른 Stream 으로 변경할 수 있는 flatMap 을 이용하여, Inner join 과 같은 패턴을 구현해 볼 수 있습니다.


이를 활용해 보기 위한 예제는 아래와 같았습니다.


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
// 구성원 정보 출력.
List<MemberVo> memberVoList = MemberService.GetInstance().selectMemberList();
 
// 코멘트 정보 출력.
List<CommentVo> commentVoList = MemberService.GetInstance().selectMemberCommentList();
 
final ArrayList<MemberCommentVo> memberCommentVoList = new ArrayList<>();
{
    // 구성원 정보와 코멘트 정보를 엮는다.
    for (MemberVo memberVo : memberVoList) {
        for (CommentVo commentVo : commentVoList) {
            if (memberVo.getMemberSubjectSn().intValue() == commentVo.getMemberSubjectSn().intValue()) {
                MemberCommentVo memberCommentVo = new MemberCommentVo();
                memberCommentVo.setMemberSubjectSn(memberVo.getMemberSubjectSn());
                memberCommentVo.setComment(commentVo.getComment());
                memberCommentVo.setName(memberVo.getName());
 
                memberCommentVoList.add(memberCommentVo);
            }
        }
    }
 
    // 이름으로 정렬.
    Collections.sort(memberCommentVoList, new Comparator<MemberCommentVo>() {
        @Override
        public int compare(MemberCommentVo o1, MemberCommentVo o2) {
            return o1.getName().compareTo(o2.getName());
        }
    });
}
 
// 오직 세개까지만 처리.
for (int i = 0, size = memberCommentVoList.size(); i < 3++i) {
    MemberCommentVo memberCommentVo = memberCommentVoList.get(i);
 
    System.out.println(String.format("%s : %s", memberCommentVo.getName(), memberCommentVo.getComment()));
}
cs


두 타입의 목록에서 구성원순번이 같은 데이터 끼리 엮어 새로운 리스트를 만듭니다.

또한 새로운 목록을 이름 순으로 정렬하고 있네요.

게다가 3개 까지만, 출력하도록 제한하고 있습니다.


이 로직을 구현하기 위해서 꽤 코드가 길어졌군요.


하지만, Stream 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
// 구성원 정보 출력.
List<MemberVo> memberVoList = MemberService.GetInstance().selectMemberList();
 
// 코멘트 정보 출력.
List<CommentVo> commentVoList = MemberService.GetInstance().selectMemberCommentList();
 
memberVoList.stream().
    flatMap(memberVo -> {
        // flatMap 은 현재 stream 에서, 다른 타입의 stream 으로 변경할 수 있게 해줌.
        return commentVoList.stream().
                    filter(commentVo -> {
                        // 구성원 순번이 같은 것끼리만 필터하도록 처리!
                        return memberVo.getMemberSubjectSn().equals(commentVo.getMemberSubjectSn());
                    }).
                    map(commentVo -> {
                        // 취합 데이터 생산.
                        MemberCommentVo memberCommentVo = new MemberCommentVo();
                        memberCommentVo.setMemberSubjectSn(memberVo.getMemberSubjectSn());
                        memberCommentVo.setComment(commentVo.getComment());
                        memberCommentVo.setName(memberVo.getName());
 
                        return memberCommentVo;
                    });
                }
    ).
    sorted(Comparator.comparing(MemberCommentVo::getName)).
    limit(3).
    forEach(memberCommentVo -> String.format("%s : %s", memberCommentVo.getName(), memberCommentVo.getComment()));
cs


flatMap 구문을 보면, 특정 구성원 데이터(memberVo)에 대하여 새로운 Stream 를 출력하도록 하는 Function 을 받도록 되어 있습니다.

이에 따라, CommentVoList 에서는 구성원 순번에 따라 알맞은 데이터를 filter 하고 mapping 하는 Stream 을 넘기도록 하고 있습니다.

나머지 구문은 이름만 봐도 무슨 일을 하는지 알 수 있겠죠? :-)

- sorted : 구성원 이름에 따라 정렬

- limit : 3개 제한

- foreach : 데이터를 이용한 행위 구현.

해당 내용은 아래에서 확인할 수 있습니다.



Stream API 에 대한 첫 번째 튜토리얼은 꽤 심박하지만, 강력했던 것 같습니다.


특히, 내부반복을 이용한다는 측면에서 많은 비지니스 로직을 [필터,맵핑,결과]등 여러 단계를 명시적으로(반 강제적 ㅡㅡ^) 나눌 수 있기 때문에 유지보수성에 있어서 꽤 괜찮은 코드들을 작성할 수 있을 것 같습니다.


조금만 익숙해지면, 실무에서 이를 적용하는 것은 어렵지 않을 것입니다. :-)


그럼, 이 쯤에서 이번 포스팅을 마칩니다.


복습하는 그대여, 파이팅!!! @.@

반응형
Posted by N'

JAVA8 을 이용한 FP 의 마지막 스터디입니다.

(시작이 있으면 끝도 있는 법...... ㅜㅡㅜ)


다음 스터디에서 진행할 아래 글들을 미리 읽고 오면 좋을 것 같습니다. :-)



이번 포스팅 역시, [JAVA8 IN ACTION] 과 같이 읽는 것을 권장합니다.


또한, 이번 스터디 역시 무언가 있습니다.


STUDY_OOP_FP_17.zip


알집을 보고, "또 숙제야" 할 수 있겠지만, 이번에는 숙제가 아니라 실습입니다.

마지막 내용이니, 같이 (페어 프로그래밍?.. 아닌 듯..) 한 번 해보죠. 


분명 스터디 시작 때는 같이 코딩하자고 해놓고서, 한 번도 안한 것 같아 준비했습니다. @.@



그럼, 마지막까지 파이팅!!!

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