6장: 스트림으로 데이터 수집

컬렉터로 가독성 좋게 데이터를 모아보자.

Collector란?

  • 스트림의 collect 메서드에 Collector 인터페이스 구현을 넘겨 스트림의 요소를 어떤 식으로 도출할지 지정한다.

  • 예를 들어 toList()는 리스트를 반환하도록 하고, groupingBy()는 주어진 키에 대응하는 리스트를 갖는 맵 형태로 반환하도록 한다.

고급 리듀싱 기능 수행

  • 컬렉터의 최대 강점은 collect로 결과를 수집하는 과정을 간단하면서도 유연한 방식으로 정의할 수 있다는 점이다.

  • 스트림에서 collect를 호출하면 리듀싱 연산을 이용해서 스트림의 각 요소를 방문하면서 컬렉터가 자동으로 작업을 처리해준다.

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

미리 정의해 둔 컬렉터

  • Collectors 클래스는 자주 사용하는 컬렉터 인스턴스를 손쉽게 생성할 수 있는 정적 메서드를 제공한다.

  • Collectors에서 제공하는 메서드의 기능은 크게 세 가지로 구분되며, 앞으로 이 세 가지를 공부해볼 것이다.

    • 스트림 요소를 하나의 값으로 리듀스 및 요약

      • 리스트에서 총합을 구하는 등의 다양한 연산을 수행한다.

    • 스트림 요소 그룹화

      • 특정 키를 이용해 그룹화하거나 서브 그룹에 추가로 리듀싱 연산을 적용하는 등의 작업이 가능하다.

    • 스트림 요소 분할

      • Predicate를 그룹화 함수로 사용한다.

리듀싱과 요약

  • 컬렉터는 스트림의 모든 항목을 하나의 결과로 합칠 수 있다.

  • 아래는 counting() 팩토리 메서드가 반환하는 컬렉터를 사용해 메뉴의 총 개수를 구하는 코드이다.

최댓값, 최솟값 찾기

  • 스트림 Collectors.maxBy, Collectors.minBy 메서드를 이용해서 스트림의 최댓값과 최솟값을 계산할 수 있다

  • 두 컬렉터는 스트림의 요소를 비교하는 데 사용할 Comparator를 입력 인자로 받는다.

요약 연산

  • 리듀싱 기능을 사용해 스트림에 있는 객체의 숫자 필드의 합계나 평균 등을 반환하는 연산

  • 합계

    • Collectors.summingInt, summingDouble, summingLong 메서드를 통해 합계를 구하는 요약 연산을 수행하는 Collector를 얻을 수 있다.

    • Collectors.summingInt 메서드는 객체를 int로 매핑하는 함수를 인수로 받고, 인수로 전달된 함수는 객체를 int로 매핑한 컬렉터를 반환한다. 그리고 summingInt가 collect 메서드로 전달되면 요약 작업을 수행한다.

    • 다음은 메뉴 리스트의 총 칼로리를 계산하는 코드다.

  • 평균값 계산

    • Collectors.averagingInt, averagingLong, averagingDouble 정적 메서드를 통해 제공된다.

  • 합계, 평균, 최소값 및 최대값을 한번에 구하기

    • 만약 두개 이상의 연산이 한번에 수행되어야 한다면 Collectors.summarizingInt 정적 메서드가 반환하는 컬렉터를 사용할 수 있다.

문자열 연결

  • 컬렉터에 joining 팩토리 메서드를 이용하면 스트림의 각 객체에 toString 메서드를 호출해서 추출한 모든 문자열을 하나의 문자열로 연결해서 반환한다.

  • 내부적으로 StringBuilder를 이용해 문자열을 하나로 만든다.

  • joining 메서드의 인자로 구분자를 넣을 수 있다.

