CATEGORY

카테고리 (607)
AI (12)
Language & Specs (259)
FrameWork (34)
Library (19)
App (41)
Git (8)
Build & Dependency (2)
AWS (15)
DataBase (45)
OS (33)
Tool (16)
IT (120)
반응형
SEEMINGLY ONLINE

Seemingly
Online

이모저모 방방곡곡 두루두루 개발지식 저장소

RECENT POSTS

Language & Specs/Java

자바 stream groupingBy 예제 — 그룹핑·카운팅·다중키 한 번에 정리

반응형

Collectors.groupingBy는 스트림 요소를 특정 키로 묶어 Map<K, List<T>>를 만드는 수집기입니다. 오버로드가 3개라 ① 부서별로 묶기(기본), ② 부서별 인원수·합계 세기(다운스트림), ③ 정렬된 Map으로 받기(mapFactory) 순서로 익히면 됩니다. 아래 예제는 전부 Java 21에서 직접 실행해 출력까지 확인한 것입니다.

1. groupingBy 시그니처 — 오버로드 3개

공식 문서(Java SE 21)의 시그니처는 이렇습니다. 두 번째 인자는 다운스트림 수집기(집계 방식), 세 번째는 Map 공급자(어떤 Map 구현체로 받을지)입니다.

// ① 분류 함수 하나 → Map<K, List<T>>
groupingBy(Function classifier)

// ② + 다운스트림 수집기 → Map<K, D>
groupingBy(Function classifier, Collector downstream)

// ③ + Map 공급자 → M extends Map<K, D>
groupingBy(Function classifier, Supplier mapFactory, Collector downstream)

예제에 쓸 데이터는 아래 record 하나입니다.

import java.util.*;
import static java.util.stream.Collectors.*;

record Emp(String name, String dept, int salary) {}

List<Emp> emps = List.of(
    new Emp("Kim","dev",5000),  new Emp("Lee","dev",6000),
    new Emp("Park","sales",4000), new Emp("Choi","sales",4500),
    new Emp("Jung","hr",3800));

2. 기본 그룹핑 — 부서별로 묶기

분류 함수 하나만 넘기면 키별 List가 만들어집니다.

Map<String, List<Emp>> byDept =
    emps.stream().collect(groupingBy(Emp::dept));

// 실행 결과
// keys = [dev, hr, sales]
// byDept.get("dev").size() = 2

3. 다운스트림 수집기 — 세고, 더하고, 평균 내기

두 번째 인자로 counting()·summingInt()·averagingInt()·mapping()을 넘기면 그룹마다 집계합니다. 실무에서 가장 많이 쓰는 형태입니다.

// 부서별 인원수
Map<String, Long> count =
    emps.stream().collect(groupingBy(Emp::dept, counting()));
// {dev=2, hr=1, sales=2}

// 부서별 급여 합계
Map<String, Integer> sum =
    emps.stream().collect(groupingBy(Emp::dept, summingInt(Emp::salary)));
// {dev=11000, hr=3800, sales=8500}

// 부서별 평균 급여
Map<String, Double> avg =
    emps.stream().collect(groupingBy(Emp::dept, averagingInt(Emp::salary)));
// {dev=5500.0, hr=3800.0, sales=4250.0}

// 부서별 이름만 뽑아 리스트로 (mapping)
Map<String, List<String>> names =
    emps.stream().collect(groupingBy(Emp::dept, mapping(Emp::name, toList())));
// {dev=[Kim, Lee], hr=[Jung], sales=[Park, Choi]}
다운스트림 그룹마다 하는 일 값 타입
counting() 개수 세기 Long
summingInt() 합계 Integer
averagingInt() 평균 Double
mapping(f, toList()) 값 변환 후 모으기 List<R>
maxBy(cmp) 그룹 내 최댓값 Optional<T>

maxBy·minBy는 값이 Optional로 나옵니다. 그룹이 비어 있을 수 있어서인데, 꺼낼 때는 get() 대신 map()·orElse()를 쓰는 게 안전합니다. 이 부분은 자바 Optional 사용법(map·orElseGet·orElseThrow)에서 자세히 다뤘습니다.

