CodeGym /행동 /JAVA 25 SELF /고급 컬렉터

고급 컬렉터

JAVA 25 SELF
레벨 31, 레슨 4
사용 가능

1. 소개

Stream API로 컬렉션을 다룰 때는 종종 단순히 요소를 그룹화하는 것뿐 아니라, 즉시 무언가를 수행해야 합니다: 변환하거나, 필터링하거나, 다른 타입의 컬렉션으로 수집하는 등. 이를 위해 Java에는 각 그룹이나 데이터 조각에 적용되는 downstream 컬렉터 — 중첩 컬렉터가 있습니다.

downstream 컬렉터란?

그룹화 또는 분할의 결과에 적용되는 컬렉터입니다. 예를 들어, groupingBy로 학생을 코스별로 그룹화하고, 각 그룹 안에서는 이름만 또는 우수 학생만(예: GPA 4.5 이상) 수집할 수 있습니다.

예시

mapping

그룹의 요소를 수집하기 전에 변환할 수 있습니다.

Map<Integer, List<String>> namesByCourse = students.stream()
    .collect(Collectors.groupingBy(
        Student::getCourse,
        Collectors.mapping(Student::getName, Collectors.toList())
    ));
  • 학생을 코스별로 그룹화합니다.
  • 각 그룹에서는 전체 객체가 아니라 이름만 수집합니다.

filtering

그룹 내부에서 요소를 필터링할 수 있습니다(예: GPA가 >= 4.5).

Map<Integer, List<Student>> honorsByCourse = students.stream()
    .collect(Collectors.groupingBy(
        Student::getCourse,
        Collectors.filtering(s -> s.getGpa() >= 4.5, Collectors.toList())
    ));

각 그룹에는 우수 학생만 남습니다.

flatMapping

그룹 내부의 중첩 컬렉션을 “펼쳐서” 평탄화할 수 있습니다.

Map<String, Set<String>> tagsByAuthor = books.stream()
    .collect(Collectors.groupingBy(
        Book::getAuthor,
        Collectors.flatMapping(
            book -> book.getTags().stream(),
            Collectors.toSet()
        )
    ));

각 저자에 대해 그의 모든 책의 고유 태그를 수집합니다.

partitioningBy와 downstream

groupingBy와 비슷하게 동작하지만, 불리언 조건으로 두 그룹으로 나눕니다.

Map<Boolean, List<String>> namesByPassed = students.stream()
    .collect(Collectors.partitioningBy(
        s -> s.getGpa() >= 3.0,
        Collectors.mapping(Student::getName, Collectors.toList())
    ));

학생을 합격/불합격으로 나누고, 각 그룹에서는 이름만 수집합니다.

2. teeing: 두 컬렉터로 동시에 집계

가끔 스트림에 대해 여러 집계를 동시에 계산해야 합니다. 예를 들어 합계와 평균, 혹은 최소값과 최대값을 함께 구하고 싶을 때가 있습니다. 이를 위해 Java 12+teeing 컬렉터가 도입되었습니다.

teeing은 어떻게 동작하나요?

두 개의 컬렉터와 그 결과를 결합하는 함수를 전달합니다.

구문:

Collectors.teeing(collector1, collector2, (result1, result2) -> ...)

예시

최소 + 최대

Optional<MinMax> minMax = numbers.stream()
    .collect(Collectors.teeing(
        Collectors.minBy(Integer::compareTo),
        Collectors.maxBy(Integer::compareTo),
        (min, max) -> min.isPresent() && max.isPresent() ? new MinMax(min.get(), max.get()) : null
    ));

최소와 최대를 동시에 구합니다.

합계 + 평균

Result result = numbers.stream()
    .collect(Collectors.teeing(
        Collectors.summingInt(Integer::intValue),
        Collectors.averagingInt(Integer::intValue),
        (sum, avg) -> new Result(sum, avg)
    ));

합계와 평균 값을 담은 객체를 얻습니다.

예시: 급여 리포트

SalaryStats stats = employees.stream()
    .collect(Collectors.teeing(
        Collectors.summingInt(Employee::getSalary),
        Collectors.averagingInt(Employee::getSalary),
        SalaryStats::new
    ));

SalaryStats에 합계와 평균을 모두 저장합니다.

3. toUnmodifiableList/Set/MapcollectingAndThen으로 “동결”하기

