자바 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