Study/Java

자바 기본서를 다시 읽다. 7 - 스트림의 개념과 파이프라인

going.yoon 2022. 4. 5. 23:39

스트림의 개념과 특징

스트림(Stream)이란, 컬렉션의 저장 요소를 하나씩 참조해서 람다식으로 처리할 수 있도록 해주는 반복자를 말한다.

// Iterator 와 Stream(람다식 사용)의 비교
public class IterationStreamExample {
    public static void main (String[] args){
        List<String> strings = Arrays.asList("홍길동","신용권","김자바");

        Iterator<String> iterator = strings.iterator();
        while(iterator.hasNext()){
            String name = iterator.next();
            System.out.println("name is : " + name);
        }

        Stream<String> stream = strings.stream();
        // Stream의 for Each는 Consumer 함수적 인터페이스 타입의 매개값을 가진다.
        stream.forEach(name-> System.out.println("name is : " +name));
    }
}

 

스트림은 아래와 같이 3가지 특징을 지닌다.

  • 람다식으로 요소 처리 코드를 제공한다.
  • 내부 반복자를 사용하므로 병렬 처리가 쉽다.
  • 중간 처리와 최종 처리를 할 수 있다.

 

각각의 특징들을 예시를 통해 알아보자.

public class IterationStreamExample {
    public static void main (String[] args){

        /**1. 람다식으로 요소 처리 코드를 제공한다.*/
        List<Student> list = Arrays.asList(
                new Student("윤가영",100, 100),
                new Student("김말숙", 20, 30)
        );
        Stream<Student> studentStream = list.stream();
        studentStream.forEach(s-> System.out.println(
                // s -> 람다식으로 요소를 처리하기 위해 stream 값을 매개 변수로 넘김
                "name : " + s.getName() +
                " englishScore : " + s.getEnglishScore() +
                " mathScore : " + s.getMathScore()
                ));

        /**2. 내부 반복자를 사용하므로 병렬 처리가 쉽다.*/
        List<String> list2 = Arrays.asList("홍길동","청길동","홍길금","홍길은","청길금","청길은");
        Stream<String> basicStream = list2.stream();
        basicStream.forEach(IterationStreamExample::print);

        Stream<String> parellelStream = list2.parallelStream();
        parellelStream.forEach(IterationStreamExample::print);


        /**3. 중간 처리와 최종 처리를 할 수 있다.*/
        double avgEnglishScore = list.stream()
                .mapToInt(Student::getEnglishScore)
                .average()
                .getAsDouble();
        System.out.println("평균 영어점수 : " + avgEnglishScore);

    }

    public static void print(String str){
        System.out.println(str + " : " + Thread.currentThread().getName());
    }
}

 

 

실행 결과를 살펴보면, parallelStream()을 호출했을 때의 Thread name이 ForkJoinPool.commonPool-worker-# 으로 찍힌 것을 확인할 수 있다. 이처럼 병렬(Parallel) 처리는 한가지의 작업을 런타임시에 여러개의 서브 작업으로 나누고, 서브작업의 결과를 자동으로 결합해서 최종 결과물을 생성한다. 이때 병렬 처리 스트림은 main스레드를 포함해서 ForkJoinPool(스레드풀)을 사용하여 요소를 처리한다.

 

스트림 파이프라인

스트림은 데이터의 필터링, 매핑, 정렬, 그룹핑 등의 중간 처리와 합계, 평균, 카운팅, 최대최소 같은 리덕션 처리를 파이프라인을 통해 해결한다. 파이프 라인이란, 여러개의 스트림이 연결되어있으며 최종처리를 제외하고는 전부 중간처리 스트림이라고 부른다.

 

이 파이프 라인의 실행 순서는 아래와 같다.

  • 중간 처리 스트림 1 -> 중간처리 스트림 2 --> 중략--> 최종 처리 스트림 (X)
  • 최종 처리 스트림 -> 중간처리 스트림1 -> 중간처리 스트림 2 ---> 중략 (O)

이렇듯, 최종 처리가 시작되기 전까지 중간 처리는 지연(Lazy) 된다.

List<Student> list = Arrays.asList(
        new Student("윤가영",100, 100),
        new Student("김말숙", 20, 30)
);

double ageAvg = list.stream() // 오리지널 스트림
        .filter(m-> m.getEnglishScore()>=80) // 중간스트림
        .mapToInt(Student::getMathScore) // 중간스트림
        .average() // 최종처리
        .getAsDouble();

System.out.println("영어점수가 80 점 이상인 사람들의 수학 평균점수 : " + ageAvg);

 

위의 코드에서 볼 때 처리 순서는 average(최종 처리) -> filter -> mapToInt의 순서로 진행 될 것이다.

cf) getAsDouble은 java.util.OptionalDouble 의 메소드이기 때문에 스트림이 아니다.

java.util.OptionalDouble 의 getAsDouble 메소드