EEALL@ONCE

☕Java 스트림과 Collectors의 groupingBy 및 reducing 메서드 본문

언어💻/자바☕

☕Java 스트림과 Collectors의 groupingBy 및 reducing 메서드

올엣원스 2023. 11. 29. 22:02
728x90

List안에 dto 값에 대해 특정 필드를 기준으로 같은 것 끼리 group by 해서 sum을 하고 싶을 뗴, 

stream을 어떻게 잘 사용할 것인가에 대한 공부를 해보겠습니다. 

 

(1) item이라는 dto가 있습니다.

class Item {
    String category;
    int quantity;

    // constructor, getters, setters
}

(2) stream()을 태우고, collect를 사용하는데 그 중에 collector라는 유틸리티를 사용해서, 그룹핑을 (groupingBy를) 통해 시켜줍니다. 그리고 나서 reducing을 통해 더해준다. 

 

List<Item> items = Arrays.asList(
    new Item("A", 2),
    new Item("B", 3),
    new Item("A", 5),
    new Item("B", 2)
);

Map<String, Integer> categorySumMap = items.stream()
    .collect(Collectors.groupingBy(Item::getCategory,
            Collectors.reducing(0, Item::getQuantity, Integer::sum)));

System.out.println(categorySumMap);

 

다시 정리해보면, 

  1. items.stream(): items 리스트를 스트림으로 변환합니다.
  2. .collect(...): 스트림의 요소를 수집합니다.
  3. Collectors.groupingBy(Item::getCategory, ...): Item 객체를 getCategory 메서드의 반환값(카테고리)으로 그룹화합니다. 이 때, 각 카테고리에 속한 Item 객체들은 리스트로 묶입니다.
  4. Collectors.reducing(0, Item::getQuantity, Integer::sum): 각 그룹의 Item 리스트에 대해 Item::getQuantity를 통해 수량을 추출하고, Integer::sum을 통해 그 값을 합산합니다. 초기값으로 0이 사용됩니다.

 

하지만 ! 이렇게 사용했을 때, 짜증나는 일이 발생한다! 그건 바로 여러 필드를 계산할 때는 위의 방식을 쓰기가 어렵다는 것이다. 

여러 필드를 계산해야할 때, 어떤식으로 대체해서 써야하는지 알아보겠습니다.

class Person {
    String name;
    int age;
    double salary;

    // constructor, getters, setters
}

이제 그럼 같은 이름을 가진 사람의 총 나이와 총 셀러리를 구해보는 로직은.. 다음과 같습니다. 

     List<Person> people = Arrays.asList(
                new Person("Alice", 25, 50000.0),
                new Person("Bob", 30, 60000.0),
                new Person("Alice", 35, 70000.0),
                new Person("Bob", 40, 80000.0)
        );

        Map<String, Person> result = people.stream()
                .collect(Collectors.groupingBy(
                        Person::getName,
                        Collectors.reducing(
                                new Person("", 0, 0.0),
                                (p1, p2) -> {
                                    Person mergedPerson = new Person();
                                    mergedPerson.setName(p1.getName());
                                    mergedPerson.setAge(p1.getAge() + p2.getAge());
                                    mergedPerson.setSalary(p1.getSalary() + p2.getSalary());
                                    return mergedPerson;
                                }
                        )
                ));

 

하지만, 아직 궁금증이 남아있습니다. 

만약, grouping 값이 1개라면? 즉 그 카테고리에 하나의 값만 속해있다면...? 

하지만 똑똑하게, 리듀스는 

Collectors.reducing은 스트림의 요소가 1개일 경우에도 리듀스 연산을 수행하며 초기값을 사용하지 않습니다.

따라서 1개의 요소만 있는 경우에는 초기값을 사용하지 않고 해당 요소를 그대로 반환합니다.

라고 합니다. 

하지만, 문제가 있습니다. 그 그룹핑해도 한 개의 값이 있는 것들은 초기화를 하지 않았기 때문에,

기존의 값이 바뀌면 똑같이 바뀐다는 ... .그런 불상사가 있을 수 있습니다. 또한 반대로

그 값을 조작해도 기존의 값이 바뀌어 버리게 되는거죠. 즉 

현재의 reduce 메서드는 인스턴스를 직접 수정하고 있기 때문에, 해당 객체가 참조로 공유될 경우 수정이 반영되게 되는거죠

따라서 조작이 필요할시... 따로 복제해서 조작해야합니다.. !

 

 

콜렉터에 대해 궁금하시면 아래의 포스팅을 확인해주세요.

https://at-once-moment.tistory.com/121

 

728x90