스트림으로 데이터 수집

김종준·2023년 1월 25일
0

모던자바

목록 보기
5/15

스트림으로 데이터 수집

컬렉터란?

Collector 인터페이스 구현은 스트림의 요소를 어떤 식으로 도출할지 지정한다.

스트림에 collect를 호출하면 스트림의 요소에 리듀싱 연산이 수행된다.

collect에서는 리듀싱 연산을 이용해서 스트림의 각 요소를 방문하면서 컬렉터가 작업을 처리한다.

보통 함수를 요소로 변환할 때는 컬렉터를 적용하며 최종 결과를 저장하는 자료구조에 값을 누적한다.

Collector 인터페이스의 메서드를 어떻게 구현하느냐에 따라 스트림에 어떤 리듀싱 연산을 수행할지 결정된다.

Collectors 유틸리티 클래스는 자주 사용하는 컬렉터 인터페이스를 손쉽게 생성할 수 있는 정적 팩토리 메서드를 제공한다.

Collectors

Collectors는 크게 세 가지 기능을 제공한다.

  • 스트림 요소를 하나의 값으로 리듀스하고 요약
  • 요소 그룹화
  • 요소 분활

collect와 reduce의 차이

collect 메서드는 도출하려는 결과를 누적하는 컨테이너를 바꾸도록 설계된 메서드이다.

하지만 reduce는 두 값을 하나로 도출하는 불변형 연산이라는 점에서 collect와 차이가 있다.

그룹화

groupingBy를 이용해서 쉽게 그룹화를 할 수 있다.

menu.stream().collect(groupingBy(Dish::getType));

이때 기본적인 반환값은 groupingBy의 파라미터로 값을 키로 하는 맵으로 반환된다.

그리고 그룹화된 것을 다시 필터링 하고 싶다면 filtering 메서드를 사용할 수 있다.

menu.stream().collect(groupingBy(Dish::getType, filtering(dish -> dish.getCalories() > 500, toList())));

Collectors 클래스의 또 다른 정적 팩토리 메서드인 filtering 메서드를 인수로 받아 각 그룹의 요소와 필터링 된 요소를 재그룹화한다.

예제 하나를 더 보면 다음과 같다.

menu.stream().collect(groupingBy(Dish::getType, 
                                 flatMapping(dish -> dishTags.get(dish.getName()),stream, toSet()))
                     );

이를 통해 알 수 있는 것은

groupingBy의 첫 파라미터로는 그룹화할 기준인 를 설정할 수 있고,

두 번째 파라미터에는 키를 기준으로 나뉜 그룹의 요소를 조작할 수 있다는 것이다.

그렇기에 아래 처럼 다수준 그룹화도 가능해진다.

menu.stream().collect(
  groupingBy(Dish::getType, // 1차 그룹화
             groupingBy((Dish dish) -> { // 2차 그룹화
               if (dish.getCalories() <= 400) {
                 return CaloricLevel.DIET;
               }
               else if (dish.getCalories() <= 700) {
                 return CaloricLevel.NORMAL;
               }
               else {
                 return CaloricLevel.FAT;
               }
             })
            )
);

또 컬렉터 결과를 다른 형식에 적용할 수도 있는데 이는 collectingAndThen으로 컬렉터가 반환한 결과를 다른 형식으로 활용할 수 있다.

menu.stream().collect(
  groupingBy(Dish::getType,
             collectingAndThen(
               reducing((d1, d2) -> d1.getCalories() > d2.getCalories() ? d1 : d2), // Optional<Dish> 반환
               Optional::get))); // Optional의 get() 적용하여 반환 형식 변환

분할

분할 함수는 불리언을 반환하므로 맵의 키 형식은 Boolean이다.

그렇기에 분할은 특수한 종류의 그룹화라고 할 수 있다.

이는 partitioningBy 메서드에 의해 분할된다.

예시는 아래와 같다.

menu.stream().collect(partitioningBy(Dish::isVegetarian));

Collector 인터페이스

Collector 인터페이스는 리듀싱 연산을 어떻게 구현할지 제공하는 메서드 집합으로 구성된다.

아래는 Collector 인터페이스의 시그니처와 다섯 개의 메서드 정의이다.

public interface Collector<T, A, R> {
  Supplier<A> supplier();
  BiConsumer<A,T> accumulator();
  Function<A,R> finisher();
  BinaryOperator<A> combiner();
  Set<Characteristics> characteristics();
}
  • T는 수집될 스트림 항목의 제네릭 형식이다.
  • A는 누적자, 즉 수집 과정에서 중간 결과를 누적하는 객체의 형식이다.
  • R은 수집 연산 결과 객체의 형식(대개 컬렉션 형식)이다.

supplier

supplier 메서드는 빈 결과로 이루어진 Supplier을 반환한다.

즉, supplier는 수집 과정에서 빈 누적자 인스턴스를 만드는 파라미터가 없는 함수다.

accumulator

accumulator 메서드는 리듀싱 연산을 수행하는 함수를 반환한다.

함수의 반환값은 void, 즉 요소를 탐색하면서 적용하는 함수에 의해 누적 내부 상태가 바뀌므로 누적자가 어떤 값일지 단정할 수 없다.

finisher

finisher 메서드는 스트림 탐색을 끝내고 누적자 객체를 최종 결과로 변환하면서 누적 과정을 끝낼 때 호출할 함수를 반환해야한다.

combiner

combiner 메서는 스트림의 서로 다른 서브파트를 병렬로 처리할 때 누적자가 이 결과를 어떻게 처리할지 정의한다.

characteristics

characteristics 메서드는 컬렉터의 연산을 정의하는 Characteristics 형식의 불변 집합을 반환한다.

public class ToListCollector<T> implements Collector<T, List<T>, List<T>> {

  @Override
  public Supplier<List<T>> supplier() {
    return () -> new ArrayList<T>();
  }

  @Override
  public BiConsumer<List<T>, T> accumulator() {
    return (list, item) -> list.add(item);
  }

  @Override
  public Function<List<T>, List<T>> finisher() {
    return i -> i;
  }

  @Override
  public BinaryOperator<List<T>> combiner() {
    return (list1, list2) -> {
      list1.addAll(list2);
      return list1;
    };
  }

  @Override
  public Set<Characteristics> characteristics() {
    return Collections.unmodifiableSet(EnumSet.of(IDENTITY_FINISH, CONCURRENT));
  }

}

위 코드는 toList()와 동일한 코드를 Collector 인터페이스를 구현하여 만든 것이다.

위와 같이 복잡하게 만들 수도 있지만 아래처럼 간단하게 만들 수도 있다.

List<Dish> dishes = menuStream.collect(ArrayList::new, List::add, List::addAll);

하지만 이보다는 적절한 커스텀 컬렉터를 구현하는 편이 중복을 피하고 재사용성을 높이는 데 도움이 된다고 한다.

0개의 댓글