본문 바로가기
JAVA

[JAVA] Stream Collectors.groupingBy() (그룹화) 를 사용해 다양한 데이터 그룹화 하기

by 딘딘은딘딘 2023. 9. 6.
반응형

데이터의 그룹화라 하면 보통 SQL의 Group By를 사용해 수행된다.

그러나 자바 8의 함수형 Collectors.groupingBy를 이용하면

한가지 기준 뿐만 아니라 여러가지의 기준으로도 쉽게 데이터를 그룹화 할 수 있다.

 

이 포스팅에서는 자바 8 스트림의 Collectors 인터페이스를 사용해 다양한 데이터들을 그룹화 한다.

 


예시를 위해 아래와 같은 데이터를 준비했다.

상품코드, 상품명, 상품유형, 가격이 각각 Key가 되고

그 안에 데이터가  Value가 된다

(상품 코드는 그룹화를 위해 중복이 가능하도록 임의로 설정)

상품코드 상품명 상품유형 가격
A123 라면 식료품 1000
A123 만두 식료품 1500
A123 즉석밥 식료품 2000
A124 반팔티 상의 3000
A124 긴팔티 상의 4000
A124 카라티 상의 5000
A124 청바지 하의 6000
A124 면바지 하의 7000
A125 목걸이 악세사리 8000
A125 반지 악세사리 9000
A126 선충기 기타 15000

 

그룹핑 테스트를 위해 데이터를 담을 GOD 클래스 (객체) 생성을 한다

 

Lombok을 사용하지 않기 때문에 Getter / Setter , toString, 생성자를 생성해준다.

(롬복을 사용하면 @Data 어노테이션을 걸어주거나 다른 Getter, Setter등 각각 어노테이션을 걸어주면 된다.)

 

 

1. 그룹화

 

1-1. 상품유형 별 상품정보를 그룹화 (1개의 인수)

 

Map<String, List<God>> groupByGodCd = godList.stream().collect(Collectors.groupingBy(v -> v.getGodTp()));

위 코드 한줄로 상품 유형별로 상품정보를 그룹화 할 수 있다.

(이때 groupingBy의 파라미터에는 메서드 참조 형태로 작성해도 된다. groupingBy(God::getGodTp))

 

그룹화된 결과를 확인 해보면 god_tp (상품유형) 별로 그룹화 된것을 확인 할 수 있다.

 

스트림의 각 상품에서 상품유형과 일치하는 모든 상품을 추출하는 함수를 groupingBy 메서드로 전달 하며

이 함수를 기준으로 스트림이 그룹화 되므로 이를 분류 함수 라고 한다. 

 

Map의 타입이 <String, List<God>> 이기 때문에 그룹화 연산의 결과로

각 키(상품유형)에 대응하는 스트림의 모든 항목 리스트를 값으로 하는 Map이 반환된다.

 

1-2. 가격이 5천원 이상의 상품 그룹화

 

요소를 그룹화 한 다음에 필터링을 하기 위해 보통의 스트림 사용자들은

그룹화를 하기 전에 프레디케이트 필터를 적용 한다고 생각한다.

 

필터링 -> 그룹화  예시)

Map<String, List<God>> groupByGodCd = godList.stream()
        .filter(v -> v.getPrice() >= 5000)
        .collect(Collectors.groupingBy(God::getGodTp));

하지만 그룹화 전에 프레디케이트를 진행하면 아래와 같이

5천원 미만의 상품유형이 그룹화 되지 않는다.

 

필터링 된 상품유형(Key) 까지 빈 배열로 그룹화 하기 위해서는 

Collectors 형식의 두 번째 파라미터를 갖도록 groupingBy 팩토리 메서드를 오버로드 하여 사용한다.

즉 두 번째 Collector 안으로 필터 프레디케이트를 이동한다.

 

그룹화 -> 필터링 -> 재그룹화 예시)

* Collectors.filtering

 

filtering 메서드는 각 그룹의 요소와 필터링 된 요소를 재그룹화 한다.

Map<String, List<God>> groupByGodCd
        = godList.stream()
        .collect(Collectors.groupingBy(God::getGodTp,
                Collectors.filtering(god -> god.getPrice() > 5000, Collectors.toList())));

filtering 컬렉터를 사용해 그룹화 -> 필터링 -> 재그룹화 를 하면 아래와 같이 출력된다.

 

* Collectors.mapping

같은 예시로 mapping 메서드는 각 그룹별 특정 값을 그룹화 해줄 수 있다.

 

Map<String, List<String>> groupByGodCd
        = godList.stream()
        .collect(Collectors.groupingBy(God::getGodTp,
                Collectors.mapping(God::getGodNm, Collectors.toList())));