범용 리듀싱 요약 연산

  • reducing 팩토리 메서드를 사용해 지금까지 다룬 모든 컬렉터를 정의할 수도 있지만, 가독성과 편의성을 위해 특화된 컬렉터를 따로 제공하고 있다.

  • reducing 메서드의 입력 인자

    • 리듀싱 연산의 시작 값 혹은 스트림이 비었을 때의 반환값

    • 변환(매핑) 함수

    • 같은 종류의 두 항목을 더해 하나의 값으로 만드는 BinaryOperator이다.

  • 다음은 리듀싱을 사용해 칼로리의 합계와 최대 칼로리를 가진 메뉴를 구하는 코드이다.

  • collect 메서드는 도출하려는 결과를 누적하는 컨테이너를 바꾸도록 설계되었으나, reduce는 두 값을 하나로 도출하는 불변형 연산이다.

  • 따라서 가변 컨테이너 관련 작업이면서 병렬성을 확보하려면 reduce 메서드 대신 collect 메서드를 사용해 리듀싱 연산을 구현해야 한다.

  • 잘 이해가 가지 않으니 7장에 가서 자세히 알아보도록 하자...😓

자신의 상황에 맞는 최적의 해법 선택

  • 함수형 프로그래밍에서는 하나의 연산을 다양한 방법으로 해결할 수 있다.

  • 컬렉터를 이용하면 스트림 인터페이스에서 직접 제공하는 메서드를 이용하는 것에 비해 코드가 복잡해지지만, 재사용성과 커스터마이즈 가능성을 제공하여 높은 수준의 추상화와 일반화를 얻을 수 있다.

  • 모든 메뉴의 칼로리 합계를 구할 때 아래와 같이 다양한 방법을 사용할 수 있다.

그룹화

  • 분류 함수

    • 스트림을 그룹화하는 기준이 되는 함수

    • groupingBy 메서드의 인자로 전달되는 함수가 이에 해당

    • 예를 들어 각 요리에서 Dish.Type과 일치하는 모든 요리를 추출하는 함수

  • 그룹화 연산의 결과로는 "그룹화 함수가 반환하는 값을 Key로, 그리고 키에 해당하는 스트림 요소의 리스트를 Value로 갖는 맵"이 반환된다.

  • 단순한 속성 접근자 대신 더 복잡한 분류 기준이 필요한 상황에서는 메서드 참조를 분류 함수로 사용할 수 없다.

  • 다음 코드는 400 칼로리 이하를 'diet', 400~700칼로리를 'normal', 700칼로리 이상을 'fat' 요리로 분류하여 Map 형태로 저장한다.

그룹화된 요소 조작

필터링

  • 그룹의 요소 중 특정 조건으로 필터링하려면 groupingBy() 팩토리 메서드를 오버로드해 두번째 인자로 filtering() 메서드를 넣어주어야 한다.

  • 미리 조건으로 필터링한 후 groupingBy()를 수행하면 조건에 해당하지 않는 키가 아예 누락되는 문제가 있다.

매핑

  • 매핑 함수를 이용해 요소를 반환할 수 있다.

  • flatMap 변환 역시 사용 가능하다.

다수준 그룹화

  • 여러 조건을 기준으로 이중, 삼중맵으로 그룹화할 수 있다.

  • groupingBy 메서드 내부에 두번째 기준을 정의하는 groupingBy 메서드를 전달해 이중맵으로 스트림의 항목을 그룹화할 수 있다.

  • 보통 groupingBy의 연산을 '버킷(물건을 담을 수 있는 양동이)' 개념으로 생각하면 쉽다.

  • 첫 번째 groupingBy는 각 키의 버킷을 만든다. 그리고 준비된 각각의 버킷을 서브스트림 컬렉터로 채우기를 반복하면 n수준의 그룹화가 가능하다.

  • 다음 코드는 메뉴 종류에 따라 1차 분류한 후, 칼로리 정도에 따라 2차 분류하는 방식이다.

