반응형

스트림 API(Stream API)

Stream

  • sequence of elements supporting sequential and parallel aggregate operations
    • 순차적인 요소(연속된 데이터)의 순차 및 병렬 집계 작업을 처리할 수 있는 연산들의 집합
    • 연속된 데이터를 처리하기 위한 연산들의 집합
  • 데이터를 담고 있는 저장소(컬렉션)가 아니다.
  • Funtional in nature, 스트림이 처리하는 데이터 소스를 변경하지 않는다.
  • 스트림으로 처리하는 데이터는 오직 한 번만 처리한다.
  • 스트림은 무제한일 수도 있다. (Short Circuit 메소드를 사용해서 제한할 수 있다.)
    • 예를 들어, 처음 10개의 데이터만 보겠다라고 제한할 수 있다.
  • 중개 오퍼레이션은 근본적으로 lazy 하다.
    • 여기서의 lazy는 다 처리하지 않아도 중간에 끝낼 수 있다라는 의미보다는 아래의 의미와 같다.
    • 종료 오퍼레이션 (terminal operation)가 오기 전까지는 실행되지 않는다.(스트림 파이프라인) 종료 오퍼레이션이 오기 전까지는 스트림이 정의만 되어 있는 상태이다.
  • 손쉽게 병렬 처리할 수 있다.

 

스트림 파이프라인

  • 0 또는 다수의 중개 오퍼레이션 (intermediate operation)과 한개의 종료 오퍼레이션 (terminal operation)으로 구성한다.
  • 스트림의 데이터 소스는 오직 터미널 오퍼네이션을 실행할 때에만 처리한다.

 

중개 오퍼레이션

  • Stream을 리턴한다.
  • Stateless / Stateful 오퍼레이션으로 더 상세하게 구분할 수도 있다. (대부분은 Stateless지만 distinct나 sorted 처럼 이전 이전 소스 데이터를 참조해야 하는 오퍼레이션은 Stateful 오퍼레이션이다.)
  • filter, map, limit, skip, sorted, ...

 

종료 오퍼레이션

  • Stream을 리턴하지 않는다.
  • collect, allMatch, count, forEach, min, max, ...

 

스트림 개념 예제

public class Main {

    public static void main(String[] args){
        List<String> names = new ArrayList<>();
        names.add("history");
        names.add("dev");
        names.add("apple");
        names.add("goo");

        /*
            종료형 오퍼레이션이 없기 때문에 실행되지 않는다.
            스트림의 정의만 있는 상태.
        */
//        List<String> collect = names.stream()
//                .map((s) -> {
//                    System.out.println(s); //중개 오퍼레이션(map)은 종료 오퍼레이션이 없으면 출력이 되지 않는다. 중개 오퍼레이션은 근본적으로 lazy 하다는 의미가 바로 이런 특징이다.
//                    return s.toUpperCase();
//                });
        System.out.println("===중간 오퍼레이션 수행 시 출력되는 데이터===");
        List<String> collect = names.stream()
                .map((s) -> {
                    System.out.println(s); //출력이 된다. 종료 오퍼레이션(collect)이 존재한다.
                    return s.toUpperCase();
                }).collect(Collectors.toList()); //종료 오퍼레이션이 오면 정의된 스트림이 실행된다.
        System.out.println("===중간 오퍼레이션 수행 후 적용 된 데이터===");
        collect.forEach(System.out::println);
        System.out.println("#######################");

        /*
            스트림이 처리하는 원본 소스는 변경되지 않는다.
        */
        names.forEach(System.out::println);
        System.out.println("#######################");

        /*
            스트림을 사용하면 손쉽게 병렬 처리할 수 있다.
        */
        //기존 forEach를 사용해서 병렬적으로 처리하기에는 쉽지 않다.
        for (String name : names) {
            if(name.startsWith("a")) {
                System.out.println(name.toUpperCase() + " " + Thread.currentThread().getName());
            }
            System.out.println(name + " " + Thread.currentThread().getName());
        }
        System.out.println("#######################");

        List<String> collect1 = names.stream().map((s) -> {
            System.out.println(s + " " + Thread.currentThread().getName());
            return s.toUpperCase();
        }).collect(Collectors.toList());
        collect1.forEach(System.out::println);
        System.out.println("#######################");

        //스트림의 parallelStream()을 사용하면 JVM이 알아서 병렬적으로 처리해준다.
        //다만 병렬처리를 한다고해서 무조건 빨라지는 것은 아니다.
        //병렬처리도 비용이 든다. 쓰레드를 만들고, 병렬적으로 처리한 것을 수집해야하고, 쓰레드 간에 왔다갔다하는 컨텍스트 스위치의 비용이 한 쓰레드에서 처리하는 비용보다 더 클 수도있다.
        //다만 데이터가 정말 방대하게 큰 경우, 병렬처리는 도움이 된다. 그러나 대부분의 경우에는 병렬처리가 필요없다. 병렬처리는 데이터 소스와 중간 오퍼레이션에서 처리하는 로직에 따라 많이 달라지기 때문에 특정상황에서 좋다고 말할 수 없다.
        //케이스별로 성능을 측정해서 병렬처리를 사용여부를 결정하는 것이 바람직하다.
        List<String> collect2 = names.parallelStream().map((s) -> {
            System.out.println(s + " " + Thread.currentThread().getName());
            return s.toUpperCase();
        }).collect(Collectors.toList());
        collect2.forEach(System.out::println);

    }
}

[참고자료]

더 자바, Java 8, 백기선

java.util.stream

Interface Stream<T>

반응형

+ Recent posts