이 메서드의 경우 리턴 타입이 Map<String, List<String>> 인데 그룹별 특정 값의 목록을 가져오기 떄문이다

예시를 보면 쉽게 이해가 가능하다.

 

상품 유형별 상품명이 그룹화 된 것을 확인할 수 있다.

 

* Collectors.counting()

각 그룹별 개수 구하기

Map<String, Long> groupByGodCd
        = godList.stream()
                .collect(Collectors.groupingBy(God::getGodTp, Collectors.counting()));

 

counting 메서드를 사용하면 각 상품 유형별로 그룹별 개수를 카운팅 해준다. 

 

2. 다수준 그룹화 (N수준 N차원 그룹화)

두 개의 인수를 받는 팩토리 메서드 Collectors.groupingBy 를 이용해 항목을 다수준으로 그룹화 할 수 있다.

 

상품 유형 > 상품 코드 로 그룹화를 진행한다.

Map<String, Map<String, List<God>>> groupByGodCd
        = godList.stream().collect(
                Collectors.groupingBy(God::getGodTp,	// 첫 번째 수준 분류
                        Collectors.groupingBy(God::getGodCd)));  // 두 번째 수준 분류

상품코드를 2개로 분류 후 조회 결과

godList.add(new God("A114", "반팔티", "상의", 3000));
godList.add(new God("A114", "긴팔티", "상의", 4000));
godList.add(new God("A124", "카라티", "상의", 5000));

상품코드를 위와 같이 A114, A124 2가지로 분류 후 조회 결과

 

godTp : 상의 = {A124=[God{godCd='A124', godNm='카라티', godTp='상의', price=5000}], A114=[God{godCd='A114', godNm='반팔티', godTp='상의', price=3000}, God{godCd='A114', godNm='긴팔티', godTp='상의', price=4000}]}

상품 유형이 상의로 1차 분류되고 각 상의별 상품코드 A114 / A124 로 2차 분류가 된 것을 확인 할 수 있다.

 

N수준 그룹화의 결과는 N수준 트리구조로 표현되는 N수준 맵이 된다.

 

3.  3개 이상의 기준을 가진 그룹화 (N수준 1차원 그룹화)

2번에서 언급한 N수준 N차원 그룹화를 사용하게 되면

그룹핑의 기준이 되는 인수가 늘어날 수록 차원의 수가 증가해 원하는 결과를 도출하기 어려울 수 있다.

 

마지막으로는 여러개의 인수를 받고 1차원 배열로 받는 그룹핑 방식이다.

 

먼저 데이터를 다시 준비해본다.

 

데이터는 위와 같이 준비한다. (상품명을 몰라서 팔도랑 농심으로 임의로 작성 ㅠㅠ)

 

그룹화 할 항목은

상품유형, 상품상세유형, 전시여부, 브랜드명 이 4가지로 그룹화를 진행한다.

 

3개 이상 수준을 그룹화 하기 위해서는 먼저 그룹화 할 클래스 (객체)를 생성해야 한다.

 

그룹핑할 클래스를 만들어 주고 Getter / 생성자 /  Equals와 HashCode / toString 까지 만들어준다

직접 만들면 시간이 오래걸리니 제너레이터를 사용한다

(롬복은 @Getter,@EqualsAndHashCode, @AllArgsConstructor 로!!)

 

클래스를 만들때 Getter / 생성자 / Equals And HashCode는 꼭 필요하다!!

 

Map<GodGrouping, List<God>> groupByGodCd =
        godList.stream()
                .collect(Collectors.groupingBy(
                        v -> new GodGrouping(v.getGodTp(), v.getGodDtlTp(), v.getDspYn(), v.getBrandNm())));

4가지 인수의 그룹화를 진행하게 되면 아래와 같이

상품유형, 상세유형, 전시여부, 브랜드 별 상품 정보가 1차원으로 그룹핑된다.

.

그룹핑의 기준이 되는 클래스의 필드가 Key가 되고

그 기준으로 그룹화 된 God의 List가 Value가 되는 것을 확인할 수 있다.

 

* 추가로 groupingBy의 두번째 인수에 filtering 컬렉터를 사용하면 기준에 맞춰 필터링 작업도 가능하다.

 

Map<GodGrouping, List<God>> groupByGodCd =
        godList.stream()
                .collect(Collectors.groupingBy(
                        v -> new GodGrouping(v.getGodTp(), v.getGodDtlTp(), v.getDspYn(), v.getBrandNm())
                    , Collectors.filtering(god -> god.getPrice() > 1000, Collectors.toList())
                ));

4가지 기준으로 그룹화 후 1000원 이상의 상품을 필터링 한 결과이다.

 

반응형