6. 스트림으로 데이터 수집
collect 역시 다양한 요소 누적 방식을 인수로 받아 스트림을 최종 결과로 도출하는 리듀싱 연산 수행 가능.
다양한 요소 누적 방식은 Collector 인터페이스에 정의.
컬렉션(Collection), 컬렉터(Collector), collect를 헷깔리지 않도록 주의.
1. 컬렉터란 무엇인가?
- Collector 인터페이스 구현은 스트림의 요소를 어떤 식으로 도출할지 지정.
1.1 고급 리듀싱 기능을 수행하는 컬렉터
- 스트림에 collect를 호출하면 스트림의 요소에(컬렉터로 파라미터화된) 리듀싱 연산 수행.
- collect에서는 리듀싱 연산을 이용해 스트림의 각 요소를 방문하면서 컬렉터가 작업 처리.
- 보통 함수를 요소로 변환할 때는 컬렉터를 적용하며 최종 결과를 저장하는 자료구조에 값을 누적.
- Collector 인터페이스의 메서드를 어떻게 구현하느냐에 따라 스트림에 어떤 리듀싱 연산을 수행할지 결정.
1.2 미리 정의된 컬렉터
- groupingBy 같이 Collectors 클래스에서 제공되는 팩토리 메서드 기능 설명.
- Collectors에서 제공하는 메서드의 기능은 크게 3가지로 구분.
> 스트림 요소를 하나의 값을 리듀스 하고 요약
> 요소 그룹화
> 요소 분할
2. 리듀싱와 요약
- 컬렉터로 스트림의 항목을 컬렉션으로 재구성 가능.(컬렉터로 스트림의 모든 항목을 하나의 결과로 합칠수 있음)
- 트리를 구성하는 다수준 맵, 잔순한 정수 등 다양한 형식 도출 가능.
2.1 스트림에서 최대값과 최소값 검색
- Collectors.maxBy, Collectors.minBy 두 개의 메서드를 이용하여 스트림의 최대값, 최소값 계산 가능.
- 두 컬렉터는 스트림의 요소를 비교하는데 사용할 Comparator를 인수로 받음.
- 자바8은 값을 포함하거나 포함하지 않을 수 있는 컨테이너 Optional을 제공.
> max, min값은 반환 값이 포함되지 않을 수 있음.
- 스트림에 있는 객체의 숫자 필드의 합계나 평균 등을 반환하는 연산에도 리듀싱 기능이 자주 사용.
> 이러한 연산을 요약(summarization)연산 이라 부름.
2.2. 요약 연산
- Collectors 클래스는 Collectors.summingInt라는 특별한 요약 팩토리 메서드 제공.
> summingInt는 객체를 int로 매핑하는 함수를 인수로 받음.
> summingInt의 인수로 전달된 함수는 객체를 int로 매핑하는 컬렉터를 반환.
- 단순 합계 외에 평균값 계산 등의 연산도 요약 가능.
- 두 개 이상의 연산을 한번에 수행할 경우 펙토리 메서드 summarizaingInt가 반환하는 컬렉터 사용 가능.
- int뿐 아니라 long, double에 대응하는 메서드도 존재함.
2.3 문자열 연결
- 컬렉터에 joining 팩토리 메서드를 이용하면 스트림의 각 객체에 toString 메서드를 호출해서 추출한 모든 문자열을 하나의 문자열로 연결해서 반환.
- joining 메서드는 내부적으로 StringBuilder를 이용해 문자열을 하나로 만듦.
2.4 범용 리듀싱 요약 연산
- 지금까지 살펴본 모든 컬렉터는 reducing 팩토리 메서드로도 정의 가능.
> 즉, 범용 Collectors.reducing으로 구현 가능.
- 범용 팩토리 메서드 대신 특화된 컬렉터를 사용하는 이유는 프로그래밍적 편의성 때문.
- reducing은 세 개의 인수를 받음
1) 리듀싱 연산의 시작값이거나 스트림에 인수가 없을 때는 반환값임.
2) 변환 함수
3) 같은 종류의 두 항목을 하나의 값으로 더한ㄴ BinaryOperator.
- 한 개의 인수를 갖는 reducing 팩토리 메서드는 세 개의 인수를 갖은 reducing 메서드에서 스트림의 첫 번째 요소를 시작요소, 즉 첫 번째 인수로 받으며, 자신을 그대로 반환하는 항등 함수를 두 번째 인수로 받는 상황에 해당.
> 즉, 한 개의 인수를 갖는 reducing 컬렉터는 시작값이 없으므로 빈 스트림이 넘겨졌을 때 시작값이 설정되지 않은 상황.
> 한 개의 인수를 갖는 reducing은 Optional<?> 객체를 반환.
컬렉션 프레임워크 유연성: 같은 연산도 다양한 방식으로 수행 가능!
- 람다 표현식 대신 Integer 클래스의 sum 메서드 레퍼런스등을 이용하면 코드 단순화 가능.
자신의 상황에 맞는 최적의 해법 선택
- 함수형 프로그래밍에서는 하나의 연산을 다양한 방법으로 해결할 수 있음을 보여줌.
- 스트림 안터페이스에서 직접 제공하는 메서드를 이용하는 것에 비해 컬렉터를 이용하는 코드가 더 복잡.
- 코드가 복잡한 대신 재사용성과 커스터마이즈 가능성을 제공하는 높은 수준의 추상화와 일반화를 획득.
- 문제를 해결할 수 있는 다양한 해결 방법을 확인한 다음 가장 일반적으로 문제데 특화된 해결책을 고르는 것이 바람직.
3. 그룹화
- 데이터 집합을 하나 이상의 특성으로 분류해서 그룹화하는 연산도 데이터베이스에서 많이 수행되는 작업.
- 명령형으로 그룹화를 구현하려면 까다롭고, 할일이 많으며, 에러도 많이 발생.
- 함수형을 이용하면 가독성 있는 한 줄의 코드로 그룹화를 구현.
- Collectors.groupingBy를 이용하여 쉽게 그룹화 가능.
- 이 함수를 기준으로 스트림이 그룹화되므로 이를 분류 함수(classification function)이라고 부름.
- 단순한 속성 접근자 대신 더 복잡한 분류 기준이 필요한 상황에서는 메서드 레퍼런스를 분류 함수로 사용 불가.
> 이 경우, 람다 표현식으로 필요 로직 구현 가능.
3.1 다수준 그룹화
- 두 인수를 받는 팩토리 메서드 Collectors.groupingBy를 이용해 항목을 다수준으로 그룹화 가능.
- Collectors.groupingBy는 일반적인 분류 함수와 컬렉터를 인수로 받음.
- 바깥쪽 groupingBy 메서드에 스트림의 항목을 분류할 두 번째 기준을 정의하는 내부 groupingBy를 전달해서 두 수준으로 스트림의 항목 그룹화 가능
3.2 서브그룹으로 데이터 수집
- 첫 번째 groupingBy로 넘겨주는 컬렉터의 형식은 제한이 없음.
컬렉터 결과를 다른 형식에 적용
- 그룹화 연산에서 맵의 모든 값을 Optional처리할 필요 없음.
- 팩토리 메서드 Collectors.collectingAndThen으로 컬렉터가 반환한 결과를 다른 형식으로 활용 가능.
- 팩토리 메서드 collectingAndThen은 적용할 컬렉터와 변환 함수를 인수로 받아 다른 컬렉터를 반환.
- 반환되는 컬렉터는 기존 컬렉터의 래퍼 역활을 하며 collect의 마지막 과정에서 변환 함수로 자신이 반환하는 값을 매핑.
groupingBy와 함께 사용하는 다른 컬렉터 예제
- 일반적으로 스트림에서 같은 그룹으로 분류된 모든 요소에 리듀싱 작업을 수행할 때는 팩토리 메서드 groupingBy에 두 번째 인수로 전달한 컬렉터를 사용.
- 이 외에도 mapping 메서드로 만들어진 컬렉터도 groupingBy와 자주 사용.
- mapping 메서드는 스트림의 인수를 변환하는 함수와 변환 함수의 결과 객체를 누적하는 컬렉터를 인수로 받음.
- mapping은 입력 요소를 누적하기 전에 매핑 함수를 적용해서 다양한 형식의 객체를 주어진 형식의 컬렉터에 맞게 변환하는 역할.
4. 분할
- 분할 함수(patitioning function)라 불리는 프레디케이트를 분류 함수로 사용하는 특수한 그룹화 기능.
- 불린을 반환하므로 맵의 키 형식은 boolean.
> 결과적으로 그룹화 맵은 최대 두 개의 그룹으로 분리(true or false).
4.1 분할의 장점
- 분할 함수가 반환하는 참, 거짓 두 가지 요소의 스트림 리스트를 모두 유지한다는 것이 분할의 장점.
5. Collector 인터페이스
- 리듀싱 연산(즉, 컬렉터)을 어떻게 구현할지 제공하는 메서드 집합으로 구성.
> T는 수집될 스트림 항목의 제네릭 형식
> A는 누적자. 즉, 수집 과정에서 중간 결과를 누적하는 객체의 형식.
> R은 수집 연산 결과 객체의 형식.
5.1 Collector 인터페이스 메서드
- 다섯가지의 메서드가 존재.
- 네 개의 메서드는 collect 메서드에서 실행하는 함수 반환.
- 다섯 번째 메서드 characteristics는 collect 메서드가 어떤 최적화를 이용해서 리듀싱 연산을 수행할 것인지 결정하도록 돕는 힌트 특성 집합 제공.
supplier 메서드: 새로운 결과 컨테이너 생성.
- 빈 결과로 이루어진 Supplier 반환해야 함.
> 즉, 수집과정에서 빈 누적자 인터페이스를 만드는 파라미터가 없는 함수.
- ToListCollector 처럼 누적자를 반환하는 컬렉터에서는 빈 누적자가 비어있는 스트림의 수집 과정의 결과가 될수 있음.
accumulator 메서드: 결과 컨테이너에 요소 추가.
- 리듀싱 연산을 수행하는 함수를 반환.
- 스트림에서 n번째 요소를 탐색할 때 두 인수, 즉, 누적자(스트림의 첫 n-1개 항목을 수집한 상태)와 n번째 요소를 함수에 적용.
- 함수의 반환값은 void.
> 즉, 요소를 탐색하면서 적용하는 함수에 의해 누적자 내부 상태가 바뀌므로 누적자가 어떤 값일지 단정할 수 없음.
finisher 메서드: 최종 변환값을 결과 컨테이너로 적용.
- 스트림 탐색을 끝내고 누적자 객체를 최종 결과로 변환하면서 누적 과정을 끝낼 때 호출할 함수를 반환.
- 때로는 누적자 객체가 이미 최정 결좌인 상황도 존재. (ex- toListCollector)
> 이 경우 변환 과정이 필요하지 않으므로 finisher 메서드는 항등 함수를 반환.
combiner 메서드: 두 결과 컨테이너 병합
- 스트림의 서로 다른 서브파트를 병렬로 처리할 때 누적자가 이 결과를 어떻게 처리할지 정의.
- 스트림의 리듀싱을 병렬로 수행 가능.
> 스트림의 리듀싱을 병렬로 수행할 때 포크.조인 프레임워크와 Spliterator를 사용. (다음 챕터에서 추가 설명.)
Characteristics 메서드
- 컬렉터의 연산을 정의하는 Characteristics 형식의 불변 집합을 반환.
- 스트림을 병렬로 리듀스할 것인지 그리고 병렬로 리듀스한다면 어떤 최적화를 선택할지 힌트 제공.
- 다음 세 항목을 포함하는 열거형임.
> UNORDERED
* 리듀싱 결과는 스트림 요소의 방문 순서나 누적 순서에 영향 받지 않음.
> CONCURRENT
* 다중 스레드에서 accumulator 함수를 동시에 호출 가능.
* 스트림의 병렬 리듀싱 수행 가능.
* 컬렉터의 플래그에 UNORDERED를 함꼐 설정하지 않았다면 데이터 소스가 정룔되어 있지 않은 상황에서만 병렬 리듀싱 수행 가능.
> IDENTITY_FINISH
* finisher 메서드가 반환하는 함수는 단순히 identity를 적용할 뿐이므로 이를 생략 가능.
* 따라서 이듀싱 과정의 최종 결과로 누적자 객체를 바로 사용 가능.
6. 요약
- collect는 스트림의 요소를 요약 결과로 누적하는 다양한 방법을 인수로 갖는 최종 연산.
- 스트림의 요소를 하나의 값으로 리듀스하고 요약하는 컬렉터뿐 아니라 최소값, 최대값, 평균값을 계산하는 컬렉터 등이 미리 정의되어 있음.
- 미리 정의된 컬렉터인 groupingBy로 스트림의 요소를 그룹화하거나, partitioningBy로 스트림의 요소 분할 가능.
- 컬렉터는 다수준의 그룹화, 분할, 리듀싱 연산에 적합하게 설계.
- Collector 인터페이스에 정의된 메서드를 구현해서 커스텀 컬렉터 개발 가능.
'Java8 > Java 8 in Action' 카테고리의 다른 글
리팩토링, 테스팅, 디버깅 (0) | 2017.07.03 |
---|---|
병렬 데이터 처리와 성능 (0) | 2017.06.29 |
스트림 활용 예제 소스 (0) | 2017.06.26 |
스트림 활용 (0) | 2017.06.26 |
스트림 소개 (0) | 2017.06.16 |