서브 그룹으로 데이터 수집

  • 분류 함수 한개만 인수를 갖는 groupingBy(f) 메서드는 사실 groupingBy(f, toList())의 축약형이다.

  • groupingBy 메서드는 두번째 인수로 다양한 컬렉터를 전달받을 수 있어, 이를 이용해 스트림에서 같은 그룹으로 분류된 모든 요소에 리듀싱 작업을 수행할 수 있다.

  • 아래는 counting 컬렉터를 입력하는 코드이다.

  • Collectors.collectingAndThen 팩토리 메서드는 컬렉터가 반환한 결과를 변환 함수에 적용시켜 다른 컬렉터를 반환한다.

  • 아래와 같이 Optional.get()으로 Optional의 값을 꺼낼 수 있으며, 이 때 리듀싱컬렉터는 절대 Optional.empty()를 반환하지 않으므로 get()을 호출해도 안전하다.

여러 컬렉터 예제

  • summingInt 컬렉터를 사용해 메뉴 종류에 따른 칼로리의 합계를 반환한다.

  • mapping 컬렉터를 사용해 메뉴 종류에 따른 칼로리 분류 셋을 반환한다.

  • toCollection을 사용해 원하는 객체로 반환하도록 할 수 있다.

분할

  • 분할 함수를 분류 함수로 사용하는 특수한 그룹화 기능이다.

  • 분할 함수(boolean을 반환하는 Predicate)를 분류 함수로 사용하기 때문에 결과 맵의 키 형식은 Boolean이다.

  • 다음 코드는 채식 메뉴이면 true 그룹에, 채식 메뉴가 아니면 false 그룹에 들어가도록 한다.

장점

  • 분할 함수가 반환하는 참, 거짓의 스트림 리스트를 모두 유지한다.

  • partitioningBy 메서드의 두번째 인자로 컬렉터를 전달할 수도 있다.

  • partitioningBy 메서드의 두번째 인자로 partitioningBy 메서드를 중첩할 수도 있다.

소수와 비소수로 분할

  • 정수 n을 인수로 받아서 2에서 n까지의 자연수를 소수와 비소수로 나누어보자.

  • 소수이면 true 그룹에, 비소수이면 false 그룹에 들어가도록 한다.

  • 먼저 소수인지 판별하는 메서드부터 만든다. (특정 수의 제곱근보다 작은 수로 특정 수를 나눌 수 없으면 소수이다.)

Collector 인터페이스

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

메서드 종류

  • 위 코드는 Collector 인터페이스의 5가지 메서드를 나타낸다.

    • supplier: 새로운 결과 컨테이너를 만든다.

    • accumulator: 결과 컨테이너에 요소를 추가한다.

    • finisher: 최종 변환값을 결과 컨테이너로 적용한다.

    • combiner: 두 결과 컨테이너를 병합한다.

    • characteristics: collect 메서드가 어떤 최적화를 이용해 리듀싱 연산을 수행할 것인지 결정하도록 돕는 힌트 특성 집합을 제공한다.

  • T는 수집될 스트림 항목의 제네릭 형식이다.

  • A는 누적자. 즉 수집 과정에서 중간 결과를 누적하는 객체의 형식이다.

  • R은 수집 연산 결과 객체의 형식(보통 컬렉션 형식)이다.

supplier

  • 새로운 결과 컨테이너를 만들기 위해 빈 결과로 이루어진 Supplier를 반환해야 한다.

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

  • ToListCollector처럼 누적자를 반환하는 컬렉터에서는 빈 누적자가 비어있는 스트림의 수집 과정의 결과가 될 수 있다.

accumulator

  • 리듀싱 연산을 수행하는 함수를 반환한다.

  • 스트림에서 n번째 요소를 탐색할 때, n-1번째 항목까지 수집한 누적자와 n번째 요소를 함수에 적용한다.

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

  • ToListCollector에서 accumulator가 반환하는 함수는 기존 리스트(누적자)에 현재 항목을 추가한다.

