ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Java Stream] GroupingBy 를 이용한 그룹화 예시 소개
    [IT] 공부하는 개발자/JAVA 2021. 2. 15. 08:55

    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 을 쓰는 이유, 문법, 코드 예시 소개

     

    댓글

Copyright in 2020 (And Beyond)