최신 Java 버전에는 생성 후 변경할 수 없는 불변(unmodifiable) 컬렉션이 있습니다. 이는 결과가 실수로 변경되지 않도록 해야 하는 API에 유용합니다.

toUnmodifiableList/Set/Map

  • 불변 컬렉션을 반환합니다.
  • 요소를 추가/삭제하려는 모든 시도는 UnsupportedOperationException을 발생시킵니다.

예시:

List<String> names = students.stream()
    .map(Student::getName)
    .collect(Collectors.toUnmodifiableList());
Map<Integer, Student> byId = students.stream()
    .collect(Collectors.toUnmodifiableMap(Student::getId, Function.identity()));

collectingAndThen

컬렉터의 결과에 함수를 적용할 수 있습니다 — 예를 들어 컬렉션을 “동결”할 수 있습니다.

List<String> names = students.stream()
    .map(Student::getName)
    .collect(Collectors.collectingAndThen(
        Collectors.toList(),
        Collections::unmodifiableList
    ));

먼저 일반 리스트로 수집하고, 이후 불변으로 만듭니다.

Set 예시:

Set<String> tags = books.stream()
    .flatMap(book -> book.getTags().stream())
    .collect(Collectors.collectingAndThen(
        Collectors.toSet(),
        Set::copyOf // Java 10+
    ));

4. 파이프라인 사례

리포트와 분할 통계

advanced 컬렉터를 사용하면 복잡한 리포트와 통계를 “한 줄”로 구성할 수 있습니다.

예시: 부서별 평균 급여

Map<String, Double> avgSalaryByDept = employees.stream()
    .collect(Collectors.groupingBy(
        Employee::getDepartment,
        Collectors.averagingInt(Employee::getSalary)
    ));

예시: 카테고리별 최고가 상품 Top 3

Map<String, List<Product>> top3ByCategory = products.stream()
    .collect(Collectors.groupingBy(
        Product::getCategory,
        Collectors.collectingAndThen(
            Collectors.toList(),
            list -> list.stream()
                        .sorted(Comparator.comparing(Product::getPrice).reversed())
                        .limit(3)
                        .toList()
        )
    ));

API 계약으로서의 불변 결과

메서드가 변경할 수 없는 컬렉션을 반환하면, 우발적인 실수를 방지하고 API를 더 견고하게 만듭니다.

public List<String> getTags() {
    return tags.stream()
        .collect(Collectors.toUnmodifiableList());
}

사용자는 getTags().add("새 태그")를 호출할 수 없으며 — 예외가 발생합니다.

예시: 여러 집계를 포함한 리포트 (teeing)

public SalaryReport getSalaryReport(List<Employee> employees) {
    return employees.stream()
        .collect(Collectors.teeing(
            Collectors.averagingInt(Employee::getSalary),
            Collectors.summingInt(Employee::getSalary),
            SalaryReport::new
        ));
}

SalaryReport에 평균과 총급여를 함께 저장합니다.

5. 흔한 오류와 주의사항

오류 №1: 불변성을 잊음. 일반 리스트를 반환하면 외부에서 변경될 수 있습니다. 결과를 “동결”하려면 toUnmodifiableList/Set/Map 또는 collectingAndThen을 사용하세요.

오류 №2: 잘못된 downstream 컬렉터 선택. 그룹 내부에서 요소를 변환해야 한다면 mapping, 필터링해야 한다면 filtering, 중첩 컬렉션을 “펼쳐야” 한다면 flatMapping을 사용하세요.

오류 №3: UnsupportedOperationException. toUnmodifiableList/Set/Map으로 수집했거나 collectingAndThen으로 “동결”한 컬렉션을 변경하려 할 때 발생합니다.

오류 №4: 유일성 손실/키 충돌. toUnmodifiableSet은 요소가 유일해야 하며, toUnmodifiableMap은 키가 유일해야 합니다. 그렇지 않으면 수집 중 예외가 발생합니다.

1
과제
JAVA 25 SELF, 레벨 31, 레슨 4
잠금
위대한 업적 아카데미의 지원자 분류 📜
위대한 업적 아카데미의 지원자 분류 📜
1
과제
JAVA 25 SELF, 레벨 31, 레슨 4
잠금
포트폴리오 빠른 분석 💰
포트폴리오 빠른 분석 💰
1
설문조사/퀴즈
그룹화와 집계, 레벨 31, 레슨 4
사용 불가능
그룹화와 집계
Stream API: 그룹화와 집계
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION