계속해서 JAVA8 에서의 병렬 처리 방법에 대해 포스팅을 하고 있습니다.


JAVA8 에서 Stream API 를 사용하면, 병렬처리를 구현할 때 고려해야할 문제들을 생각하지 않아도 됐지만, 어떤 시점에 어떤식으로 주의하며 적용을 해야한다는 내용을 포스팅 했었습니다.



오늘은 병렬처리관련 마지막 포스팅으로, JAVA8 에서 새로 추가된 인터페이스인 Spliterator 를 소개하려 합니다. Spliterator 는 '분할할 수 있는 반복' 로, 기존 존재했던 반복자인 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
public interface Spliterator<T> {
    
    /**
     * <pre>
     *     해당 반복자를 순차적으로 소비(Consumer) 하면서, 
     *     탐색해야할 요소가 남아있는지에 대한 여부를 출력합니다.
     * </pre>
     * 
     * @param action
     * @return
     */
    boolean tryAdvance(Consumer<super T> action);
    
    /**
     * <pre>
     * 본인의 Spliterator 의 요소 값을 일부 분할하여, 
     * 또 다른 Spliterator 를 생성합니다. 
     * 
     * (작업의 분할과정이라 생각하면, 쉬울 것 같습니다.)
     * </pre>
     * 
     * @return
     */
    Spliterator<T> trySplit();
    
    /**
     * <pre>
     *     작업해야할 요소 정보 수를 추측한 크기를 반환합니다.
     *    해당 크기는 꼭 일치해야 하는 것은 아닙니다. 
     *
     *    (trySplit 메소드를 이용하여, 작업분할 시 참고 자료가 됩니다.)
     * </pre>
     * 
     * @return
     */
    long estimateSize();
    
    /**
     * 현재 Spliterator 의 특성정보를 표시합니다.
     * 
     * <pre>
     *     ORDER : 요소분할 시, 순서가 존재하기 때문에 유의해서 분할하도록 합니다.
     *     DISTINCT : 요쇼 간 같은 값(equals)은 없다고 보장합니다.
     *     SORTED : 탐색된 요소는 미리 정의된 정렬순서를 따릅니다. 
     *     SIZED : estimateSize 은 정확한 정보를 줄 것을 보장합니다.
     *     NONNULL : 탐색하는 모든 요소는 null 이 아님을 보장합니다.
     *     IMMUTABLE : 요소가 탐색되는 동안 데이터를 추가하거나, 삭제할 수 없습니다.
     *     CONCURRENT : 동기화 없이, 여러 스레드가 소스를 동시에 수정할 수 있습니다.
     *     SUBSIZED : 이 Spliterator 와 분할 된 모든 Spliterator 는 모두 SIZE 임을 보장합니다.
     * </pre>
     * 
     * @return
     */
    int characteristics();
}
 
cs


즉 Spliterator 의 메소드 시그니처들은 요소들의 분할을 위해 존재하고 있습니다.


해당 분할 과정은 다음과 같이 이루어 집니다.


출처 : [JAVA8 in Action]


Spliterator 들은 trySplit 메소드를 사용하여, 분할할 수  있는 단계까지 분할합니다. 

즉 내부적으로 분할할 수 있다면, 분할된 부분에 대한 정보를 가진 Spliterator 을 생성한다는 것이죠. 더이상 분할할 수 없다면 null 을 출력합니다. 


이런식으로 최종적으로 도출된 [병렬 특화 반복자]로 병렬처리를 수행합니다. 


한번 예제를 작성해보겠습니다.


1
2
3
4
5
6
7
8
List<Integer> dataSet = IntStream.range(01000).boxed().collect(Collectors.toList());
        
dataSet.parallelStream().
        reduce(Integer::sum).
        ifPresent(System.out::println);
 
// 출력
// 499500
cs


간단한 덧셈에 대한 선언입니다.  


현재는 parallelStream 을 이용해서, Stream 형태로 추출하였지만 우리는 병렬에 특화된 Spliterator 를 만들겁니다. ㅡㅡ^


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
/**
 * Integer 의 덧셈을 위한 Spliterator 
 * 
 * @author Doohyun
 *
 */
public class IntegerSumSpliterator implements Spliterator<Integer>{
    // 자르지 말아야할 최소 사이즈!
    private static final int LIMITED_SPLIT_SIZE = 500;
    
    private List<Integer> integerList;
    private Integer currentIndex = 0;
    
    public IntegerSumSpliterator(List<Integer> integerList){
        this.integerList = integerList;
    }
 
    @Override
    public boolean tryAdvance(Consumer<super Integer> action) {
        action.accept(integerList.get(currentIndex++));
        return currentIndex < integerList.size();
    }
 
    @Override
    public Spliterator<Integer> trySplit() {
        Integer currentSize = integerList.size() - currentIndex;
        
        if (currentSize <= LIMITED_SPLIT_SIZE) {
            // 잘라진 사이즈가 자르지 말아야할 최소사이즈보다 작다면 null 을 출력.
            return null;
        } else {
            // 할 일을 절반씩 잘라줍시다.
            Integer splitTargetSize = currentIndex + currentSize/2;
    
            List<Integer> subList = integerList.subList(currentIndex, splitTargetSize);
            currentIndex = splitTargetSize;
            
            return new IntegerSumSpliterator(subList);
        }
    }
 
    @Override
    public long estimateSize() {
        return integerList.size() - currentIndex;
    }
 
    /**
     * 타겟 리스트의 속
     *     - 각 요소의 유일함을 보장 [(1~1000) 까지의 리스트] : DISTINCT
     *     - 연산 중 중간에 수정될 일이 없으며 : IMMUTABLE
     *     - 여러 스레드가 동시 접근 가능 : CONCURRENT
     *     - 크기는 보장, 하위로 만들어지는 크기도 보장 : SIZED, SUBSIZED - filter 등 고려 안함.
     */
    @Override
    public int characteristics() {
        return Spliterator.DISTINCT + Spliterator.IMMUTABLE + Spliterator.CONCURRENT + Spliterator.SIZED + Spliterator.SUBSIZED;
    }
 
}
 
cs


각 인터페이스를 구현하고, StreamSupport 팩토리를 통해서 Stream 으로 변환해줍니다.


1
2
3
4
5
6
StreamSupport.stream(new IntegerSumSpliterator(dataSet), true)
            .reduce(Integer::sum)
            .ifPresent(System.out::println);
 
// 출력
// 499500
cs


위의 일반 Stream 을 사용한 것과 같은 결과가 나왔습니다. 그런데 굳이 귀찮게 Spliterator 를 만들어야 하나요? 같은 결과가 나오는 데 ㅡㅡ^


Spliterator 의 장점은 개발자의 의지병렬로 처리할 양이나 처리할 때 고려할 사항 (characteristics) 등을 변경할 수 있다는 것입니다. 성능 측정 결과를 기반으로 병렬처리 로직을 더욱 최적화 시킬 수 있을 것이라 생각합니다.



자바 8 인 액션
국내도서
저자 : 라울-게이브리얼 우르마(RAOUL-GABRIEL URMA),마리오 푸스코(MARIO FUSCO),앨런 마이크로프트(ALAN MYCROFT) / 우정은역
출판 : 한빛미디어 2015.04.01
상세보기








반응형
Posted by N'

Stream API 의 병렬스트림을 이용하여, 큰 고민 없이 병렬처리를 할 수 있음을 알 수 있었습니다.



현재 공부하고 있는 책인 [JAVA8 in Action] 에 따르면, 성능을 최적화할 때는 세 가지 황금 규칙이 있다고 합니다. 첫째도 측정, 둘째도 측정, 셋째도 측정!


병렬스트림으로 변경했을 때, 순차스트림보다 빠르다는 것을 보장할 수 있을까요? 

한번 측정을 해보겠습니다.


아래는 0~1000000 에 대한 덧셈 로직과 수행시간 결과입니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
final int[] dataSet = IntStream.range(01000000).toArray();
 
// 단순한 for-loop 순차처리
{
    int sum = 0;
    for (int i = 0, size = dataSet.length; i < size; ++i) {
        sum += i;
    }
}
 
// 병렬스트림 사용.
{
    IntStream.of(dataSet).boxed().parallel().reduce(Integer::sum);
}
 
// Misure time!!
// 1.  3msecs.
// 2. 70msecs.
cs


결과는 단순한 for-loop 이 훨씬 빠르게 나왔습니다. 꼭 빠르다고만은 볼 수 없군요. ㅡㅡ^ 


