프로그래밍 언어/Java

Stream이란? - 최종 처리 메소드의 종류와 사용 방법 [3 - groupingBy()] (JAVA)

제이온 (Jayon) 2021. 1. 10.

안녕하세요? 코딩중독입니다.

 

저번 시간에는 그룹핑을 제외한 스트림의 수집 방식을 알아 보았습니다. 이번에는 collect() 메소드를 이용하여 그룹핑하는 방법을 설명하겠습니다.

 

 

시작하기 전에...

예제에서 사용할 사용자 정의 클래스를 미리 만들어 두겠습니다. 학생의 정보를 담는 클래스로, 아래 코드를 참고하시면 되겠습니다. 저번 포스팅에서 사용한 학생 클래스에 도시 정보를 추가하고, 오버라이드된 메소드를 제거하였습니다.

 

 

 

 

요소를 그룹핑해서 수집

collect() 메소드는 단순히 요소를 수집하는 기능 이외에 컬렉션의 요소들을 그룹핑해서 Map 객체를 생성하는 기능도 제공합니다. 여기서 collect()를 호출할 때 매개 변수로 groupingBy() 또는 groupingByConcurrent() 메소드를 사용하면 됩니다. 전에 설명한 대로 전자는 쓰레드에 안전하지 않은 Map을 생성하지만, 후자는 쓰레드에 안전한 ConcurrentMap을 생성합니다.

 

 

이것이 자바다 16강 인강 중 사용된 학습 자료

 

 

저는 싱글 쓰레드만 내용을 다룰 것이기때문에 groupingByConcurrent() 메소드는 사용하지 않고, groupingBy() 메소드만 사용하겠습니다.

 

첫 번째는, 매개변수로 Function<T, K> classifier를 사용하는 groupingBy() 메소드입니다. 이 메소드의 리턴 타입을 보면, Collector<T, ?, Map<K, List<T>>>로, T를 K로 매핑한 후, 키가 K이면서 T를 저장하는 요소를 값으로 갖는 Map을 생성합니다.

 

두 번째는 매개변수로 Function<T, K> classifier, Collector<T, A, D> collector를 사용하는 groupingBy() 메소드입니다. 이 메소드의 리턴 타입을 보면, Collector<T, ?, Map<K, D>>로, T를 K로 매핑한 후, 키가 K이면서 키에 저장된 D객체에 T를 누적한 Map을 생성합니다.

 

세 번째는 두 번째의 매개 변수에서 Supplier가 추가된 형태입니다. 사용 방식도 위와 유사한데, 다만 그냥 Map이 아니라 TreeMap같은 Supplier가 제공하는 Map을 사용합니다.

 

 

그룹핑은 이렇게 설명만 들으면 이해가 바로 가질 않아서 예시 코드로 살펴보겠습니다.

 

 

 

 

위 예시 코드는 첫 번째 groupingBy() 메소드를 사용한 것이고, 학생의 성별을 기준으로 그룹핑하였습니다. groupingBy() 메소드의 매개변수로는 Function<T, K>를 사용하여 T를 K로 매핑하였습니다. 이때, T는 Student이고, K는 Student.Sex이므로 Map<Student.Sex(K), List<Student(T)>>이 생성되는 것입니다. 

 

출력 결과는 아래와 같습니다.

 

 

 

 

다음 예시 코드를 살펴보겠습니다.

 

 

 

 

위 예시 코드는 두 번째 groupingBy() 메소드를 사용한 것이고, 학생의 지역을 기준으로 그룹핑하였습니다. groupingBy() 메소드의 매개변수로는 Function<T, K>와 Collector<T, A, D> collector을 사용하였습니다. 여기서, mapByCity를 만드는 과정만 따로 자세히 살펴보겠습니다.

 

 

Function<Student, Student.City> classifier = Student::getCity;


Function<Student, String> mapper = Student::getName;


Collector<String, ?, List<String>> collector1 = Collectors.toList();


Collector<Student, ?, List<String>> collector2 = Collectors.mapping(mapper, collector1);


Collector<Student, ?, Map<Student.City, List<String>>> collector3 = Collectors
.groupingBy(classifier, collector2);


Map<Student.City, List<String>> mapByCity = studentList.stream().collect(collector3);

 

 

하나로 연결된 코드를 모두 분리해놨습니다. 먼저, Map의 Key를 얻어내기 위하여 T를 K로 매핑합니다. 여기서 T는 Student이며, K는 Student.City가 될 것이고, 이 과정은 첫 번째 줄에 해당합니다.

 

그리고 Map의 Value에 해당하는 D를 얻어야 하는데, 이 부분이 복잡합니다. 결과적으로, 우리는 Value가 List<Student>가 아니라, List<String>을 얻어야 합니다. 여기서 String은 학생의 이름이 되겠죠. 그래서 Student를 Student::getName으로 매핑하고, 이것을 다시 List<String>으로 매핑해야 합니다. 이 과정이 2 ~ 4번째 줄이 되겠습니다. 참고로, Collectors.mapping() 메소드는 아래에서도 다룰 예정이지만, T를 U로 매핑한 후, U를 R에 수집하는 역할을 합니다.

 

