공부/프로그래밍

[java] lambda 를 이용한 GroupBy mulitiple field 사용하기

demonic_ 2021. 1. 20. 08:00

lambda 에서 Collector를 이용해 GroupBy를 사용할 수 있다

 

결론만 말하자면 DB에 저장되어 있다면 DB에 있는 함수를 사용하길 권장하지만... 데이터 자체를 가공해야한다면 어쩔 수 없이 써야할 거 같다. 암튼 한번 알아보자.

 

 

다음과 같이 테스트 데이터를 준비했다.

데이터를 담을 객체를 먼저 생성한다

@Getter
@ToString
public class Sales {
    private String yyyymm;
    private String storeId;
    private Long sales;

    public Sales(String yyyymm, String storeId, Long sales) {
        this.yyyymm = yyyymm;
        this.storeId = storeId;
        this.sales = sales;
    }
}
@ExtendWith(SpringExtension.class)
public class TestGroupBy {

    private List<Sales> dtos;

    @BeforeEach
    void init() {
        dtos = new ArrayList<>();
        Sales dto1 = new Sales("2020-11", "1", 6233800l);
        Sales dto2 = new Sales("2020-11", "1", 725700l);
        Sales dto3 = new Sales("2020-11", "2", 285300l);
        Sales dto4 = new Sales("2020-11", "2", 437800l);
        Sales dto5 = new Sales("2020-12", "1", 12991300l);
        Sales dto6 = new Sales("2020-12", "1", 2650500l);
        Sales dto7 = new Sales("2020-12", "2", 108400l);
        Sales dto8 = new Sales("2020-12", "2", 445600l);
        Sales dto9 = new Sales("2020-12", "1", 1314100l);
        Sales dto10 = new Sales("2021-01", "1", 5660700l);
        Sales dto11 = new Sales("2021-01", "3", 904300l);
        Sales dto12 = new Sales("2021-01", "1", 8000l);
        Sales dto13 = new Sales("2021-01", "1", 221500l);
        Sales dto14 = new Sales("2021-01", "1", 391700l);
        dtos.add(dto1);
        dtos.add(dto2);
        dtos.add(dto3);
        dtos.add(dto4);
        dtos.add(dto5);
        dtos.add(dto6);
        dtos.add(dto7);
        dtos.add(dto8);
        dtos.add(dto9);
        dtos.add(dto10);
        dtos.add(dto11);
        dtos.add(dto12);
        dtos.add(dto13);
        dtos.add(dto14);
    }
}

 

 

그럼 이제 GroupBy를 이용해 묶어보도록 한다

위의 객체에서 월(yyyymm), 그리고 storeId 별로 묶으려 한다

 

묶을 필드를 클래스로 만든다.

@Getter
@ToString
public class SalesKey {
    private String yyyymm;
    private String storeId;

    public SalesKey(Sales salesDto) {
        this.yyyymm = salesDto.getYyyymm();
        this.storeId = salesDto.getStoreId();
    }
}

 

그리고 stream을 이용해 묶기를 시도한다.

...
    @Test
    @DisplayName("groupBy mulitiple key test")
    void groupByMultipleKeyTest() {
        Map<SalesKey, List<Sales>> collect = dtos.stream()
                .collect(Collectors.groupingBy(SalesKey::new));

        // then
        System.out.println("=======================================");
        for (SalesKey salesKey : collect.keySet()) {
            System.out.println("key:value = " + salesKey + ":" + collect.get(salesKey));
        }
        System.out.println("=======================================");

    }
...

 

리턴된 유형을 보면 Map 형식에서 키=SalesKey와 값=List<Sales> 으로 구성되어 있다. 그룹별로 List객체에 담은 것이다. 그런데 로그를 찍어보니 조금 이상하다.

hashCode: 1008608255
hashCode: 1937693946
hashCode: 468776694
hashCode: 1959758632
hashCode: 939475028
hashCode: 14633842
hashCode: 1053744929
hashCode: 1455177644
hashCode: 8996952
hashCode: 918899286
hashCode: 732189840
hashCode: 2063009760
hashCode: 216746962
hashCode: 1613332278
=======================================
key:value = SalesKey(yyyymm=2020-11, storeId=1):[Sales(yyyymm=2020-11, storeId=1, sales=6233800)]
key:value = SalesKey(yyyymm=2021-01, storeId=3):[Sales(yyyymm=2021-01, storeId=3, sales=904300)]
key:value = SalesKey(yyyymm=2020-11, storeId=2):[Sales(yyyymm=2020-11, storeId=2, sales=437800)]
key:value = SalesKey(yyyymm=2021-01, storeId=1):[Sales(yyyymm=2021-01, storeId=1, sales=221500)]
key:value = SalesKey(yyyymm=2020-12, storeId=2):[Sales(yyyymm=2020-12, storeId=2, sales=445600)]
key:value = SalesKey(yyyymm=2020-12, storeId=1):[Sales(yyyymm=2020-12, storeId=1, sales=2650500)]
key:value = SalesKey(yyyymm=2021-01, storeId=1):[Sales(yyyymm=2021-01, storeId=1, sales=8000)]
key:value = SalesKey(yyyymm=2020-12, storeId=1):[Sales(yyyymm=2020-12, storeId=1, sales=12991300)]
key:value = SalesKey(yyyymm=2020-12, storeId=1):[Sales(yyyymm=2020-12, storeId=1, sales=1314100)]
key:value = SalesKey(yyyymm=2021-01, storeId=1):[Sales(yyyymm=2021-01, storeId=1, sales=391700)]
key:value = SalesKey(yyyymm=2020-12, storeId=2):[Sales(yyyymm=2020-12, storeId=2, sales=108400)]
key:value = SalesKey(yyyymm=2021-01, storeId=1):[Sales(yyyymm=2021-01, storeId=1, sales=5660700)]
key:value = SalesKey(yyyymm=2020-11, storeId=1):[Sales(yyyymm=2020-11, storeId=1, sales=725700)]
key:value = SalesKey(yyyymm=2020-11, storeId=2):[Sales(yyyymm=2020-11, storeId=2, sales=285300)]
=======================================

 