병렬스트림을 사용한 예제에서는 최종연산 reduce 에서 코드를 간결하게 하기 위해(Integer::sum) 박싱(boxed) 과정을 사용하였습니다. 


알게모르게 놓치는 부분이지만 Wrapper 클래스를 사용하기 위해 하는 박싱과정은 비용이 은근히 크다고 볼 수 있습니다. 그렇기 때문에 JAVA8 에서는 원시타입을 위한 함수형인터페이스와 원시타입 전용 Stream 을 제공한다고 포스팅을 했었던 적이 있습니다.



병렬스트림의 예제를 다음과 같이 변경해보겠습니다.


1
2
3
4
5
// 병렬스트림 사용.
IntStream.of(dataSet).parallel().reduce((a, b) -> a + b);
 
// Misure time!!
// 1. 26msecs.
cs


Integer::sum 이란 이미 구현된 메소드레퍼런스 사용을 포기하고 람다로 구현하였으며, 박싱을 하지 않았습니다. 위의 예제보다 분명 시간은 빨라졌는데 단순 for-loop 보다 느립니다. 


이번에는 한 번 덧셈을 해야하는 양을 늘려보겠습니다. 현재 사용하는 IDE 인 이클립스의 힙을 늘리지 않은 상태에서 out of memory 가 나지 않는 최대량인 (0~700000000) 까지 늘려보겠습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
개수 : 2000000
1. For-loop (4 msecs)
2. Parallel   (43 msecs)
 
개수 : 25000000
1. For-loop (13 msecs)
2. Parallel   (60 msecs)
 
…….
 
개수 : 350000000
1. For-loop (122 msecs)
2. Parallel   (120 msecs)
 
개수 : 400000000
1. For-loop (152 msecs)
2. Parallel   (121 msecs)
 
개수 : 700000000
1. For-loop (243 msecs)
2. Parallel   (174  msecs)
cs


개수가 350000000 부터 비슷한 성능을 보였으며, 그 이후부터는 병렬스트림이 빠른 것을 알 수 있습니다.


즉 이 실험에서 알 수 있는 것은 작업의 분할, 멀티코어 간의 데이터 이동, 결과 병합 등 각 단계 과정의 비용이 꽤 크며, 순차로 하는 작업이 꽤 걸리는 작업일 때만 빛을 볼 수 있다는 것입니다.


두 가지 정도 병렬 처리 시, 주의해야할 문제에 대해서 언급을 했습니다.


- 반복자 내부 박싱에 주의할

(일반 비지니스로직 작성 중에서도 주의할 내용입니다. ㅡㅡ^)


- 병렬로 처리하고자 하는 일이 충분히 오래걸리는 작업인가

    (한 단위의 계산시간이 길다면, 성능개선의 가능성이 있습니다.)


이 외에, [JAVA8 in Action] 에서는 병렬스트림 사용 시, 고려해야할 사항을 다음과 같이 명시하고 있습니다.


1. 확신이 서지 않는다면 측정할 것! 


최적화를 위해서 해야할 세 가지 황금규칙을 생각하세요.


2. limit 나 findFirst 와 같은 순서에 의존하는 연산을 지양할 것.


병렬처리 자체가 반복자의 작업을 분할-정복 하는 알고리즘을 사용하는 데, 순서를 고려해야한다면 그만큼 작업(Thread의 task)간 동기화 시간이 길어지게 될 것입니다.


3. 적절한 자료구조를 사용할 것!


LinkedList 보다 ArrayList 가 좋습니다. 작업 분할을 위해서 LinkedList 는 모든 요소를 탐색해야하지만, ArrayList 는 index의 요소 단위로 탐색이 가능합니다.


작업 분할을 위한 자료구조 선택에 다음 사진을 참고하세요.


출처 : [JAVA8 in Action]


4. Stream 내의 파이프라인의 중간연산에 따라 작업 분할 성능에 영향이 있음!


일정한 크기의 Stream 의 경우 크기가 정확하기 때문에 적절하게 작업을 분리할 수 있지만, 중간에 filter 등 크기를 예측할 수 없게 만드는 중간연산이 있다면 병렬처리를 하는 것에 애로 사항을 꽃피울 수 있습니다


5. 병합과정의 비용도 고려할 것.


기껏 작업을 분할, 계산 열심히 해서 성능을 최적화 했지만, 병합과정에서 오래걸린다면 상쇄될 수 있습니다. 


6. 작업 간의 공유 변수 사용 금지!


예를 들어, 다음의 코드는 문제가 있습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class A {
    Integer a = 0;
    
    void sum(int number) {
        this.a += number;
    }
}
 
A object = new A();
            
// 공유변수 접근.
IntStream.of(dataSet).boxed().parallel().forEach(object::sum);
 
// Measure time!!
// 526msecs.
cs


A 인스턴스의 멤버변수인 a 로 덧셈결과를 취합하고 있습니다. 이 때, 각 스레드는 공유 변수인 A.a 에 서로 접근하려하는 Race Condition 상황이 벌어질 수 있습니다. 


물론, 한 스레드가 한 변수에 접근가능하도록 내부적으로는 세마포어가 작동하는 것은 보장하지만 실험결과와 같이 비용이 크다는 것을 알 수 있습니다.



병렬스트림 사용 시, 주의사항을 다음과 같이 알아보았습니다. 


JAVA8 에서 멀티스레드 프로그래밍을 하기에 분명 Simple 해진 것은 맞고, 공짜로 병렬성을 얻었다고 하지만 이 세상에 완전한 공짜는 없다는 것을 깨닫고 갑니다.



자바 8 인 액션
국내도서
저자 : 라울-게이브리얼 우르마(RAOUL-GABRIEL URMA),마리오 푸스코(MARIO FUSCO),앨런 마이크로프트(ALAN MYCROFT) / 우정은역
출판 : 한빛미디어 2015.04.01
상세보기


반응형
Posted by N'

JAVA8 을 공부하는 있는 책인 [JAVA8 in Action] 에서 언급되었던 내용 중 한 가지는 "공짜로 병렬성을 얻을 수 있다"는 것이었습니다. Collection 군의 데이터를 새로 추가된 API 인 Stream 의 형태를 parallel 관련 keyword 를 사용하여, 선언형 프로그래밍을 작성하면 병렬처리가 된다는 것이었죠.


아래는 병렬처리를 수행하는 간단한 예제입니다.


1
2
3
4
5
6
7
8
9
10
// Collection 클래스 군의 parallelStream 사용.
List<Integer> boxedDataList = IntStream.of(dataSet).mapToObj(Integer::new).collect(Collectors.toList());
boxedDataList.parallelStream().reduce(Integer::sum).ifPresent(System.out::println);
        
// 순차스트림 (IntStream) 을 병렬스트림으로 변
IntStream.of(dataSet).boxed().parallel().reduce(Integer::sum).ifPresent(System.out::println);
 
// 출력 결과
// 499500
// 499500
cs


위의 예제를 보면 정말 간단하다는 것을 알 수 있습니다. (Too Simple!!)

병렬 keyword 를 제외한 중간, 최종 연산 방법이 생각이 안나면, 이 곳을 참고하세요. 



간단히 리뷰를 해보면, 


1번 선언에서는 일반적으로 Collection 에서 스트림을 구하는 것 대신에 parallelStream 키워드를 통해 병렬스트림으로 변경한 후, 처리를 하고 있습니다. 


2번 선언에서는 이미 순차스트림인 상태를 parallel 중간연산을 사용하여 병렬스트림으로 변경시켰습니다.


단순히 병렬스트림으로 변경하는 것만으로 병렬처리를 할 수 있으니, 책에서 소개한 것과 같이 공짜로 병렬성을 얻었다는 말이 뻥은 아니라는 것을 알 수 있습니다. ㅡㅡ^


즉 병렬처리를 하기 위해 생각해봐야할 고민인 

사용할 스레드 개수경쟁상태(race condition), 계산된 결과들의 동기화 등이 추상화되었습니다.


 

parallel 관련 키워드 메소드를 사용하여 순차스트림을 병렬스트림으로 변경했들이,

병렬스트림을 순차스트림으로 변경할 수도 있습니다. sequencial 키워드로 말이죠. :-) 


하지만, 이러한 중간연산(parallel 과 sequencial)을 아래와 같이 특정 중간연산의 제어를 하겠다는 목적으로 여러번 사용하는 것은 부질 없습니다. ㅡㅡ^


