[Java Stream] GroupingBy 를 이용한 그룹화 예시 소개
Introduction
Java 8에서 처음 도입된 스트림은 데이터 집합을 처리할 수 있는 반복자 역할을 수행한다.
스트림의 연산은 filter, map과 같이 중간 연산자와, collect와 같은 최종 연산자로 구분된다.
이 포스팅에서는 최종 연산자 collect 의 Collector 인터페이스를 구현하여 그룹화하는 다양한 방법에 대해 다룬다.
그룹화
Stream GroupingBy 를 이용하면 데이터 집합을 하나 이상의 특성으로 분류, 그룹화하는 연산을 쉽게 수행할 수 있다.
아래와 같은 데이터 셋을 가지고 그룹화를 연습해보자.
나라 | 도시 | 거주자 이름 |
한국 | 서울 | 홍길동 |
한국 | 서울 | 임영웅 |
한국 | 부산 | 김태희 |
중국 | 베이징 | 왕서방 |
미국 | 보스턴 | 미셸 |
미국 | 엘에이 | 마이클 |
미국 | 엘에이 | 케이트 |
이 데이터를 담을 클래스를 생성한다. Person 이라는 클래스명으로 생성해 주었다.
package com.stock.management.app.dto.request;
import lombok.Data;
@Data
public class Person {
private String Country; // 나라
private String City; // 도시
private String name; // 이름
}
단일 인수에 의한 그룹화
이 데이터 셋을 나라별로 그룹화하는 코드는 한 줄로 작성할 수 있다.
people.stream().collect(Collectors.groupingBy(Person::getCountry));
메소드를 하나 만들고, 그룹화가 잘 되는지 테스트 코드로 검증해보았다.
public Map<String, List<Person>> groupingBySingleArgument(List<Person> people){
return people.stream().collect(Collectors.groupingBy(Person::getCountry));
}
리턴된 resultMap 의 key 값에 한국, 중국, 미국을 할당해서 꺼내보니 Person 데이터가 그룹별로 잘 묶여서 들어가 있는 것을 알 수 있었다.
n 개의 인수에 의해 n 차원 그룹화
이번에는 데이터 셋의 Person 을 '나라', '도시' 2개의 인수를 가지고 그룹화를 한다.
people.stream().collect(Collectors.groupingBy(Person::getCountry, Collectors.groupingBy(Person::getCity)));
return 값은 첫번째 인수인 '나라'(Country)를 Key로 가지는 Map 이 되어야 하며, 이 Map 의 value 값에 다시 '도시'를 key로 가지는 map 이 들어간 2 레벨 Collection 이 된다.
public Map<String, Map<String, List<Person>>> groupingByDoubleArgument(List<Person> people){
return people.stream().collect(Collectors.groupingBy(Person::getCountry,
Collectors.groupingBy(Person::getCity)));
}
마찬가지로 테스트 코드로 잘 그룹화가 되는지 검증해보았다.
n 개의 인수에 의해 1 차원 그룹화
그런데 이렇게 n 레벨의 map 으로 코딩을 하다 보니, map 안의 map 구조가 너무 복잡하게 느껴졌다. 2 개까지는 괜찮을 수 있으나, 분류하고자 하는 파라미터가 늘어날수록 머리가 아파오기 시작한다.
데이터 셋을 조금 업그레이드 해보자.
나라 | 도시 | 성별 | 거주자 이름 |
한국 | 서울 | 남 | 홍길동 |
한국 | 서울 | 여 | 홍길순 |
한국 | 서울 | 남 | 임영웅 |
한국 | 서울 | 여 | 임영순 |
한국 | 부산 | 여 | 김태희 |
한국 | 부산 | 남 | 김태돌 |
중국 | 베이징 | 남 | 왕서방 |
중국 | 베이징 | 여 | 왕태순 |
미국 | 보스턴 | 여 | 미셸 |
미국 | 보스턴 | 남 | 존 |
미국 | 엘에이 | 남 | 마이클 |
미국 | 엘에이 | 여 | 린제이 |
미국 | 엘에이 | 여 | 케이트 |
미국 | 엘에이 | 남 | 제이미 |
이 데이터 셋에서, 나라/도시/성별이라는 3 가지 파라미터로 구분하여 그룹핑하고 싶을 때, 위의 방법대로 3 레벨 map을 구현하면 작성하는 이도 힘들고, 읽는 이도 힘든 코드가 되어버리고 만다.
분류하고자 하는 인수의 개수가 많아질수록 1 레벨로 구현할 필요성이 절실해진다.
1 레벨의 Map 으로 구현하기 위한 방법으로, 그룹 클래스를 생성하는 것을 소개한다.
package com.stock.management.app.dto.request;
import java.util.Objects;
@ToString
@EqualsAndHashCode
public class PersonGroup {
public String country;
public String city;
public boolean female;
public PersonGroup(Person p) {
this.country = p.getCountry();
this.city = p.getCity();
this.female = p.isFemale();
}
}
Person 객체를 각 파라미터의 value 값을 사용해 비교할 수 있도록 해주는 클래스를 생성했다.
그리고 예제 코드에서는 Lombok 의 EqualsAndHashCode 애노테이션을 사용했지만, Lombok 을 사용하고 싶지 않다면 equals와 hashCode를 상속하여 수정하여도 된다. equals 비교 시 country/city/female의 value 값이 같다면 true를 리턴하도록 작성하면 된다.
그리고 Stream에서 groupingBy의 인수를 위에서 만든 Group 클래스로 지정한다.
public Map<PersonGroup, List<Person>> groupingByMultipleArgument(List<Person> people){
return people.stream().collect(Collectors.groupingBy(PersonGroup::new));
}
테스트 코드를 통해 그룹핑 결과를 확인해보자.
1차원 맵이 return 되고, Map에서 꺼낸 값도 기댓값과 일치한다.
Collector 결과 변환
그룹화한 데이터의 결과값을 변환하여 도출할 수 도 있다.
바로 위에서 작업한 1차원 Map의 결과인 List 를 List 사이즈로 결과 변환하고 싶다고 가정해보자.
예를 들어 한국/부산/남성의 숫자, 미국/LA/여성의 숫자 라는, 위와는 다른 결과값을 도출하고 싶은 케이스이다.
public Map<PersonGroup, Long> getCountOfResidenceByCondition(List<Person> people){
return people.stream().collect(Collectors.groupingBy(PersonGroup::new, Collectors.counting()));
}
위와 같이 groupingBy 콜렉터에 두 번째 인수로 결과 변환 연산자를 전달하면, 결과 값이 연산자에 의해 변환된다.
잘 작동하는지 확인할 차례다.
Spock 테스트를 작성하여 데이터셋을 밀어 넣어보자.
의도한 대로 Stream Map의 value 값이, count로 변환되어 출력됨을 확인할 수 있었다.
유용하게 사용할 수 있는 결과 변환 연산자를 아래에 소개한다.
연산자 | 기능 |
counting | 리스트의 count 값으로 변환 |
mapping | 리스트 안의 element 를 다른 값으로 변환 |
flatMapping | 2 레벨의 리스트를 1 레벨로 평면화 변환 |
maxBy | 리스트 안에서 특정 필드가 최댓값을 가지는 element 로 변환한다. |
summingInt | 리스트 안의 element 들의 특정 필드 합계 값 |
다른 포스팅
2019/10/28 - [[IT] 공부하는 개발자/JAVA] - [JAVA Collections API] 자료구조 요약: 구조/성능/용도
[JAVA Collections API] 자료구조 요약: 구조/성능/용도
개요 이 포스팅에서는 자바 Collections API로 표현되는 자료구조들의 성능에 대해서 이야기하고자 한다. 성능은 시간 복잡도(Time Complexity)를 기준으로 하며, 발생할 수 있는 최대 복잡도를 가리키는
gem1n1.tistory.com
2021/02/15 - [[IT] 공부하는 개발자/Open Source] - Groovy Spock 의 모든 것! Spock 을 쓰는 이유, 문법, 코드 예시 소개