Java의 Stream API의 특징

KIYOUNG KWON·2021년 9월 30일
0
post-custom-banner

Stream API의 특징

Java의 Stream API는 Java에서 순차적인 혹은 병렬의 aggregate한 동작을 수행하는 함수형 인터페이스 이다. 람다식과 함께 사용하여 가독성과 코드의 재사용성을 용이하게 해준다. javascript에 익숙한 사람들이라면 사용방법을 익히기는 어렵지 않을 것이다. 사용방법은 많은 사람들이 이미 잘 정리해둔 자료들이 있으니 이 글에선 Stream API의 몇가지 특징들에 대해서 알아보도록 하자.

  • Stream의 source를 수정하지 않는다
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

public class stream {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(3);
        list.add(2);

        List<Integer> sortedList = list.stream().sorted().collect(Collectors.toList());

        System.out.println(list); // [1,3,2]
        System.out.println(sortedList); // [1,2,3]
    }    
}

위 코드를 실행시켜 보면 list와 sortedList는 별개의 객체라는 것을 알수 있다. Stream은 원본으로 부터 새로운 Stream을 만들어 작업을 수행한다.

  • 병렬성(Parallelism)
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(3);
list.add(2);
int sum = list.parallelStream().filter(v -> v % 2 == 1).mapToInt(v -> v).sum();

stream은 sequential stream과 parallel stream 2가지를 생성할 수 있고 이 두가지 stream의 차이점은 이름 그래도 여러개의 스레드에서 병렬로 작업을 실행할 것인지 순차적으로 실행을 할 것인지만 차이가 있고 결과에는 차이가 없다.(없어야한다..) 이 뒤에 이야기 할 stateless와 관련된 내용에서도 언급하겠지만 병렬로 실행하는 경우 state에 영향을 받아선 안된다. pararell stream을 생성하는 경우 ForkJoinPool이라는 threadpool에서 스레드를 가져와 사용한다.

  • 대부분의 경우 stateless하다
Set<Integer> seen = Collections.synchronizedSet(new HashSet<>());
stream.parallel().map(e -> { if (seen.add(e)) return 0; else return e; })...

parallel은 Stream에 정의된 작업을 비동기적으로 수행하겠다는 이야기 이다. 여기서 map이라는 작업을 수행한다면 스레드의 스케쥴링과 seen이라는 state에 따라 동작의 순서가 비확정적이게 된다. 따라서 stream에 매개변수로 들어가는 함수는 기본적으로 stateless한 함수를 사용해야 한다. 여기서 대부분의 경우라는 표현을 사용했는데 stream에서 사용하는 기본적인 함수중에서 stateful한 함수가 존재하기 때문인데 distinct(), sorted(), limit(), skip() 4가지가 그렇다. 이들은 요소간의 관계를 확인해야 하기 때문에 어쩔수 없이 내부적으로 state를 가질 수 밖에 없고 이때문에 pararell stream을 생성하더라고 이 함수들에 한해선 내부적으로 상태를 갖고 다른 작업과는 다른 concurrent한 수행방식 혹은 순차적으로 수행되게 될 것이다.

  • 1회용이다
IntStream sumStream = list.parallelStream().filter(v -> v % 2 == 1).mapToInt(v -> v);
int sum1 = sumStream.sum();
int sum2 = sumStream.sum(); //exception 발생!
System.out.println(sum1);
System.out.println(sum2);

위의 코드를 실행하면 stream has already been operated upon or closed 라는 예외를 볼수 있을 것이다. stream객체는 기본적으로 1회용이라 한번 수행하고 나면 더는 사용할 수 없고 새로 만들어줘야 한다. 아마 내부적으로 python의 generator function처럼 이전의 정보를 갖지않고 수행되기 때문에 그런 것이 아닌가 생각된다.

  • 후처리(lazy)
    stream은 기본적으로 3가지 과정을 거친다
  1. 생성
  2. 가공(intermediate operation)
  3. 결과(terminal operation)

여기서 결과를 도출하는 함수를 호출하기 전까진 stream이 정의가 된 상태일 뿐 실제 동작을 수행하지 않는다.

IntStream sumStream = list.parallelStream().filter(v -> v % 2 == 1).mapToInt(v -> v); //정의만 된 상태
int sum1 = sumStream.sum(); // 이 때 실제 정의된 작업이 수행된다

intermediate operation과 terminal operation은 해당 링크를 참조하기 바란다.

post-custom-banner

0개의 댓글