1
2
3
4
5
6
IntStream.of(dataSet).boxed()
            .parallel()
            .filter(n -> n % 2 == 0)
            .map(n -> n + 2)
            .sequential()
            .collect(Collectors.reducing(Integer::sum)).ifPresent(System.out::println);
cs


최종적으로 선택된 sequential 만 적용되며, 위 연산은 순차상태로 계산됩니다.


하지만, 이러한 병렬처리가 무조건 성능을 끌어다 줄까요?


이 이야기는 다음 포스팅에서...




자바 8 인 액션
국내도서
저자 : 라울-게이브리얼 우르마(RAOUL-GABRIEL URMA),마리오 푸스코(MARIO FUSCO),앨런 마이크로프트(ALAN MYCROFT) / 우정은역
출판 : 한빛미디어 2015.04.01
상세보기



반응형
Posted by N'


JAVA8 의 등장하면서 생긴 가장 큰 변화 중 하나는 Stream API 를 통한 함수형 프로그래밍 패러다임을 사용할 수 있게 된 것입니다. 비지니스 로직 중 대부분을 SQL 과 같은 질의로 처리할 수 있게 되었기 때문에 활용을 잘한다면 성능과 간결함이라는 토끼를 모두 잡을 수 있게 된 것이죠. 


지난 포스팅까지 살펴본 Stream API 는 파이프라인식으로 여러 메소드를 연결하여 원하는 결과를 질의하는 형태였는데요. 이러한 파이프라인에 해당하는 각 메소드는 데이터를 필터하거나 타입을 변경하는 중간연산(filter, skip, map 등)과 결과를 원하는 형태로 반환하는 최종연산(foreach, collect, reduce)으로 분류할 수 있을 것 같습니다.


오늘은 그 중 최종 연산을 원하는 형태로 질의할 수 있는 방법중 하나인 collect 집중적으로 알아보려 합니다. 

(중간 연산에 대한 내용은 아래에서 참고하실 수 있습니다.)




Stream API를 포스팅 했던 대부분 예제에서는 collect 를 다음과 같이 중간연산의 결과를 List 형태로 출력하는 방법을 많이 사용했었습니다.


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
class Menu {
    private Integer price;
    private String name;
    
    public Menu (String name, int price) {
        this.name = name;
        this.price = price;
    }
 
    public String getName() {
        return name;
    }
    
    public Integer getPrice() {
        return price;
    }
}
 
final List<Menu> menuList = Arrays.asList(
                new Menu("고기"1200)
                , new Menu("랍스타"7600)
                , new Menu("피자"3100));
        
List<Menu> meatMenuList = menuList.stream().
            filter(menu -> menu.getName().equals("고기")).
            collect(Collectors.toList());
cs


collect 부분의 메소드를 조금 zoom-in 해서 보면, Collector 클래스를 파라미터로 받겠금 되어있는 것을 볼 수 있는데 오늘의 메인은 바로 이 Collector 입니다. :-)




1. Collector 란?


Collector 는 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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public interface Collector<T, A, R> {
    /**
     * 새로운 결과 컨테이너를 만들기 위한 메소드
     * <pre>
     *         처음에는 빈 값이 들어가며, 연산 과정을 통해 채워진 후 결과를 출력합니다.
     * </pre>
     * 
     * @return
     */
    Supplier<A> supplier();
    
    /**
     * 컨테이너에 요소를 추가하는 메소드
     * <pre>
     *         공급자 (Supplier) 를 통해 출력해야할 컨테이너에 데이터를 추가합니다.  
     * </pre>
     * 
     * @return
     */
    BiConsumer<A, T> accumulator();
    
    /**
     * 병렬 처리(parallelStream) 시, 두 컨테이너를 병합합니다.
     * 
     * @return
     */
    BinaryOperator<A> combiner();
    
    /**
     * 최종 변환값을 결과 컨테이너 적용합니다.
     * 
     * <pre>
     *         누적된 결과 컨테이너와 최종형태가 같다면, 항등함수( f(x) = x ) 를 내보냅니다.
     * </pre>
     * 
     * @return
     */
    Function<A, R> finisher();
    
    /**
     * 컬렉터 연산 시, 방법에 대한 전략 목록을 정의합니다.
     * 
     * <pre>
     *         UNORDERED : 리듀싱 결과는 방문 순서나 누적 순서에 영향을 받지 않는다.
     *         CONCURRENT : 누적 컨테이너에 요소를 추가하는 accumulator 메소드를 동시에 호출할 수 있다.
     *         IDENTITY_FINISH : finisher 메소드가 항등함수를 내보내야하는 상황이라면, 해당 메소드를 생략하고 누적객체를 바로 사용한다.
     * </pre>
     * 
     * @return
     */
    Set<Characteristics> characteristics();
}
cs


구현된 Collector 는 위의 메소드를 일련의 논리적 순서를 수행하며 원하는 결과를 도출할 수 있습니다. 


즉 collect 를 사용하여 최종결과를 얻기 위해서는 위의 interface 를 사용자가 구현하거나, Collectors 클래스 내부의 toList() 와 같이 이미 구현된 클래스를 사용하는 방법이 있습니다.


Collectors 는 구현화된 전략 Collector 를 출력하는 팩토리로, Stream API 는 이 전략 Collector 를 바탕으로 최종연산을 할 수 있습니다.


Collectors 내부에 정의된 최종연산을 수행하는 방법이 크게 다음과 같은 세 가지 (Reducing, Grouping, Partitioning) 정도의 목적을 가지고 있다고 할 수 있습니다.



2. Reducing 


Stream API 와 관련된 포스팅 중 map 과 reduce  연산을 통하여 요약된 결과를 출력하는 map-reduce  패턴에 대해 언급을 한 적이 있습니다. map 과 reduce 를 통해 Collection 의 원하는 부분만을 원하는 결과로 출력할 수 있다는 것은 매우 매력적인 일이라 할 수 있습니다. (해당 내용은 아래 포스트에서 확인할 수 있습니다.)



Collectors 클래스에서도 이러한 요약연산을 할 수 있는 reducing 메소드를 제공하며, 이 외에도 많이 사용될 법한 통계용 전략 Collector 역시 볼 수 있습니다.


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
final List<Menu> menuList = Arrays.asList(
                new Menu("고기"1200)
                , new Menu("랍스타"7600)
                , new Menu("피자"3100));
                
// 총 합
menuList.stream().collect(Collectors.summingInt(Menu::getPrice));
        
// 사용자 정의 총 합
menuList.stream().collect(Collectors.reducing(0, Menu::getPrice, (a, b) -> a+b));
        
// 사용자 정의 총 합 Optional
menuList.stream().map(Menu::getPrice).collect(Collectors.reducing((a, b) -> a+b)).ifPresent(System.out::println);
        
// 연산 통계
IntSummaryStatistics statics = menuList
                                .stream()
                                .collect(Collectors.summarizingInt(Menu::getPrice));
 
System.out.println("Max : " + statics.getMax());
System.out.println("Min : " + statics.getMin());
System.out.println("sum : " + statics.getSum());
System.out.println("Average : " + statics.getAverage());
System.out.println("count : " + statics.getCount());
 
// 출력물 
// 11900
// Max : 7600
// Min : 1200
// sum : 11900
// Average : 3966.6666666666665
// count : 3
cs


Reducing 을 하는 방법은 위와 같이 열려 있으며, 비지니스 로직 구현 시 알맞은 상황따라 구현하면 될 것 같네요.



3. Grouping


SQL 로 쿼리를 만들다보면 특정 필드따라 그룹을 만들고 싶은 경우가 있으며 GROUP BY 키워드를 사용하여 다음과 같이 grouping 된 결과를 얻을 수 있습니다.


1
2
3
4
5
6
7
SELECT
    company_subject_sn
    , COUNT(member_subject_sn)
FROM
    MEMBER
GROUP BY
    company_subject_sn
cs


Collectors 의 groupingBy 메소드를 사용하면, Collection 객체에 대하여 특정조건으로 분류시키는 작업을 간단하게 만들 수 있습니다.


예를들어 아래의 조직구성원 타입의 Collection 을 회사별, 조직별로 묶는 Map 을 제작은 아래와 같습니다.


비지니스 로직을 처리하기 위한 작업 중 아래와 같이 여러 key 값을 이용하여 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
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
/**
 * 조직 정보를 담은 객체
 * 
 * @author Doohyun
 *
 */
