• 간단한 Stream API 사용 예제

    • Java 8 이전의 코드

      List<Dish> lowCaloricDishes = new ArrayList<>();
      for(Dish dish : menu) {
          if(dish.getCalories() < 400) {
              lowCaloricDishes.add(dish)
          }
      }
      Collections.sort(lowCaloricDishes, new Comparator<Dish>() {
          public int compare(Dish dish1, Dish dish2) {
              return Integer.compare(dish1.getCalories(), dish2.getCalories());
          };
      });
      List<String> lowCaloriesDishesName = new ArrayList<>();
      for(Dish dish : lowCaloricDishes) {
          lowCaloriesDishesName.add(dish.getName());
      }
      
    • Java 8 이후의 코드

      // 기본적인 사용
      List<String> lowCaloricDishesName =
          menu.stream()
              .filter(d -> d.getCalories() < 400)
              .sorted(comparing(Dish::getCalories))
              .map(Dish::getName)
              .collect(toList());
      
      // 아래처럼 parallelStream 사용하면 멀티코어 아키텍처에서 병렬로 실행 가능
      List<String> lowCaloricDishesName =
          menu.parallelStream()
              .filter(d -> d.getCalories() < 400)
              .sorted(comparing(Dish::getCalories))
              .map(Dish::getName)
              .collect(toList());
      
  • Java 8 Stream API의 특징

    • 선언형: 더 간결하고 가독성이 좋아진다.
    • 조립할 수 있음: 유연성이 좋아짐
    • 병렬화: 성능이 좋아짐
  • Stream 이란

    • 연속된 요소
      • 컬렉션과 마찬가지로 스트림은 특정 요소 형식으로 이루어진 연속된 값 집합의 인터페이스 제공
      • 컬렉션은 자료구조이므로 컬렉션에는 시간과 공간의 복잡성과 관련된 요소 및 접근 연산이 주를 이룸(예 → ArrayList/LinkedList 선택)
      • 즉, 컬렉션의 주제는 데이터이고 스트림의 주제는 계산
    • 소스
      • 스트림은 컬렉션, 배열, I/O 자원 등의 데이터 제공 소스로부터 데이터를 소비
      • 즉, 정렬된 컬렉션을 스트림으로 생성하면 정렬이 유지
    • 데이터 처리 연산
      • 스트림은 함수형 프로그래밍 언어에서 지원하는 연산과 데이터베이스와 비슷한 연산 지원
      • 예 → filter, map, reduce, find, match, sort 등으로 데이터 조작 가능
      • 스트림 연산은 순차/병렬로 실행 가능
  • Stream의 주요 특징

    • 파이프라이닝

      • 대부분의 스트림 연산은 스트림 연산끼리 연결되어 커다란 파이프라인을 구축할 수 있도록 스트림 반환
      • 이로 인해 게으름, 쇼트서킷 같은 최적화도 가능
      • 연산 파이프라인은 데이터 소스에 적용하는 데이터베이스 질의와 비슷
    • 내부 반복

      • 반복자를 이용해 명시적으로 반복하는 컬렉션과 달리 스트림은 내부 반복 지원
      • 즉, 컬렉션 → 외부반복, 스트림 → 내부반복
    • Stream을 활용한 예시 코드

      List<String> threeHighColoricDishNames = menu
      	.stream()
      	.filter(dish -> dish.getColories() > 400)
      	.map(Dish::getName)
      	.limit(3) // MySQL LIMIT처럼 사용됨(선착순 3개만 반환)
      	.toList();
      // 마지막 Collect 연산으로 파이프라인 형성할 수 있도록 스트림 반환
      // 마지막에 Collect 호출하기 전까지는 어떠것도 선택/출력되지 않음
      
  • 컬렉션과 스트림의 차이

    영화를 예제로 컬렉션과 스트림의 차이

    영화를 예제로 컬렉션과 스트림의 차이

    • 데이터 계산 시점
      • 컬렉션
        • 현재 자료구조가 포함되는 모든 값을 메모리에 저장하는 자료구조
        • 컬렉션에 요소 추가/삭제하기 위해서는 연산을 수행할 때마다 모든 요소를 메모리에 저장해야하며 추가하려는 요소는 미리 계산되어야 함
        • 즉, 컬렉션의 요소는 컬렉션에 추가하기 전에 계산되어야 함
        • 적극적으로 데이터 생산(생산자 중심 → 팔기도 전에 창고를 가득 채움)
      • 스트림
        • 요청할 때만 요소를 계산하는 고정된 자료구조(스트림에 요소를 추가/제거할 수 없음)
        • 즉, 사용자가 요청하는 값만 추출하는 것이 핵심
  • 외부 반복과 내부 반복

    Untitled

    • 외부 반복

      • 컬렉션 인터페이스를 사용하여 사용자가 직접 요소를 반복하는 것

      • 내부 요소를 하나하나 가져와서 처리하는 방식

      • 병렬성을 이용하기 위해서는 개발자가 직접 관리

      • 예) for문을 활용한 반복

      • 예시 코드

        // for-each 루프를 이용하는 외부반복
        List<String> names = new ArrayList<>();
        for(Dish dish : menu) {
        	names.add(dish.getName());
        }
        
        // 내부적으로 숨겨졌던 반복자를 사용한 외부반복
        List<String> names = new ArrayList<>();
        Iterator<String> menu.iterator();
        while(iterator.hasNext()) {
        	Dish dish = iterator.next();
        	names.add(dish.getName());
        }
        
    • 내부 반복

      • 반복을 알아서 처리하고 결과 스트림값을 어딘가에 저장해줌

      • 내부 반복을 사용하면 투명하게 병렬로 처리하거나 최적화된 순서로 처리 가능

      • 예시 코드

        // 스트림을 활용한 내부반복(반복을 알아서 처리)
        List<String> names = menu
        	.stream()
        	.map(Dish::getName)
        	.toList();
        
  • 스트림 연산 구분

    코드에서의 중간 연산과 최종 연산

    코드에서의 중간 연산과 최종 연산

    Untitled

    • 중간 연산
      • filter, sorted 같은 중간 연산은 다른 스트림을 반환, 이러한 중간 연산을 연결해 질의를 만들 수 있음
      • 중간 연산의 중요한 특징은 단말 연산을 스트림 파이프라인에 실행하기 전까지는 아무 연산도 수행하지 않는다는 것
      • 즉, 최종 연산을 하기 전까지는 결과를 알 수 없음
    • 최종 연산
      • 스트림 파이프라인에서 결과를 도출, 보통 최종 연산에 의해 List, Integer, void 등 스트림 이외의 결과 반환