Map<String, Optional<Emp>> top =
    emps.stream().collect(groupingBy(Emp::dept,
        maxBy(Comparator.comparingInt(Emp::salary))));

top.get("dev").map(Emp::name).orElse("none");  // "Lee"

4. 반환 Map은 HashMap — 순서가 안 지켜진다 (함정)

기본 groupingBy가 돌려주는 Map은 HashMap이라 키 순서가 보장되지 않습니다. 공식 문서도 "반환되는 Map의 타입·가변성·직렬화·스레드 안전성에 대한 보장이 없다"고 못 박습니다. 정렬된 결과가 필요하면 세 번째 오버로드에 TreeMap::new를 넘기세요.

⚠️ groupingBy(...)의 결과 순서에 의존하는 코드는 언젠가 깨집니다. 키를 정렬하려면 TreeMap, 삽입 순서를 유지하려면 LinkedHashMap 공급자를 명시하세요.
// 키(부서명)로 정렬된 TreeMap으로 받기
Map<String, Long> sorted =
    emps.stream().collect(groupingBy(Emp::dept, TreeMap::new, counting()));
// {dev=2, hr=1, sales=2}, 실제 구현체 = TreeMap

5. 다중 키 그룹핑 — groupingBy를 겹치기

다운스트림 자리에 groupingBy를 한 번 더 넣으면 2단계로 묶입니다. "부서 → 도시 → 이름 목록"처럼요.

record Emp(String name, String dept, String city, int salary) {}

Map<String, Map<String, List<String>>> nested =
    emps.stream().collect(
        groupingBy(Emp::dept,
            groupingBy(Emp::city,
                mapping(Emp::name, toList()))));

// {dev={Busan=[Lee], Seoul=[Kim]}, sales={Seoul=[Park]}}

6. partitioningBy — 참/거짓 두 그룹으로만

조건 하나로 true/false 두 덩어리로만 나눌 때는 groupingBy보다 partitioningBy가 맞습니다. 키가 항상 true·false 둘 다 존재한다는 게 차이입니다(빈 쪽도 빈 리스트로 들어옴).

Map<Boolean, List<String>> part =
    emps.stream().collect(
        partitioningBy(e -> e.salary() >= 4500,
            mapping(Emp::name, toList())));

// {false=[Park, Jung], true=[Kim, Lee, Choi]}

자주 묻는 질문 (FAQ)

Q. groupingBy와 partitioningBy는 언제 갈라 쓰나요?
키가 여러 개면 groupingBy, 조건 하나로 참/거짓 두 그룹이면 partitioningBy입니다. partitioningBy는 조건에 맞는 요소가 없어도 false 키에 빈 리스트가 들어와, 항상 키 2개가 보장됩니다.

Q. 결과 Map의 순서가 실행할 때마다 다릅니다.
기본 반환이 HashMap이라 정상입니다. 순서가 필요하면 세 번째 오버로드에 TreeMap::new(키 정렬)나 LinkedHashMap::new(삽입 순서)를 넘기세요.

Q. 값을 List가 아니라 Set으로 받고 싶어요.
다운스트림에 toSet()을 쓰면 됩니다: groupingBy(Emp::dept, toSet()).

마무리

groupingBy①기본 → ②다운스트림 집계 → ③Map 공급자 세 오버로드만 잡으면 대부분의 집계를 커버합니다. 순서가 필요하면 TreeMap, 참/거짓 둘로만 나눌 땐 partitioningBy를 기억하세요. 정확한 시그니처와 보장 범위는 항상 공식 API 문서로 확인하는 게 안전합니다.


📚 참고 출처 (2026년 7월 20일 확인)
· Oracle Java SE 21 API — Collectors (groupingBy·partitioningBy 시그니처, Map 보장 없음)
· 본문 모든 출력값은 Java 21에서 직접 컴파일·실행해 확인.

반응형

COMMENTS