class OrganizationMember {
    private Integer companySubjectSn;    // 회사 순번 
    private Integer memberSubjectSn;    // 구성원 순번
    private Integer organizationSn;        // 조직 순번  
    private String name;                // 이름
    
    public OrganizationMember(Integer companySubjectSn, Integer memberSubjectSn, Integer organizationSn, String name) {
        this.companySubjectSn = companySubjectSn;
        this.memberSubjectSn = memberSubjectSn;
        this.organizationSn = organizationSn;
        this.name = name;
    }
    
    public Integer getCompanySubjectSn() {
        return companySubjectSn;
    }
    public Integer getMemberSubjectSn() {
        return memberSubjectSn;
    }
    public Integer getOrganizationSn() {
        return organizationSn;
    }
    public String getName() {
        return name;
    }
}
 
List<OrganizationMember> testList = Arrays.asList(
                new OrganizationMember(511"남두현")
                , new OrganizationMember(622"윤석진")
                , new OrganizationMember(633"성지윤")
                , new OrganizationMember(744"백선기")
                , new OrganizationMember(754"황후순")
                , new OrganizationMember(561"이현우")
                , new OrganizationMember(572"태재영"));
        
HashMap<Integer, Map<Integer, List<String>>> groupingMap = new HashMap<>();
        
for (OrganizationMember organizationMember : testList) {
    final Integer companySubjectSn = organizationMember.getCompanySubjectSn();
    final Integer orgSn = organizationMember.getOrganizationSn();
            
    if (!groupingMap.containsKey(companySubjectSn)) {
        // 회사순번으로 분류
        groupingMap.put(companySubjectSn, new HashMap<>());
    }
            
    final Map<Integer, List<String>> orgMap = groupingMap.get(companySubjectSn);
    if (!orgMap.containsKey(orgSn)) {
        // 조직순번으로 분류
        orgMap.put(orgSn, new LinkedList<>());
    }
            
    // 조직순번으로 분류
    orgMap.get(orgSn).add(organizationMember.getName());
}
        
System.out.println(groupingMap.toString());
 
// 출력
// {5={1=[남두현, 이현우], 2=[태재영]}, 6={2=[윤석진], 3=[성지윤]}, 7={4=[백선기, 황후순]}}
 
cs



Collectors 팩토리에서 제공하는 groupingBy 를 사용하면 위의 문제를 보다 쉽고 가독성있게 구현할 수 있습니다. 아래 예제는 위의 기능을 Collectors.groupingBy() 를 사용하여 간결하게 작성한 내용입니다.


1
2
3
4
5
6
7
8
9
10
11
Map<Integer, Map<Integer, List<String>>> groupinParallelMap = 
                    testList
                    .parallelStream()
                    .collect(
                        Collectors.groupingBy(OrganizationMember::getCompanySubjectSn
                        , Collectors.groupingBy(OrganizationMember::getOrganizationSn, Collectors.mapping(OrganizationMember::getName, Collectors.toList()))));
        
System.out.println(groupinParallelMap.toString());
 
// 출력
// {5={1=[남두현, 이현우], 2=[태재영]}, 6={2=[윤석진], 3=[성지윤]}, 7={4=[백선기, 황후순]}}
cs


파이프라인이 조금 복잡해보이지만,  groupingBy 에서 회사조직 순번순으로 분류작업을 하였으며Stream API map 비슷한 역할을 하는 mapping  메소드를 통해 조직객체를 이름으로 변경을 했습니다.



4. Partitioning


앞서 설명한 Collectors.groupingBy() 과 비슷하게, Stream 내의 객체들을 특정 Predicate 로 분류하여 그룹화할 수 있는 기능 또한 존재합니다. Collectors.partitioningBy() 을 통해 사용할 수 있습니다.


Predicate 로 분류작업을 수행하기 때문에 반환되는 key 의 값은 Boolean(참, 거짓) 입니다. 그러므로, 결과 Map 은 최대 2 개의 그룹이 존재한다고 봐도 될 것 같습니다.


아래 예제는 회사순번이 5인 그룹과 아닌 그룹으로 분류, 또한 세부 그룹을 조직순번으로 분류한 작업입니다.


1
2
3
4
5
6
7
8
9
10
Map<Boolean, Map<Integer, List<String>>> groupinParallelMap = 
                    testList
                        .parallelStream()
                        .collect(
                            Collectors.partitioningBy((OrganizationMember v) -> v.getCompanySubjectSn() == 5
                            , Collectors.groupingBy(OrganizationMember::getOrganizationSn
                                    , Collectors.mapping(OrganizationMember::getName, Collectors.toList()))));
 
// 출력
// {false={2=[윤석진], 3=[성지윤], 4=[백선기, 황후순]}, true={1=[남두현, 이현우], 2=[태재영]}}
cs


앞의 예제와 크게 변하지 않고 단순히 파이프라인 한 개만 변할 것을 알 수 있습니다. (이게 함수형 프로그래밍의 장점이죠!!)


또한 이런 분류작업을 병렬로 하고 있음(parallelStream) 을 알 수 있습니다.



오늘 포스팅으로 Stream 에서 진행되는 중간연산과 최종연산을 수행하는 방법을 간략하게는 훑어보게 된 것 같습니다. 이러한 결과로 컨테이너 객체를 이용한 비지니스 로직이 한 결 더 유연하고 간략하며, 최적화까지 할 수 있는 여지가 생겼습니다. 


하지만 잘 사용해야 가능한 이야기겠죠? :-)



자바 8 인 액션
국내도서
저자 : 라울-게이브리얼 우르마(RAOUL-GABRIEL URMA),마리오 푸스코(MARIO FUSCO),앨런 마이크로프트(ALAN MYCROFT) / 우정은역
출판 : 한빛미디어 2015.04.01
상세보기






반응형
Posted by N'

현재 공부하고 있는 도서의 Stream API 에 대한 자세한 설명이 나와 있어서, 같은 주제로 계속 블로깅을 하고 있습니다. 


하지만 대략적인 부분이 끝이 난 것 같습니다. Stream API 소개는 이번 포스팅이 마지막입니다.

지난 Stream API 가 궁금하다면, 하단 글들을 참고해주세요. :-)



오늘 포스팅은 보통 전공 과목에서 흔히 말하는 기타 기능에 대해 알아보려고 합니다. 그러나 유용할 수 있죠? :-)


1. 숫자형 스트림으로 변환


람다를 포스팅할 때도 있었지만, 스트림 역시 원시타입의 스트림을 지원합니다. 보다 더 정확히 말하면, 숫자들에 대한 스트림을 지원하며 이 기본형 특화 스트림들은 sum, max, min 등 자주 사용하는 리듀싱 메소드를 제공해줍니다.


숫자 스트림을 사용하기 위해서는 map 의 파생 메소드인 mapToInt, mapToDouble, mapToLong 등의 메소드를 사용해야 합니다.


예제부터 한번 봐볼까요?


1
2
3
4
5
6
7
8
9
List<Integer> numberList = Arrays.asList(1,2,3,4,5,6);
    
System.out.println(
        numberList.
            stream().
            mapToInt(Integer::intValue).
            sum());
 
// 출력 21
cs


간단한 Integer Collection 객체의 stream 에서 int value 의 stream 얻기 위해 mapToInt 메소드를 사용하였으며(언박싱), 리듀스 메소드 중 하나인 sum 을 사용하였습니다.


추가적으로 또다른 리듀싱 메소드인 max나 min 은 Optional 상태로 데이터가 제공됩니다. 컬렉션이 비어있는 상태에서 max 나 min 의 값이 무조건 존재한다는 것을 보장할 수 없기 때문이죠.



2. 숫자 범위 스트림


JAVA8 에서는 숫자형스트림에서 특정 범위안의 숫자 집합을 출력해주는 정적메소드인 rangerangeClosed 를 제공해줍니다. 두 메소드 모두 범위에 대한 인수를 받으며, 차이는 시작값과 종료값이 결과에 포함하는 여부입니다. (range 의 경우 결과에 포함되지 않습니다.)


사용예제는 다음과 같습니다. 아래 예제는 숫자 Stream 을 이용하여, 피타고라스의 수를 구하는 쿼리입니다.


