일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
- 가장 큰 수
- 코딩 테스트
- 롬복 어노테이션
- @Data
- 완주하지 못한 선수
- 스택/큐
- 다리를 지나는 트럭
- 쿠버네티스
- kubenetes
- @configuration
- 정렬
- @EnableScheduling
- 커링
- @Setter
- 크론 표현식
- 알고리즘
- H-index
- @Getter
- 해시
- 모던 자바 인 액션
- 루씬 인 액션
- 고차원 함수
- 스프링 스케쥴러
- 영속 자료구조
- 프로그래머스
- Java
- 기능개발
- K번째수
- 검색 기능 확장
- 전화번호 목록
- Today
- Total
Today I Learned
[모던 자바 인 액션] 6장. 스트림으로 데이터 수집 (1) 본문
이 장의 내용
- Collectors 클래스로 컬렉션을 만들고 사용하기
- 하나의 값으로 데이터 스트림 리듀스하기
- 특별한 리듀싱 요약 연산
- 데이터 그룹화와 분할
- 자신만의 커스텀 컬렉션 개발
6.1 컬렉터란 무엇인가?
Collector 인터페이스 구현은 스트림의 요소를 어떤 식으로 도출할지 지정한다.
리스트를 만들기위해 toList를 Collector 인터페이스의 구현으로 사용하거나 groupingBy를 이용해서 각 키 버킷에 대응하는 요소 별로 맵을 만들 수도 있다.
6.1.1 고급 리듀싱 기능을 수행하는 컬렉터
컬렉터의 최대 강점은 collect로 결과를 수집하는 과정을 간단하면서도 유연한 방식으로 정의할 수 있다는 점이다.
스트림에서 collect를 호출하면 collect에서는 리듀싱 연산을 이용해서 스트림의 각 요소를 방문하면서 컬렉터가 작업을 수행한다.
보통 함수를 요소로 변환할 때는 컬렉터를 적용하며 최종 결과를 저장하는 자료구조에 값을 누적한다.
6.1.2 미리 정의된 컬렉터
Collectors 유틸리티 클래스는 자주 사용하는 컬렉터 인스턴스를 손쉽게 생성할 수 있는 정적 메서드를 제공한다.
Collectors에서 제공하는 메서드의 기능은 크게 세 가지로 구분된다.
- 스트림 요소를 하나의 값으로 리듀스하고 요약
- 요소 그룹화
- 요소 분할
6.2 리듀싱과 요약
컬렉터로 스트림의 모든 항목을 하나의 결과로 합칠 수 있다.
첫 번째 예제로 counting() 팩토리 메서드가 반환하는 컬렉터를 사용해보자.
long howManyDishes = menu.stream().collect(Collectors.counting());
//collect 생략 가능
long howManyDishes = menu.stream().count();
6.2.1 스트림 값에서 최댓값과 최솟값 검색
Collectors.maxBy, Collectors.minBy 메서드를 이용해서 스트림의 최댓값과 최솟값을 계산할 수 있다.
두 컬렉터는 스트림의 요소를 비교하는 데 사용할 Comparator를 인수로 받는다.
Comparator<Dish> dishCaloriesComparator = Comparator.comparingInt(Dish::getCalories);
Optional<Dish> mostCaloriesDish = menu.stream().collect(maxBy(dishCaloriesComparator));
스트림에 있는 객체의 숫자 필드의 합계나 평균 등을 반환하는 연산에도 리듀싱 기능이 자주 사용된다. 이러한 연산을 요약 연산이라 부른다.
6.2.2 요약 연산
Collectors 클래스는 Collectors.summingInt라는 특별한 요약 팩토리 메서드를 제공한다.
summingInt는 객체를 int로 매핑하는 함수를 인수로 받으며, 인수로 전달된 함수는 객체를 int로 매핑한 컬렉터를 반환한다. 그리고 summingInt가 collect 메서드로 전달되면 요약 작업을 수행한다.
다음은 메뉴 리스트의 총 칼로리를 계산하는 코드다.
int totalCalories = menu.stream().collect(summingInt(Dish::getCalories));
이러한 단순 합계 외에 평균값 계산 등의 연산도 요약 기능으로 제공된다.
만약 두개 이상의 연산이 한번에 수행되어야 한다면 summarizingInt가 반환하는 컬렉터를 사용할 수 있다.
IntSummaryStatistics menuStatistics = menu.stream().collect(summarizingInt(Dish::getCalories));
// menuStatistics : IntSummaryStatistics{count=9, sum=4300, min=120, average=477.778, max=800}
6.2.3 문자열 연결
컬렉터에 joining 팩토리 메서드를 이용하면 스트림의 각 객체에 toString 메서드를 호출해서 추출한 모든 문자열을 하나의 문자열로 연결해서 반환한다.
String shortMenu = menu.stream().map(Dish::getName).collect(joining());
연결된 두 요소 간에 구분 문자열을 넣을 수 있도록 오버로드된 joining 팩토리 메서드도 있다.
String shortMenu = menu.stream().map(Dish::getName).collect(joining(","));
6.2.4 범용 리듀싱 요약 연산
지금까지 살펴본 모든 컬렉터는 reducing 팩토리 메서드로도 정의할 수 있다.
예를 들어 다음 코드처럼 reducing 메서드로 만들어진 컬렉터로도 메뉴의 모든 칼로리 합게를 게산할 수 있다.
int totalCalrories = menu.stream().collect(reducing(0, Dish::getCalories, (i, j) -> i + j);
또는 한 개의 인수를 가진 reducing 버전을 이용해서 가장 칼로리가 높은 요리를 찾을 수도 있다.
Optional<Dish> mostCaloriesDish = menu.stream().collect(reducing(d1, d2)
-> d1.getCalories() > d2.getCalories() ? d1 : d2));
컬렉션 프레임워크 유연성 : 같은 연산도 다양한 방식으로 수행할 수 있다.
이전 예제에서 람다 표현식 대신 Integer 클래스의 sum 메서드 참조를 이용하면 코드를 좀 더 단순화할 수 있다.
int totalCalories = menu.stream().collect(reducing(0, Dish::getCalories, Integer::sum));
counting 컬렉터도 세 개의 인수를 갖는 reducing 팩토리 메서드를 이용해서 구현할 수 있다.
public static <T> Collector<T, ?, Long> counting() {
return reducing(0L, e -> 1L, Long::sum);
}
자신의 상황에 맞는 최적의 해법 선택
함수형 프로그래밍에서는 하나의 연산을 다양한 방법으로 해결할 수 있다.
컬렉터를 이용하면 스트림 인터페이스에서 직접 제공하는 메서드를 이용하는 것에 비해 코드가 복잡해지지만,
재사용성과 커스터마이즈 가능성을 제공하는 높은 수준의 추상화와 일반화를 얻을 수 있다.
6.3그룹화
자바8의 함수형을 이용하면 가독성 있는 한 줄 코드로 그룹화를 구현할 수 있다.
Map<Dish, Type, List<Dish>> dishsByType = menu.stream().collect(groupingBy(Dish::getType));
//dishsByType : {FISH=[prawns, salmon], OTHERS=[french fries, rice, pizza], MEAT[pork, beef, chicken]}
스트림의 각 요리에서 Dish.Type과 일치하는 모든 요리를 추출하는 함수를 groupbingBy 메서드로 전달했다.
이 함수를 기준으로 스트림이 그룹화되므로 이를 분류함수라고 부른다.
단순한 속성 접근자 대신 더 복잡한 분류 기준이 필요한 상황에서는 메서드 참조를 분류 함수로 사용할 수 없다.
예를 들어 400 칼로리 이하를 'diet', 400~700칼로리를 'normal', 700칼로리 이상을 'fat' 요리로 분류한다 가정해보자.
public enum CaloricLevel { DIET, NORMAL, FAT }
Map<CaloricLevel, List<Dish>> dishesByCaloricLevel = menu.stream().collect(
groupingBy(dish -> {
if (dish.getCalories() <= 400 ) return CaloricLevel.DIET;
else if (dish.getCalories() <= 700 ) return CaloricLevel.NORMAL;
else return CaloricLevel.FAT;
}));
6.3.1 그룹화된 요소 조작
요소를 그룹화 한 다음에는 각 결과 그룹의 요소를 조작하는 연산이 필요하다.
예를 들어 500칼로리가 넘는 요리만 필터한다고 가정하자. 다음 코드처럼 그룹화를 하기 전에 프레디케이트로 필터를 적용해 문제를 해결할 수 있다고 생각할 것이다.
Map<Dish, Type, List<Dish>> caloricDishesByType = menu.stream()
.filter(dish -> dish.getCalories() > 500)
.collect(groupingBy(Dish::getType));
//caloricDishesByType : {OTHER=[french fries, pizza], MEAT=[pork, beef]}
위 코드로 문제를 해결할 수 있지만, 필터 프레디케이트를 만족하는 FISH 종류 요리는 없으므로 결과 맵에서 해당 키 자체가 사라진다.
필터 프레디케이트를 groupingBy 팩토리 메서드의 인수로 사용하면 이런 문제를 해결할 수 있다.
Map<Dish, Type, List<Dish>> caloricDishesByType = menu.stream()
.collect(groupingBy(Dish::getType, filtering(dish -> getCalrories() > 500, toList())));
//caloricDishesByType : {OTHER=[french fries, pizza], MEAT=[pork, beef], FISH=[]}
filtering 메서드는 Collectors 클래스의 또다른 정적 팩토리 메서드로 프레디케이트를 인수로 받는다. 이 프레디케이트로 각 그룹의 요소와 필터링된 요소를 재그룹화한다.
그룹화된 항목을 조작하는 다른 유용한 기능 중 하나는 매핑 함수를 이용해 요소를 변환하는 작업이다.
Map<Dish, Type, List<Sting>> dishNamesByTypes = menu.stream()
.collect(groupingBy(Dish::Type, mapping(Dish::getName, toList())));
각 그룹이 리스트 형태라면 flatMap 변환을 사용해서 추출할 수도 있다.
Map<String, List<String>> dishTags = new HashMap<>();
dishTag.push("pork", asList("greasy", "salty"));
dishTag.push("beef", asList("salty", "roasted"));
dishTag.push("chicken", asList("fried", "crisp"));
dishTag.push("rice", asList("light", "natural"));
Map<Dish.Type, Set<String>> dishNamesByType = menu.stream()
.collect(groupingBy(Dish::getType,
flatMapping(dish -> dishTags.get(dish.getName()).stream(),
toSet())));
6.3.2 다수준 그룹화
두 인수를 받는 팩토리 메서드 Collectors.groupingBy를 이용해서 항목을 다수준으로 그룹화할 수 있다.
Map<Dish.Type, Map<CalricLevel, List<Dish>>> dishesByTypeCaloricLevel = menu.stream()
.collect(groupingBy(Dish::getType,
groupingBy(dish -> {
if (dish.getCalories() <= 400) return CaloricLevel.DIET;
else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL;
else return CaloricLevel.FAT;
})
)
};
//dishesByTypeCaloricLevel :
//{MEAT={DIET=[chicken], NORMAL=[beef], FAT=[pork]}, FISH={DIET=[prawns], NORMAL=[salmon]}, OTHER={...}}
외부 맵은 첫 번째 수준의 분류함수에서 분류한 키값 'fish, meat, other'를 가지며, 내부 맵은 두 번째 분류 함수의 키값 'normal, diet, fat'을 가진다.
보통 groupingBy의 연산을 '버킷(물건을 담을 수 있는 양동이)' 개념으로 생각하면 쉽다.
첫 번째 groupingBy는 각 키의 버킷을 만든다. 그리고 준비된 각각의 버킷을 서브스트림 컬렉터로 채워가기를 반복하면서 n수준 그룹화를 달성한다.
6.3.3 서브그룹으로 데이터 수집
groupingBy 메서드의 두번째 인수로 전달받는 컬렉터의 형식은 제한이 없다.
분류 함수 한개의 인수를 갖는 groupingBy(f)는 groupingBy(f, toList())의 축약형일 뿐이며, 다양한 컬렉터를 전달받을 수 있다.
Map<Dish.Type, Long> typesCount = menu.stream().collect(groupingBy(Dish::getType, counting()));
//{MEAT=3, FISH=2, OTHER=4}
Map<Dish.Type, Optional<Dish>> mostCaloricByType = menu.stream()
.collect(groupingBy(Dish::getType, maxBy(CompaingInt(Dish::getCalories))));
//{FISH=Optional[salmon], OTHER=Optional[pizza], MEAT=Optional[pork]}
컬렉터 결과를 다른 형식에 적용하기
마지막 그룹화 연산에서 맵의 모든 값을 Optional로 감쌀 필요가 없으므로 Optional을 삭제할 수 있다.
Collectors.collectingAndThen 팩토리 메서드로 컬렉터가 반환한 결과를 다른 형식으로 활용할 수 있다.
Map<Dish.Type, Optional<Dish>> mostCaloricByType = menu.stream()
.collect(groupingBy(Dish::getType,
collectingAndThen(maxBy(CompaingInt(Dish::getCalories)), Optional::get)));
//{FISH=salmon, OTHER=pizza, MEAT=pork}
groupingBy와 함꼐 사용하는 다른 컬렉터 예제
일반적으로 스트림에서 같은 그룹으로 분류된 모든 요소에 리듀싱 작업을 수행할 때에는 팩토리 메서드 groupingBy에 두 번째 인수로 전달한 컬렉터를 사용한다.
Map<Dish.Type, Integer> totalCaloriesByType = menu.stream()
.collect(groupingBy(Dish::getType, summingInt(Dish::getCalories)));
이 외에도 mapping 메서드로 만들어진 컬렉터도 groupingBy와 자주 사용된다.
mapping은 입력 요소를 누적하기 전에 매핑 함수를 적용해서 다양한 형식의 객체를 주어진 형식의 컬렉터에 맞게 변환한다.
Map<Dish.Type, Set<CaloricLevel>> caloricLevelsByType = menu.stream()
.collect(groupingBy(Dish::getType, mapping(dish -> {
if (dish.getCalories() <= 400) return caloricLevel.DIET;
else if (dish.getCalories() <= 700) return caloricLevel.NORMAL;
else return caloricLevel.FAT;
}, toSet() )));
//{OTHER=[DIET, NORMAL], MEAT=[DIET, NORMAL, FAT], FISH=[DIET, NORMAL]}
'JAVA & Spring > 모던 자바 인 액션' 카테고리의 다른 글
[모던 자바 인 액션] 7장. 병렬 데이터 처리와 성능 (1) (0) | 2021.08.04 |
---|---|
[모던 자바 인 액션] 6장. 스트림으로 데이터 수집 (2) (0) | 2021.07.28 |
[모던 자바 인 액션] 5장. 스트림 활용 (2) (0) | 2021.07.15 |
[모던 자바 인 액션] 5장. 스트림 활용 (1) (0) | 2021.07.07 |
[모던 자바 인 액션] 4장. 스트림 소개 (0) | 2021.06.27 |