분명 yyyymm와 storeId 별로 묶여야 할거 같은데, 전체 다 1개씩 나열되어 있다. List안에도 1개씩만 들어있다. Java에서는 필드가 아닌 Hash코드를 통해 그룹을 분리한다. 그래서 해시코드를 찍어보면 다음과 같이 되어있다.

...
        for (SalesKey salesKey : collect.keySet()) {
            System.out.println("hashCode: " +salesKey.hashCode());
        }
...

 

그럼 이 문제를 어떻게 해결하면 될까?

Lombok을 사용한다면 @EqualsAndHashCode 를 사용하면 된다.

SalesKey 객체에 다음을 추가한다

@Getter
@ToString
@EqualsAndHashCode // 추가
public class SalesKey {
    private String yyyymm;
    private String storeId;

    public SalesKey(Sales salesDto) {
        this.yyyymm = salesDto.getYyyymm();
        this.storeId = salesDto.getStoreId();
    }
}

 

해시 값을 확인해보면 중복되는건 제거된걸 확인할 수 있다.

hashCode: 490587957
hashCode: 490587958
hashCode: 490588017
hashCode: 492343797
hashCode: 490588016
hashCode: 492343799
=======================================
key:value = SalesKey(yyyymm=2020-11, storeId=1):[Sales(yyyymm=2020-11, storeId=1, sales=6233800), Sales(yyyymm=2020-11, storeId=1, sales=725700)]
key:value = SalesKey(yyyymm=2020-11, storeId=2):[Sales(yyyymm=2020-11, storeId=2, sales=285300), Sales(yyyymm=2020-11, storeId=2, sales=437800)]
key:value = SalesKey(yyyymm=2020-12, storeId=2):[Sales(yyyymm=2020-12, storeId=2, sales=108400), Sales(yyyymm=2020-12, storeId=2, sales=445600)]
key:value = SalesKey(yyyymm=2021-01, storeId=1):[Sales(yyyymm=2021-01, storeId=1, sales=5660700), Sales(yyyymm=2021-01, storeId=1, sales=8000), Sales(yyyymm=2021-01, storeId=1, sales=221500), Sales(yyyymm=2021-01, storeId=1, sales=391700)]
key:value = SalesKey(yyyymm=2020-12, storeId=1):[Sales(yyyymm=2020-12, storeId=1, sales=12991300), Sales(yyyymm=2020-12, storeId=1, sales=2650500), Sales(yyyymm=2020-12, storeId=1, sales=1314100)]
key:value = SalesKey(yyyymm=2021-01, storeId=3):[Sales(yyyymm=2021-01, storeId=3, sales=904300)]
=======================================

 

 

이제야 제대로 그룹별로 묶는것이 보인다.

그럼 이제 월,매장별 매출 갯수와 금액을 합쳐보자.

 

결과가 저장될 객체를 생성한다

@Getter
@ToString
public class SalesGroupBy {
    private SalesKey salesKey;
    private Long salesCount;
    private Long salesSum;

    public SalesGroupBy(SalesKey salesKey, Long salesCount, Long salesSum) {
        this.salesKey = salesKey;
        this.salesCount = salesCount;
        this.salesSum = salesSum;
    }
}

 

 

Collectors.toMap 을 이용해 해당 키에 해당하는 값을 불러와 stream 을 이용해 값을 카운팅하거나 합친 후, 각각의 값을 SalesGroupBy 객체에 주입하면서 생성한다.

...
        Map<SalesGroupBy, SalesKey> collect1 = collect
                .entrySet().stream()
                .collect(Collectors.toMap(o -> {
                    Long salesCount = o.getValue().stream().count();
                    Long salesSum = o.getValue().stream().mapToLong(Sales::getSales).sum();
                    return new SalesGroupBy(o.getKey(), salesCount, salesSum);
                }, Map.Entry::getKey));


        System.out.println("=======================================");
        for (SalesGroupBy salesGroupBy : collect1.keySet()) {
            System.out.println("salesGroupBy = " + salesGroupBy);
        }
        System.out.println("=======================================");
...

 

결과를 보면 다음과 같이 나온다.

=======================================
salesGroupBy = SalesGroupBy(salesKey=SalesKey(yyyymm=2021-01, storeId=1), salesCount=4, salesSum=6281900)
salesGroupBy = SalesGroupBy(salesKey=SalesKey(yyyymm=2020-12, storeId=1), salesCount=3, salesSum=16955900)
salesGroupBy = SalesGroupBy(salesKey=SalesKey(yyyymm=2021-01, storeId=3), salesCount=1, salesSum=904300)
salesGroupBy = SalesGroupBy(salesKey=SalesKey(yyyymm=2020-11, storeId=2), salesCount=2, salesSum=723100)
salesGroupBy = SalesGroupBy(salesKey=SalesKey(yyyymm=2020-11, storeId=1), salesCount=2, salesSum=6959500)
salesGroupBy = SalesGroupBy(salesKey=SalesKey(yyyymm=2020-12, storeId=2), salesCount=2, salesSum=554000)
=======================================

 

원하는데로 값이 샘되어 나온것을 확인할 수 있다.

 

여기서 배운 것.

1) multiple field를 사용할 땐 Key 역할을 할 객체를 새로 만드는게 좋다.

2) Key 객체는 반드시 hashCode 쓰도록 @EqualsAndHashCode 를 사용한다

 

 

끝.