개발언어/JAVA

[모던자바인액션] 2. 스트림 정리

nomoreFt 2022. 4. 5. 14:53

스트림

Collection이란?

image

자바에서는 이 Collection으로 데이터를 그룹화하고 처리한다. 모든 요리의 칼로리 합이라던지, 칼로리가 50 이하인 음식을 고른다던지 하는 식이다.

데이터베이스에서는 선언형으로 SELECT name FROM dishes WHERE calorie 이런 식으로 구현한다.

위에서 봤듯, SQL 질의는 요리의 속성을 이용하여 어떻게 필터링할 것인지 구현할 필요가 없다. (자바처럼 반복자, 누적자 등을 이용하는 것)

자바 8에서는 이런 식으로 Collection을 처리하고 싶었다. 성능 좋게 멀티코어 아키텍처를 활용해서 병렬적으로. 그렇기에 스트림은 탄생했다.

스트림이란?

스트림을 이용하면 선언형 (즉, 데이터를 처리하는 임시 구현 코드 대신 질의로 표현)으로 Collection 데이터를 처리할 수 있다. 그리고 스트림을 사용하면 멀티스레드 구현을 하지 않더라도 데이터를 투명하게 병렬로 처리할 수 있다.

정의 : 스트림이란 '데이터 처리 연산을 지원하도록 소스에서 추출된 연속된 요소'

구성 요소

  • 데이터 처리 연산 : 스트림은 함수형 프로그래밍 언어에서 일반적으로 지원하는 연산과 데이터베이스와 비슷한 연산을 지원. EX) filter, map, reduce, find, match, sort 등으로 데이터 조작
  • 소스 : 스트림은 컬렉션, 배열, I/O 자원 등의 데이터 제공 소스로부터 데이터를 소비한다. (정렬된 컬렉션으로 스트림을 생성하면 정렬이 유지된다.)
  • 연속된 요소 : 컬렉션은 자료구조이므로 컬렉션에서는 ArrayList를 쓸지, LinkedList를 쓸지에 대한 고민과 같은 시간,공간복잡성 등 데이터 성질 주체이다. 스트림은 연속된 값 집합의 인터페이스를 제공하여 계산을 주체로 사용된다. *( 여기서 연속된이란 의미는 순차적으로 값에 접근하는 것이다)

두가지 특징

  • 파이프라이닝 : 대부분의 스트림 연산은 스트림 연산끼리 연결해 스트림 자신을 반환한다.
  • 내부 반복 : 반복자를 명시하여 반복하는 컬렉션과 다르게 내부 반복을 지원한다.
  • 게으른 생성 : 필요할 때만 값을 계산한다.

예시 자바7

public static List<String> getLowCaloricDishesNamesInJava7(List<Dish> dishes) {
    List<Dish> lowCaloricDishes = new ArrayList<>();
    for (Dish d : dishes) {
      if (d.getCalories() < 400) {
        lowCaloricDishes.add(d);
      }
    }
    List<String> lowCaloricDishesName = new ArrayList<>();
    Collections.sort(lowCaloricDishes, new Comparator<Dish>() {
      @Override
      public int compare(Dish d1, Dish d2) {
        return Integer.compare(d1.getCalories(), d2.getCalories());
      }
    });
    for (Dish d : lowCaloricDishes) {
      lowCaloricDishesName.add(d.getName());
    }
    return lowCaloricDishesName;
  }

예시 자바8

public static List<String> getLowCaloricDishesNamesInJava8(List<Dish> dishes) {
    return dishes.stream()
        .filter(d -> d.getCalories() < 400)
        .sorted(comparing(Dish::getCalories))
        .map(Dish::getName)
        .collect(toList());
  }

작동 순서
dishes 리스트에서 stream 메서드를 호출하여 스트림을 얻었다.
데이터 소스 : dishes (연속된 요소) 를 스트림에 제공한다.
filter, sort, map, collect 같은 데이터 처리 연산을 적용한다.
*collect를 제외한 모든 연산은 서로 파이프라인으로 연결되도록 스트림을 반환한다.
마지막으로 collect로 스트림을 List로 변환하여 배출한다.

**마지막 collect 호출 전까지는 무엇도 선택되지 않았고 출력 결과도 없다. (메서드 호출이 collect 이전까지 저장됨)

filter : 람다를 인수로 받아 스트림에서 특정 요소 제외

map : 람다를 이용해서 한 요소를 다른 요소로 변환하거나 정보를 추출

limit : 정해진 개수 이상으로 요소가 스트림에 저장되지 못하게 스트림 크기 축소

collect : 스트림을 다른 형식으로 변환한다.

image

컬렉션 vs 스트림

컬렉션은 DVD이고, 스트림은 인터넷 스트리밍이다.

(DVD는 영화 전체가 로딩되고 나서야 재생 가능하고, 스트리밍은 들어오는 순서부터 바로 보여주기 시작한다.)

컬렉션은 팔기도 전에 창고를 가득 채우고 스트림은 데이터를 요청할 때만 값을 계산한다.

수학적으로 이는 굉장히 큰 값어치를 가진다.

예를 들어 무제한의 소수를 포함하는 스트림과 컬렉션이 있을 때, 컬렉션은 모든 소수를 다 구하고 값을 사용자에게 반환하려 할 것이다 그렇다면 무한 루프를 돌고 사용자는 평생 결과값을 받을 수 없을 것이다.

반면에 스트림은 소수가 하나하나 요청이 들어올 때 마다 결과를 반환하기 때문에 무제한의 수에도 연산이 가능하다.

이를 DVD는 '적극적 생성' 이라 하고 스트림은 '게으른 생성' 이라고 칭한다.

  • 스트림은 딱 한번만 탐색할 수 있다. (흩어짐)
  • 컬렉션은 외부반복, 스트림은 내부반복이다.
//기존 컬렉션 처리 방법
List<String> names = new ArrayList<>();
for(Dish dish:menu){
    names.add(dish.getName()); //<- 메뉴리스트를 명시적으로 순차 반복한다.
}

//스트림
List<String> name = menu.stream
                                        .map(Dish::getName)
                                        .collect(toList()); //파이프라인을 실행한다. 반복자는 필요 없다.

아이에게 장난감을 치우라고 지시할 때, 장난감이 있냐고 물어본 뒤, 그 장난감을 치우고 다음 장난감은 무엇이 있지? 그리고 치워를 반복하는 것과 바닥에 있는 모든 장난감을 치우렴의 차이이다.

  • Quiz
    • Ans
  • 다음 Collection코드를 Stream으로 바꿔보자. List<String> highCaloricDishes = new ArrayList<>(); Iterator<String> iterator = menu.iterator(); while(iterator.hasNext(){ Dish dish = iterator.next(); if(dish.getCalories() > 300) { highCaloricDishes.add(d.getName()); } }

중간연산, 최종연산

스트림의 연산 중, 스트림을 반환하는 filter,map,limit 등은 중간연산이다.

collect같이 결과를 반환하는 것은 최종연산이다.

스트림의 게으른 특성 덕분에 중간 연산을 합친 다음 합쳐진 중간 연산을 최종 연산으로 한 번에 처리한다.

최종 연산은 스트림 파이프라인에서 결과를 도출한다. 보통 List, Integer,void 등 스트림 이외의 결과

image

요약

스트림 이용 과정은 다음 세 가지로 요약 가능하다.

  • 질의를 수행할 (컬렉션 같은) 데이터 소스
  • 스트림 파이프라인을 구성할 중간 연산 연결
  • 스트림 파이프라인을 실행하고 결과를 만들 최종 연산