1
2
3
4
5
6
7
IntStream.rangeClosed(1,100).
        boxed().
        flatMap(a -> IntStream.
                        rangeClosed(a, 100).
                        mapToObj(b -> new int[]{a, b, (int) Math.sqrt(a *+ b * b)})).
        filter(v -> v[2]%1 == 0).
        forEach(v -> System.out.println(v[0+ " " + v[1+ " " + v[2]));
cs


flatMap 을 통해, 두 개의 숫자 스트림을 엮었습니다. 첫번째 범위에서는 Stream 연산의 결과를 객체로 받기 위해 boxed 메소드를 사용하였습니다. boxed 객체를 사용하지 않으면, 오직 중간연산 결과로 숫자타입의 스트림밖에 생산할 수 없습니다. (왜냐하면 IntStream 은 숫자 스트림이기 때문이죠. ㅡㅡ^)



3. 스트림 만들기


컬렉션이나 배열은 다음과 같이 임의의 값으로 데이터를 만들 수 있습니다.


1
2
String[] dataArray = new String[]{"남두현""윤석진""성지윤""오진명"}; 
List<String> dataList = Arrays.asList(dataArray);
cs


스트림 역시 임의의 값으로 스트림 만들기가 가능합니다. 꼭 컬렉션 클래스의 stream 키워드를 쓸 필요는 없는 것이죠. ㅡㅡ^


1
2
3
4
String[] dataArray = new String[]{"남두현""윤석진""성지윤""오진명"}; 
        
Stream<String> ofStream = Stream.of("남두현""윤석진""성지윤""오진명");
Stream<String> arrayStream = Arrays.stream(dataArray);
cs



4. 고정되지 않은 크기의 스트림 만들기


여태까지 Stream은 고정된 크기의 컬렉션을 통해서 만들곤 했습니다. 그러나 특정 Function, Supplier 타입의 메소드 레퍼런스 (람다 포함) 를 통해 무한한 Stream 을 만들 수 있습니다.


1
2
3
4
5
// iterate 는 Function type 람다를 사용 
Stream.iterate(0, n -> n + 2).forEach(System.out::println);
        
// generate 는 Supplier type 메소드 레퍼런스 사용
Stream.generate(Math::random).forEach(System.out::println);    
cs


만들 수 있는 key-word 메소드는 iterate 와 generate 로 보통 limit 로 제한을 두어 사용하곤 합니다. 제한을 두지 않은 스트림을 언바운드 스트림(Unbounded Stream) 이라 하며, 특정 로직에서 무한한 결과 때문에 제대로된 결과를 얻을 수 없을 지도 모릅니다. 

(그러나 위 예제 에서는 무한한 숫자가 출력이 될 것입니다. 컬렉션과 달리 Stream 은 즉시 생산을 하는 것이 특징이기 때문이죠. ㅡㅡ^)


것으로 Stream API 활용과 관련된 주제는 끝났습니다.


다음 포스팅 부터는 Stream 을 통한 데이터 수집과 관련된 주제로 진행을 하게 될 것 같습니다. 

(스포일러를 하자면, 언제나 최종 연산을 구할 때 사용했던 Collector 와 관련된 이야기 입니다. ^^)



자바 8 인 액션
국내도서
저자 : 라울-게이브리얼 우르마(RAOUL-GABRIEL URMA),마리오 푸스코(MARIO FUSCO),앨런 마이크로프트(ALAN MYCROFT) / 우정은역
출판 : 한빛미디어 2015.04.01
상세보기


반응형
Posted by N'

 지금까지는 Stream API 의 최종 연산에서 여러 종류의 값 (Boolean, filter 된 리스트, void 연산) 을 반환을 함을 알 수 있었습니다. 여러 중간 연산을 pipe 처럼 묶어서 사용하고, 그 결과를 취함으로 여러 로직의 이점을 많이 챙길 수 있었습니다.


오늘의 포스팅 주제는 조금 더 복잡한 질의를 표현할 수 있는 리듀싱(reducing)에 대해 알아보려고 합니다. 예를 들어, 컬렉션 내부에 대해 총합을 구한다거나 최대값, 최소값을 구하는 연산이 대표적이라고 할 수 있습니다. ㅡㅡ^


 간단한 예제 부터 한번 봐보도록 할까요? 아래 코드는 컬렉션 내부의 값 중, 짝수 만을 덧셈하는 연산입니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
List<Integer> numberList = Arrays.asList(5103692);
        
int sum = 0;
        
for (Integer number : numberList) {
    if (number % 2 == 0) {
        sum += number;
    }
}
 
System.out.println("총 합 : " + sum);
 
// 총 합 : 108
cs


 이 간단한 코드는 Stream 연산을 통해 더욱 간단해질 수 있습니다. 약간의 중간연산과 리듀싱을 통해 처리할 수 있습니다.


1
2
3
4
5
6
7
8
9
10
List<Integer> numberList = Arrays.asList(5103692);
 
System.out.println("총 합 : " + 
            numberList.
                stream().
                filter(n -> n % 2 == 0).
                reduce(0, (a, b) -> a + b)
            );
 
//총 합 : 108
cs


 filter 를 통해 짝수만 추출했으며, reduce 를 이용해 덧셈의 결과를 구했습니다.


위의 예제에서 사용한 reduce 는 초기 값을 0 을 했음을 알 수 있습니다. 물론, 초기값이 없는 연산을 수행할 수 있습니다. 이 때 결과는 Optional 객체로 나타나게 되며 해당 객체를 이용해 값이 있을 때만 처리하는 기능 등을 수행할 수 있습니다.


리듀싱의 응용은 다양하게 할 수 있습니다. 예를들어 특정 객체에 대한 컬렉션 중, 한 필드에 대한 합은 다음과 같이 구할 수 있습니다.


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
/**
 * Test 를 위한  VO
 * 
 * @author namduhyeon
 *
 */
class TestVo {
    private String name;
    private Integer value;
    
    public TestVo (String name, Integer value) {
        this.name = name;
        this.value = value;
    }
 
    public String getName() {
        return name;
    }
 
    public Integer getValue() {
        return value;
    }
}
 
List<TestVo> numberList = Arrays.asList(
                new TestVo("Doohyun Nam",7)
                , new TestVo("Ford Mill",9)
                , new TestVo("Hwang",2));
        
numberList.stream().
    map(v -> v.getValue()).
    reduce((v1, v2) -> v1 + v2).
    ifPresent(System.out::println);
 
// 18
cs


특정 필드를 추출하기 위해 map 을 사용하였으며, 이 Stream 을 통해 합을 구할 수 있었습니다. 


이러한 map 과 reduce 를 이용하여, 데이터를 가공하는 패턴을 map-reduce 패턴이라 하며 웹 검색 엔진 등에서 많이 사용하는 것을 볼 수 있습니다.


그 이유는 언제나 Stream API 에서 쉽게 볼 수 있는 병렬성이라는 특징 덕분입니다. 예를 들어 특정 컬렉션의 합을 병렬적으로 구하기 위해서는 임계영역간 스레드의 경쟁 비용이 발생하기 마련이지만, 리듀스 내부의 내부반복은 개발자에게는 추상화되어 제공됩니다. (분할하여 결과를 구하고 머징하는 과정이 아닐지...) 


하지만, 모든 기능이 만병통치약일 수는 없습니다. ㅡㅡ^ 

병렬스트림으로 리듀싱 결과를 일반 스트림과 동일하게 얻기 위해서는 연산이 어떤 순서로 연산되어도 결과가 보장되는 구조이며, 람다 내부 인스턴스는 변경되어서는 안되는 구조이어야 합니다. 즉 각 트랜잭션 중 병렬구조로 처리가 가능한 부분만 사용이 가능함을 알 수 있습니다.


복잡한 데이터의 질의를 쉽게 표현할 수 있는 리듀싱에 대해 알아보았습니다. 생각보다 자주 접해보면, 금방 눈에 들어오지 않을까 생각이 듭니다. 


점점 함수형 프로그래밍의 장점이 눈에 들어오지 않으시나요? :-) 


자바 8 인 액션
국내도서
저자 : 라울-게이브리얼 우르마(RAOUL-GABRIEL URMA),마리오 푸스코(MARIO FUSCO),앨런 마이크로프트(ALAN MYCROFT) / 우정은역
출판 : 한빛미디어 2015.04.01
상세보기




반응형
Posted by N'

지난 포스트에 이어서, 계속 Stream API에 대한 포스팅을 하고 있습니다.


지난 포스팅에서는 각 컨테이너를 매핑하여, 원하는 형태의 객체로 출력을 할 수 있는 방법을 소개했었습니다. 

마치 SQL 처럼 특정 필드만을 추출한다던가, 두 개 이상의 컨테이너를 JOIN 할 수도 있었죠.


해당 포스팅은 이 곳에서 확인!! 



오늘은 특정 속성이 데이터 집합에 있는지, 있다면 해당 객체를 추출할 수 있는 다양한 유틸 메소드를 살펴볼까 합니다.


예를 들어, 우리는 이러한 작업을 많이 하곤 합니다.


아래 로은 특정 숫자 컬렉션에서 5의 배수가 있는지 확인하는 로직입니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
List<Integer> sampleList = Arrays.asList(5,10,12,38);
        
Boolean isHave = false;
for (Integer number : sampleList) {
    if (number % 5 == 0) {        
        // 5의 배수인 데이터가 있는지 확인
        isHave = true;
        break;
    }
}
        
System.out.println("5의 배수가 존재  : " + isHave);
 
// 출력 : 5의 배수가 존재  : true
cs


루프를 돌면서, 출력할 컬렉션 객체에 if 조건을 사용하여 여부를 업데이트하려 하고 있습니다.


해당 로직은 다음과 같이 수정될 수 있습니다.



1. Predicate 가 적어도 한 요소와 일치하는지 확인 (anyMatch)


anyMatch 유틸 메소드로 다음과 같은 로직을 구할 수 있습니다.


1
2
3
4
5
List<Integer> sampleList = Arrays.asList(5,10,12,38);
        
System.out.println("5의 배수가 존재  : " + sampleList.stream().anyMatch(n -> n % 5 == 0));
 
// 출력 : 5의 배수가 존재  : true
cs


단순히 predicate 형태로 컬렉션 제네릭 객체의 조건만 명시해주면 조금 더 간소화된 코드를 구현할 수 있습니다.


2. Predicate 를 이용한 기타 확인 (allMatch, noneMatch)


Predicate 를 이용하여 컬렉션의 모든 요소가 일치하는지를 확인하려면, allMatch 메소드를 사용할 수 있습니다. 또한 반대로 일치하는 요소가 없는지를 확인하는 기능 역시도 noneMatch 메소드를 사용함으로써 간단하게 구현할 수 있습니다.


1
2
3
4
5
6
7
8
9
List<Integer> sampleList = Arrays.asList(5,10,15,40);
        
System.out.println("모든 요소가 5의 배수인가? : " + sampleList.stream().allMatch(n -> n%5 == 0));
System.out.println("5의 배수는 없는가? : " + sampleList.stream().noneMatch(n -> n % 5 == 0));
 
// 출력
// 모든 요소가 5의 배수인가? : true
// 5의 배수는 없는가? : false
 
cs


3. 요소 검색


Stream API 를 사용하면 쉽게 한 요소에 접근할 수 있습니다. 일련의 중간 연산(filter, distinct 등)을 수행한 후 findAny, findFirst 등의 최종연산 메소드로 조건에 맞는 한 요소를 선택할 수 있습니다. 예제를 한번 볼까요? 


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
List<Integer> sampleList = Arrays.asList(17,19,28,37,16);
        
sampleList.
    stream().
    filter(n -> n % 2 == 0).
    findAny().
    ifPresent(n -> System.out.println("2의 배소의 요소 찾기 : " + n));
        
sampleList.
    stream().
    filter(n -> n % 2 == 0).
    findFirst().
    ifPresent(n -> System.out.println("2의 배소의 첫번째 요소 찾기 : " + n));
 
// 출력
// 2의 배소의 요소 찾기 : 28
// 2의 배소의 첫번째 요소 찾기 : 28
cs


두 메소드의 내용을 살펴보면, 동일함을 알 수 있습니다. 왜냐하면, 쇼트서킷 검사(보통 Boolean 연산 특정 조건에 의해 검사결과가 정해지면 추 후 연산은 안하는 행위)를 통해 결과를 찾은 뒤 즉시 실행을 하기 때문이죠.


그렇다면 왜 굳이 메소드를 두 개로 둔 이유가 무엇일까요?


그 이유는 공짜로 얻을 수 있는 병렬성 이 후 후처리 때문입니다. Collection 클래스에서 Stream 추출 시, parallelStream 을 통해 병렬 처리를 쉽게 수행할 수 있습니다. 그러나 병렬처리를 수행 시 컬렉션의 순서가 보장이 되지 않습니다! ㅡㅡ^


즉 병렬처리를 통해 데이터 연산을 수행하였다면, 명시적으로 첫 번째 요소를 찾기 위해 findFirst 를 사용하여야만 합니다! 


아래 예제를 통해 결과를 확인해볼까요? 아래 예제는 병렬처리를 했음에도 findFirst 사용 시, 첫 번째 요소찾기를 보장하고 있음을 보여주고 있습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
List<Integer> sampleList = Arrays.asList(17,19,28,37,16);
        
sampleList.
    parallelStream().
    filter(n -> n % 2 == 0).
    findAny().
    ifPresent(n -> System.out.println("2의 배소의 요소 찾기 : " + n));
        
sampleList.
    parallelStream().
    filter(n -> n % 2 == 0).
    findFirst().
    ifPresent(n -> System.out.println("2의 배소의 첫번째 요소 찾기 : " + n));
 
// 출력
// 2의 배소의 요소 찾기 : 16
// 2의 배소의 첫번째 요소 찾기 : 28

cs



마지막으로 생각해봐야할 부분은 요소 검사 메소드 활용 시, 결과가 항상 있다는 것을 보장하지 않는다는 점입니다. 보통 이럴 때 기존 자바에서는 null 로써, 데이터를 처리했지만 JAVA8 부터 많은 함수형 프로그래밍에서 볼 수 있는 Optional 의 개념이 생겼습니다. 


Optional값의 존재나 부재 여부를 표현하는 컨테이너 클래스로, 예제에서 사용한 ifPresent 는 값이 존재할 때만 실행을 명령하는 메소드 입니다. (Optional 은 추 후 따로 포스팅을 한 후 연결을 시켜두겠습니다. ㅡㅡ^)



자바 8 인 액션
국내도서
저자 : 라울-게이브리얼 우르마(RAOUL-GABRIEL URMA),마리오 푸스코(MARIO FUSCO),앨런 마이크로프트(ALAN MYCROFT) / 우정은역
출판 : 한빛미디어 2015.04.01
상세보기



반응형
Posted by N'

지난 포스트에 이어서, 계속 Stream API에 대한 포스팅을 하고 있습니다. 


Stream API를 활용하여, 컨테이너(Collection)에서 복잡하고 다양한 조건을 질의형태로 구현할 수 있다는 것을 배웠습니다. 방어코드 없이 말이죠!! 


지난 포스팅은 이곳에서 확인! 



이러한 질의와 같은 방법은 SQL과 비슷해서 우리가 원하는 결과를 깔끔하게 구현할 수 있는데요. 오늘은 검색과 더불어 특정 데이터를 선택하는 작업인 매핑에 대해 알아보려 합니다.


계속해서 알기 쉬운 SQL에 비교하여 알아보겠습니다.


우리는 DBMS에 질의를 할 때, 언제나 테이블의 ROW 데이터 모두를 원하지 않습니다. 질의어 자체에서 출력 결과를 조정할 수 있으며, 선택을 할 수 있습니다. 예를들어 "구성원이라는 테이블에, 이름만 출력" 하고자 한다면 이렇게 사용을 하죠.


1
2
3
4
5
6
SELECT
    member.name
FROM
    member as member
WHERE
    member.age = 40
cs


JAVA8에서는 map 메소드를 사용하여 특정 필드의 결과만을 가지는 Collection을 생성할 수 있습니다. 함수형 인터페이스로 친다면 Function 의 성격을 가지고 있습니다. (T를 받아 R을 출력하고 있으니 말이죠 ㅡㅡ^) 


함수형 인터페이스의 성격이 무슨 말인지 모르신다면 해당 포스트를 참고하세요.



다시 돌아와서, map의 활용을 보도록하겠습니다.


일단 테스트VO 클래스에 대한 명시입니다.


1
2
3
4
5
6
public class MemberVo {
    private Integer sn;
    private Integer age;
    private String name;
}
 
cs


위 VO는 구성원을 가르키며, 고유넘버, 나이, 이름 정도의 멤버 변수를 가지고 있습니다. 

상단의 언급한 SQL 쿼리의 로직을 수행해보도록 하죠!


Map의 활용


1
2
3
4
5
6
7
8
9
10
List<MemberVo> memberList = Arrays.asList(
                new MemberVo(127"남두현"), new MemberVo(220"유형주"),
                new MemberVo(320"태재영"), new MemberVo(440"남궁계홍"));
        
memberList.stream()
    .filter(a -> a.getAge().intValue() == 40)
    .map(MemberVo::getName)
    .forEach(System.out::println);
 
console value : 남궁계홍
cs


지난번에 배웠던 검색 메소드인 filter를 이용하여 나이에 대한 조건을 언급하였고, map 메소드를 이용하여 이름이라는 데이터를 출력하였습니다.


보다 다양한 결과를 출력하고 싶다면, map 메소드의 파라미터안에 적절한 람다 혹은 메소드 레퍼런스를 언급해주면 됩니다.


JAVA 8 IN ACTION 에서는 이러한 map 과 더불어, flatMap을 같이 소개했습니다. flatMap의 기능은 스트림의 평면화로, 두 개의 컨테이너 객체의 스트림을 하나로 묶는 것을 말합니다. (쉽게 말해서 SQL의 JOIN과 같다고 볼 수 있습니다. ㅡㅡ^)


flatMap을 이용하여 두 테이블을 합친 로직을 구현해볼까요?


FlatMap의 활용


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
List<MemberVo> memberList = Arrays.asList(new MemberVo(127"남두현"), new MemberVo(220"유형주"),
                new MemberVo(320"태재영"), new MemberVo(440"남궁계홍"));
List<BoardDetailVo> boardList = Arrays.asList(new BoardDetailVo(1"람다게시판입니다.""람다에 관련된 클만 올려주세요."),
                new BoardDetailVo(1"람다란 무엇이니가?""함수형 프로그래밍의 꽃?"),
                new BoardDetailVo(2"물어볼 게 있습니다.""람다공부는 어떻게 하죠?"));
 
memberList.stream().flatMap(i ->
        boardList.stream()
        .filter(j -> j.getSn() == i.getSn().intValue())
        .map(j -> {
                MemberBoardResultVo result = new MemberBoardResultVo();
                result.setSn(i.getSn());
                result.setName(i.getName());
                result.setContents(j.getContens());
                result.setTitle(j.getTitle());
                return result;
        })).forEach(ret -> {
                System.out.println(String.format("[%s]%s:%s"
                    ret.getName(), 
                    ret.getTitle(), 
                    ret.getContents()));
            }
        );
 
- console result -
 
[남두현]람다게시판입니다.:람다에 관련된 클만 올려주세요.
[남두현]람다란 무엇이니가?:함수형 프로그래밍의 꽃?
[유형주]물어볼 게 있습니다.:람다공부는 어떻게 하죠?
 
cs


일단 chain된 Stream 이 살짝 복잡해 보이네요. 파이프 하나씩 한번 살펴보죠!


flatMap 내부부터 한번 살펴보면,


1. memberList의stream이 주가 되어, flatMap 내부에서 boardList의 Stream을 사용하고 있습니다.


1
memberList.stream().flatMap(i -> boardList.stream()....)
cs



2. boardList의 stream 내부 파이프라인 중 filter는 memberList 의 각 객체 중, 본인 객체의 sn이 서로 같은 것만 찾으라는 조건을 명시하고 있습니다.


1
filter(j -> j.getSn() == i.getSn().intValue())
cs


3. boardList stream의 내부 map에서는 Function 의 역할로 MemberBoardResultVo 를 출력하도록 되어 있습니다. 람다표현식을 사용하여, MemberVo와 BoardDetailVo 에서 각각 필요한 정보를 주입합니다.


1
2
3
4
5
6
7
8
map(j -> {
                MemberBoardResultVo result = new MemberBoardResultVo();
                result.setSn(i.getSn());
                result.setName(i.getName());
                result.setContents(j.getContens());
                result.setTitle(j.getTitle());
                return result;
        })
cs


4. foreach에서는 위의 flatMap으로부터 출력된 컨테이너를 순회하며, 필요한 정보를 출력하고 있습니다.


1
2
3
4
5
6
7
forEach(ret -> {
                System.out.println(String.format("[%s]%s:%s"
                    ret.getName(), 
                    ret.getTitle(), 
                    ret.getContents()));
            }
        );
cs


람다식이 조금 복잡해보이지만, 사실 기존 java8 이전의 구현 방식대로 진행한다고 하면, 2중 for-loop을 통해 보다 복잡한 로직이 될 수 있습니다. 위의 예제는 비교적 간단해보이지만, 컬렉션 3~4개를 합친다거나, 합쳐진 컬렉션에서 limit, skip, order by 등의 추가사항이 붙는다면 기존 구현된 소스를 보고 고민에 빠지는 시간이 조금 길어질것입니다.


하지만 람다식을 통해 다음과 같이 chain 메소드 한개만 출력해주면 간단히 해결이 됩니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
memberList.stream().flatMap(i ->
            boardList.stream()
            .filter(j -> j.getSn() == i.getSn().intValue())
            .map(j -> {
                    MemberBoardResultVo result = new MemberBoardResultVo();
                    result.setSn(i.getSn());
                    result.setName(i.getName());
                    result.setContents(j.getContens());
                    result.setTitle(j.getTitle());
                    result.setAge(i.getAge());
                    return result;
        }))
        .sorted((a, b)-> a.getAge().compareTo(b.getAge()))
        .limit(1)
        .forEach(ret -> {
                System.out.println(String.format("[%s]%s:%s"
                        ret.getName(), 
                        ret.getTitle(), 
                        ret.getContents()));
            }
        );
 
- console result -
[유형주]물어볼 게 있습니다.:람다공부는 어떻게 하죠?
cs


단순히, 두 객체를 머징하는 내부 람다에 age를 추가 하였고, sorted와 limit를 사용하였습니다.


즉 우리는 이번 포스팅을 통해 알 수 있었던 것은 컨테이너를 사용하는 로직에서 


단순히 체인형 stream을 추가하는 방법만으로 변화에 보다 쉽고 유연하게 대처


할 수 있음을 알게 되었습니다.



자바 8 인 액션
국내도서
저자 : 라울-게이브리얼 우르마(RAOUL-GABRIEL URMA),마리오 푸스코(MARIO FUSCO),앨런 마이크로프트(ALAN MYCROFT) / 우정은역
출판 : 한빛미디어 2015.04.01
상세보기



반응형
Posted by N'

안녕하세요. 블로그 주인인 초보프로그래머입니당.


오랜만의 블로그 포스팅입니다.  개인적으로 바쁜 일이 있어서(곧 회사내에서 출시를 하기 때문에 버그를 열심히 잡았죠... ㅡㅡ^), 오랜만에 공부를 해보고 내용을 정리하게 되었습니다.


솔직히 블로그할 시간은 있긴 했지만, 게을러지다보니 공부하게 되는 것이 쉽지가 않네요.


하지만 책을 산 것이 아깝고, 곧 진행했던 안드로이드 프로젝트에도 함수형 프로그래밍의 결실을 시도해봐야하니(RxJava or Android N) 다시 한번 붙잡게 되었습니다.


그런 의미에서 오늘은 계속 진행하던 Stream API 의 활용에 대해 보고자 합니다. 

Stream API에 대해 처음보게 된다면 이 곳을 먼저 확인해주세요.



이전에 Stream API를 소개할 때, 컨테이너(Collection)의 활용을 질의 형태로 작성할 수 있다고 언급했었는데요. 마치 고수준 언어인 SQL을 사용하듯 말이죠. (Collection 객체를 테이블이라고 본다면, 질의를 통해 어떤 결과를 얻는다 생각하면 좋겠네요? ㅡㅡ^)


오늘은 그 중 필터링과 슬라이싱(즉 검색.. ㅡㅡ^)을 해보려 합니다.


1. 필터링

SQL의 where 절과 같은 역할입니다. Collection 객체 중 프리디케이트(Predicate)에 부합하는 객체들을 따로 추출하는 역할을 합니다. 그 메소드는 이전부터 많은 예제로 사용되었던 filter 입니다. 


몸풀기로 시작해보겠습니다.


1
2
3
4
5
6
7
8
9
List<Integer> numberList = Arrays.asList(22345688910);
 
for (Integer a : numberList.stream().filter(a -> a % 2 == 0).
                collect(Collectors.toList())) {
    System.out.println(a);
}
 
console result : [2,2,4,6,8,8,10]
 
cs


filter 메소드를 이용하여, 짝수만을 조회하였습니다. SQL로 치자면, 이정도??


1
SELECT number FROM NUMBERLIST WHERE number % 2 = 0;

cs


2. 고유 요소 필터링

위의 결과 중, 중복된 결과가 있습니다. 원하는 결과가 중복된 값이 없기를 바란다면 distinct 메소드를 사용해주면 됩니다.


1
2
3
4
5
for (Integer a : numberList.stream().filter(a -> a % 2 == 0).
                distinct().collect(Collectors.toList())) {
    System.out.println(a);
}
 
cs


생각해보면 SQL에서 DISTINCT 를 사용하여, 고유 결과를 출력할 수 있다는 것을 알고 있습니다.


1
SELECT DISTINCT number FROM NUMBERLIST WHERE number % 2 = 0;
cs


3. 결과값 제한과 스킵

보통 게시판을 만들 때, limit offset 등을 사용하여 페이징 처리를 하곤 합니다. 왜냐하면 많은 데이터를 한번에 다 보여줄 수도 없거니와, 사용도 불편하기 때문이죠.


Stream API에서는 limit offset 과 같은 역할을 해줄 수 있는 메소드 역시 가지고 있습니다.


일단 Stream을 잘 살펴보면, limit 라는 메소드를 가지고 있습니다. 아래 코드는 Stream의 결과 중, 3개를 반환하는 메소드입니다.


1
2
3
numberList.stream().filter(a -> a % 2 == 0).distinct().limit(3).forEach(System.out::println);
 
console result : [2,4,6]
cs


재미있는 점은 limit를 사용하지 않은 결과보다, limit의 파라미터 값이 크더라도 에러를 출력하지 않습니다. 이 점은 우리가 최대 몇개를 가져온다라는 비지니스 로직을 구현할 때, 방어코드를 할 필요가 없어졌음을 말합니다.


마지막으로 skip을 이용하여, 요소를 건너뛸 수 있습니다. 예를들어 3번째 결과부터 2개 출력이라 하면 이렇게 응용을 할 수 있습니다.


1
2
3
numberList.stream().filter(a -> a % 2 == 0).distinct().skip(3).limit(2).forEach(System.out::println);
 
console result : [8,10]
cs


skip역시 입력받는 파라미터에 대해 방어코드를 할 필요가 없습니다. 조건에 만족하지 못한다면 빈 배열을 출력합니다.



오늘 포스팅에서는 Stream API를 이용한 필터 및 슬라이싱하는 방법을 알아봤습니다. 


어느정도 함수형 프로그래밍의 감이 오시나요? 


오늘 포스팅으로 부터 알 수 있는 점은 함수형 프로그래밍으로 비지니스 로직을 구현 시,


1. 간편하고, 직관적인 선언형 위주의 구현


2. 방어코드 없이, 오류가 적은 코드의 구현


이라는 장점을 볼 수 있었음을 알 수 있었습니다.




자바 8 인 액션
국내도서
저자 : 라울-게이브리얼 우르마(RAOUL-GABRIEL URMA),마리오 푸스코(MARIO FUSCO),앨런 마이크로프트(ALAN MYCROFT) / 우정은역
출판 : 한빛미디어 2015.04.01
상세보기


반응형

'개발이야기 > 함수형 프로그래밍' 카테고리의 다른 글

Stream API 활용편3 (검색과 매칭)  (0) 2017.01.18
Stream API 활용편2 (매핑)  (0) 2016.08.27
Stream과 Collection 차이  (0) 2016.08.04
Stream API  (0) 2016.08.03
람다 조합  (0) 2016.08.02
Posted by N'

지난 포스팅부터 "본격 선언형 프로그래밍을 할 수 있게 해준 Stream API" 에 대해 포스팅하고 있습니다. 

(모던해지는 자바에서 lambda와 더불어 선언형 프로그래밍이 어느정도 대세가 되지 않을까요? ㅡㅡ^)



오늘 포스팅에서는 "데이터 처리 연산을 지원하도록 추출된 연속된 요소 (JAVA8 IN ACTION 에서 인용)" 인 Stream 과 기존 Collection 의 차이에 대해 포스팅해보려 합니다. 


여기에서 연속된 요소라는 정의는 순차적으로 접근가능한 요소라는 점에서, Collection 과 Stream의 큰 차이는 데이터 저장 우선 vs 연산 우선 이라 볼 수 있을 것 같네요.


기존 Collection 은 데이터를 어떻게 잘 저장하고 접근할 것이냐에 초점을 맞췄습니다. 내부 요소에 대한 처리를 하기 위해서는 외부적으로  for-loop 를 통해 각각의 데이터에 접근하며, 비지니스 로직을 처리해야 합니다(외부 반복). 즉 어떤 처리를 위해서 Collection 내부에 모든 값을 가지고 있어야 합니다.


반면 Stream 의 경우 어떻게 처리를 할 것인가에 초점을 맞췄는데요. Stream 은 내부적으로 반복 과정을 숨겨 알아서 처리하고 결과를 어딘가에 출력하는 과정을 수행합니다. (내부 반복)  내부 반복을 사용하는 덕분에 우리는 컨테이너 처리에 대하여 순차적으로나 병렬적으로 처리를 간단하게 설정할 수 있게 되었어요. 아래 코드는 순차처리와 병렬 처리에 대한 간단한 코드입니다.


1
2
3
4
5
6
7
8
9
10
dataList.stream().filter((a) -> a % 7 == 0).sorted((a, b) -> b.compareTo(a)).limit(4)
        .forEach(System.out::println);        // 순차 처리
 
dataList.parallelStream().filter((a) -> a % 7 == 0).sorted((a, b) -> a.compareTo(b)).limit(4)
        .forEach(System.out::println);        // 병렬 처리
 
/*
* 위의 코드 중 병렬 처리는 의도된 결과가 나오지 않습니다. 병렬로 처리를 하기 때문에 
* parallelStream 의 사용은 "이 일이 병렬로 처리가능한가?" 를 생각해보고 사용해야합니다.
*/
cs


또한 Stream 의 처리 과정은 이론적으로 요청하는 값에 대해서만 처리를 하겠다는 핵심적인 내용이 있습니다. 이 것은 요청할 때만 처리하여, collection은 게으르게 만들겠다는 소리인데... 이 것 역시 예제로 보면 좋을 것 같아요. 


무한한 짝수를 표현해야하는 컨테이너의 문제에 대하여,


Collection은 아래과 같이 데이터를 우선적으로 만들고 있어, 소비는 할 수 없네요.


1
2
3
4
5
6
7
8
ArrayList<Integer> intList = new ArrayList<>();
for (int i = 0;; i+=2) {
    intList.add(i);
}

// 영원히 소비가 불가능합니다.
for (int i = 0, size = intList.size(); i < size; ++i){
    System.out.println(intList.get(i));
}
cs


반면 Stream 의 경우 무한한 짝수를 모두 만들기보다는 들어오는 요청에 대해서 즉시 만들어 소비하죠. (연속적인 요소가 어떻게 처리가 되는지에 대한 과정을 보기 위한 것이므로, for-loop 내부에 log 를 찍는 경우는 생각안하겠습니다.)


1
2
3
4
IntStream intList = IntStream.iterate(0, (i) -> i+2);;
intList.forEach(System.out::println);
 
// result : 0,2,4,6 ......
cs


사실 이러한 특성들은 사실 모두 비지니스 로직으로 풀어갈 수 있는 문제로 보이네요

하지만 이러한 것들을 미리 알아두는 게, 보다 더 편리하고 뻐그없겠금 사용할 수 있게 도와주지 않을까요?  




자바 8 인 액션
국내도서
저자 : 라울-게이브리얼 우르마(RAOUL-GABRIEL URMA),마리오 푸스코(MARIO FUSCO),앨런 마이크로프트(ALAN MYCROFT) / 우정은역
출판 : 한빛미디어 2015.04.01
상세보기

반응형

'개발이야기 > 함수형 프로그래밍' 카테고리의 다른 글

Stream API 활용편2 (매핑)  (0) 2016.08.27
Stream API 활용편1 (필터와 슬라이싱)  (0) 2016.08.26
Stream API  (0) 2016.08.03
람다 조합  (0) 2016.08.02
메서드 레퍼런스  (0) 2016.08.01
Posted by N'