Java - 스트림 (stream)

주로 Python을 다루다 보니 Java와 같은 언어에서 stream을 만들어 작업하는 것이 매력적이라고 생각했다. 스트림으로 작업 흐름을 만들면 작은 함수들의 조합을 활용해 작업을 처리할 수 있고, 가독성도 높일 수 있으며, 병렬 처리를 할 때도 유용하다. 이러한 장점을 가진 stream은 Java8에서 쉽게 구현이 가능하다.


반복문 (기존의 방식)

stream을 사용하지 않고 반복문을 사용한다면 아래와 같이 for문을 활용한다.

void test() {
    Integer[] numbers = {3, 5, 2, 7, 0};
    for (int n: numbers) {
        if (n % 2 == 0) {
            System.out.println(n);
        }
    }
}

numbers 배열의 값 중, 짝수인 값만 출력하는 간단한 동작이다. 하지만 반복문으로 구현한 코드는 가독성이 떨어진다. 한눈에 어떤 일을 수행하는지 알아보기 어렵다. 위 과정을 쉽게 설명한다면 아래와 같이 작성할 것이다. 물론 실제와는 다르다.

numbers.filterEven().println()

이렇게 함수들을 조합해 순서에 따라 실행하는 것이 stream이다. 


stream

Collection 객체는 stream() 메서드를 제공한다. Collection은 interface(추상 객체)이기 때문에 ArrayList를 활용해 예제를 작성하였다.

List<Integer> arrList = new ArrayList<Integer>();
arrList.stream() ...

stream에 사용하는 대표적인 메서드는 map, filter, limit, peek, collect 등이 있다. collect는 지정한 자료형을 반환하고, 다른 메서드들은 스트림을 반환한다.  따라서, 스트림을 반환하는 메서들을 연결하여 작성하고 마지막에 collect로 반환한다. 그 외에도 다양한 메서드를 지원한다. 자세한 내용은 공식 문서에서 확인할 수 있다. 

stream의 메서드들을 조합하는 과정은 아래와 같다. 

<코드>
.map( ).filter( ).limit( )

<과정> 
.map -> Stream
        .filter -> Stream
                   .limit -> Stream

이렇게 만들어진 stream은 for-loop와 비교했을 때, 가독성은 높아졌지만 속도는 다른 문제이다. 일반적인 상황에서는 전통적인 for-loop이 stream보다 빠르다. 


stream 예제

import java.util.*;
import java.util.stream.Collectors;

public class main {

    public static void main(String[] args) {
        List<Integer> arr = Func.initRandomList(100)
            .stream()
            .filter(Func::isEven)
            .sorted(Comparator.naturalOrder())
            .limit(10)
            .map(Func::square)
            .collect(Collectors.toList());
        System.out.println(arr);
    }
}

class Func {...}

main 메서드의 과정을 요약하면 아래와 같다.

랜덤 리스트 초기화
-> 짝수 필터링
-> 오름차순 정렬
-> 갯수 제한
-> 값 제곱
-> 리스트로 반환
리스트 츨력

실행 결과는 아래와 같다.

[0, 4, 16, 16, 36, 64, 64, 196, 196, 256]

병렬 스트림

stream은 병렬처리를 위한 메서드를 지원한다. 단순히 stream 뒤에 parallel을 추가하면 된다.

List<Integer> arr = Func.initList(100)
    .stream()
    .parallel()  // 병렬 처리
    // 생략
    .collect(Collectors.toList());

하지만 병렬 처리가 항상 빠른 것은 아니다. 다른 글에서도 반복적으로 언급했지만 오버헤드 등 문제를 고려하면 상황에 따라 빠를 수도 느릴 수도 있다. 어떤 처리를 하느냐, 어떤 CPU를 사용하느냐, 데이터의 크기가 어떤가 등에 따라 다르기 때문에 사용하기 전에 잘 검토해본 후 사용하는 것이 중요하다.