세 번째 복습 리뷰는 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'

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'

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


- 함수란 무엇인가?


- 다형성(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'

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


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

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



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

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


감사합니다. 


- 추가내용


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

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


STUDY_OOP_FP_11.zip


반응형
Posted by N'

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




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


감사합니다. :-)

반응형
Posted by N'