여기까지 classifier와 collector을 모두 정의하였으므로 이 두 개를 매개 변수로 넘겨서 mapByCity를 형성하면 됩니다. 출력 결과는 아래와 같습니다.

 

 

 

 

이제 세 번째 groupingBy() 메소드가 남았습니다. 이것은 위의 groupingBy() 메소드를 형성하는 원리와 거의 유사하나, Map의 형태를 구체화하여 저장할 수 있습니다.

 

 

TreeMap<Student.City, List<String>> treeMapByCity = studentList.stream()
.collect(Collectors.groupingBy(
Student::getCity,
TreeMap::new,
Collectors.mapping(Student::getName, Collectors.toList()))
);

 

 

다음과 같이 Supplier가 들어갈 자리에 원하는 Map의 형태를 지정해 주는 것입니다.

 

 

그룹핑 후 매핑 및 집계

Collectors.groupingBy() 메소드는 그룹핑 후, 매핑이나 집계를 할 수 있도록 두 번째 매개값으로 Collector를 가질 수 있습니다. 위 예제에서 매개 변수가 하나만 있는 groupingBy() 메소드도 있었지만, 2개 또는 3개가 있는 groupingBy() 메소드를 다뤘습니다. 이 중, 2개 또는 3개의 매개 변수를 갖는 groupingBy() 메소드가 그룹핑 후 매핑 및 집계를 사용합니다. 위 예제에서는 mapping() 메소드를 활용하였지만, 집계를 통해서 Value 값을 무궁무진하게 변화시킬 수 있습니다.

 

 

리턴 타입 메소드(매개 변수) 설명
Collector<T, ?, R> mapping(Function<T, U> mapper, Collector<U, A, R> collector> T를 U로 매핑한 후, U를 R에 수집
Collector<T, ?, Double> averagingDouble(ToDoubleFunction<T> mapper) T를 Double로 매핑한 후, Double의 평균값을 산출
Collector<T, ?, Long> counting() T의 카운팅 수를 산출
Collector<CharSequence, ?, String> joining(CharSequence delimiter) CharSequence를 구분자로 연결한 String을 산출
Collector<T, ?, Optional<T>> maxBy(Comparator<T> comparator) Comparator를 이용해서 최대 T를 산출
Collector<T, ?, Optional<T>> minBy(Comparator<T> comparator) Comparator를 이용해서 최소 T를 산출
Collector<T, ?, Integer> summingInt(ToIntFunction)
summingLong(ToLongFunction)
summingDouble(ToDoubleFunction)
Int, Long, Double 타입의 합계 산출

 

 

이제, 위 메소드의 일부를 이용한 예제 코드를 작성할 것인데, 모두 2번째 groupingBy() 메소드를 사용할 것입니다. 특히, Value 부분에서 T가 D로 매핑되는 과정을 숙지하셔야 합니다.

 

 

 

 

위 코드는 성별을 기준으로 평균 점수를 저장하는 맵을 생성합니다. maxBySex을 정의하는 과정만 자세히 살펴봅시다.

 

 

Function<Student, Student.Sex> classifier = Student::getSex;


ToDoubleFunction<Student> mapper = Student::getScore;


Collector<Student, ?, Double> collector1 = Collectors.averagingDouble(mapper);


Collector<Student, ?, Map<Student.Sex, Double>> collector2 = Collectors
.groupingBy(classifier, collector1);


Map<Student.Sex, Double> mapBySex = studentList.stream().collect(collector2);

 

 

Map의 Key는 첫 번째 줄을 통하여 얻어낼 수 있고, Value는 두, 세 번째 줄을 통하여 얻어낼 수 있습니다. 특히, Value는 Student를 Double로 매핑한다는 점을 주의 깊게 보셔야 합니다.

 

출력 결과는 남자와 여자 모두 9.5가 나올 것입니다.

 

 

다음 예제를 보겠습니다.

 

 

 

 

이 예제는 학생들을 성별로 그룹핑한 다음 같은 그룹에 속하는 학생 이름을 쉼표로 구분해서 문자열을 만들고, 성별을 Key로, 문자열을 Value로 갖는 Map을 생성합니다.

 

mapping() 메소드를 통하여 Student를 String으로 매핑합니다. 'Student -> String(구분자 없음) -> String(구분자 있음)'으로 매핑된다는 사실을 기억하셔야하고, 구분자는 맨 마지막에는 첨가되지 않는다는 것에 유의하셔야합니다.

 

출력 결과는 아래와 같습니다.

 

 

 

 

정리

이번 포스팅을 마지막으로 java의 Stream API를 대부분 다뤄보았습니다. 그리고 특히 이 수집 파트가 가장 어려웠다고 생각합니다. 하지만 그만큼 실무에서도 자주 사용되므로 성실히 연습하는 편이 좋겠습니다.

 

여담으로, 저번 포스팅에서 toMap()이라는 것을 직접적으로 다루지는 않았지만 표로 소개하였습니다. 이것도 마찬가지로 스트림을 Map으로 반환하는 것인데, 단순히 변환만 하는 것이고, groupingBy()는 특정 조건에 따라 key와 value를 얻어내서 Map으로 반환다고 이해하시면되겠습니다.

 

 

참고 자료

 

이것이 자바다. - 저자 신용권

댓글

추천 글