finisher

  • 스트림 탐색을 끝내고 누적자 객체를 최종 객체로 변환할 때 호출할 함수를 반환해야 한다.

  • ToListCollector는 누적자 객체가 최종 결과이기 때문에 변환 과정이 필요없어 항등 함수를 반환한다.

supplier -> accumulator -> finisher

combiner

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

  • toList의 combiner는 간단하게 스트림의 두 번째 서브 파트에서 수집한 항목 리스트를 첫 번째 서브파트 결과 리스트의 뒤에 추가하면 된다.

  • 스트림의 리듀싱을 병렬로 처리할 수 있도록 해주는 메서드이다.

  • 병렬 리듀싱 수행 과정

    • 스트림을 언제까지 분할할지에 대한 조건에 맞추어 스트림을 재귀적으로 분할해 서브스트림들을 만든다.

    • 모든 서브스트림의 각 요소에 리듀싱 연산을 순차적으로 적용해 병렬로 처리한다.

    • 컬렉터의 combiner 메서드가 반환하는 함수를 사용해 서브스트림의 결과들을 합친다.

    processing parallel stream

Characteristics

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

  • Characteristics는 스트림을 병렬로 리듀스 할 지 여부와, 병렬로 리듀스 시 선택할 최적화에 대한 힌트를 제공한다.

  • Characteristics의 항목

    • UNORDERED : 리듀싱 결과는 스트림 요소의 방문 순서나 누적 순서에 영향을 받지 않는다.

    • CONCURRENT : 다중 스레드에서 accumulator 함수를 호출할 수 있으며, 이 컬렉터는 스트림의 병렬 리듀싱을 수행할 수 있다. 컬렉터의 플래그에 UNORDERED를 함께 설정하지 않았다면 데이터 소스가 정렬되어있지 않은 상황에서만 병렬 리듀싱을 수행할 수 있다.

    • IDENTITY_FINISH : 최종 결과로 누적자 객체를 바로 사용하는 경우 finisher 메서드가 반환하는 함수는 단순히 identity를 적용할 뿐이므로 이를 생략할 수 있다. 또한 누적자 A를 결과 R로 안전하게 형변환할 수 있다.

정리

  • 아래와 같이 ToListCollector를 구현할 수 있다.

  • 컬렉터 구현 클래스를 만드는 대신 세 함수(발행, 누적, 합침)를 인수로 받는 collect메서드를 사용할 수 있다.

  • 단, IDENTITY_FINISH 수집 연산일 때 해당된다.

  • 세 함수는 Collector 인터페이스의 메서드가 반환하는 함수와 같은 기능을 수행한다.

커스텀 컬렉터

  • 어떤 수가 소수인지 판별하기 위해서는, 소수로만 나누어떨어지는지 확인하면 된다.

  • 일반적으로는 컬렉터 수집 과정에서 부분 결과에 접근할 수 없으므로 커스텀 컬렉터 클래스를 통해 접근할 수 있다.

  • 아래 isPrime메서드에서는 primes 리스트를 입력받도록 했다.

  • 대상의 제곱근보다 큰 소수를 찾으면 검사를 중단하기 위해서는 takeWhile 메서드를 사용한다.

커스텀 컬렉터 만들기

  • public interface Collector<T, A, R> 에서 T는 스트림 요소의 형식, A는 중간 결과 누적 객체의 형식, R은 collect 연산의 최종 결과 형식을 의미한다.

  • 우리가 만드는 컬렉터는 소수는 key가 true인 리스트에 저장하고, 비소수는 key가 false인 리스트에 저장한다.

  • 커스텀 컬렉터를 아래와 같이 사용할 수 있다.

  • 혹은 collect 메서드 인자로 supplier, accumulator, combiner 함수를 직접 넣어 사용할 수도 있다. (가독성과 재사용성이 떨어지기 때문에 코드는 생략하겠습니다 🙂)

Last updated