메뉴 닫기

Java Stream에서 collect와 Collector 제대로 활용하는 방법


Java Stream에서 collect와 Collector 제대로 활용하는 방법

📌 리스트, 맵, 그룹핑까지 쉽게! 자바 스트림 수집 메서드 완벽 정리

자바에서 스트림(Stream)을 사용하다 보면 데이터를 원하는 형태로 변환하거나, 통계나 그룹화를 적용하는 상황이 자주 생깁니다.
그럴 때 가장 유용하게 쓰이는 메서드가 바로 collect()입니다.
이 메서드는 스트림의 요소들을 원하는 자료 구조로 모을 수 있게 해주며, Collector와 함께 활용하면 리스트나 맵으로 변환하는 것은 물론, 그룹화, 평균값, 합계 등 다양한 연산까지도 간편하게 처리할 수 있습니다.
이 글에서는 collect() 메서드의 기본적인 사용법부터 고급 Collector 활용법까지 실제 코드와 함께 하나씩 자세히 살펴보겠습니다.

Stream을 처음 접하는 분들도 쉽게 이해할 수 있도록 기초적인 예제부터 차근차근 설명드릴게요.
또한, 실무에서 많이 활용되는 groupingBy, partitioningBy, joining 등의 Collector 기능도 함께 소개하니, 자바 스트림을 제대로 활용하고 싶다면 꼭 끝까지 읽어보세요.







🔗 collect() 메서드란?

자바 스트림(Stream) API에서 collect()는 스트림의 최종 연산 중 하나로, 요소들을 특정한 형태로 수집할 때 사용됩니다.
간단히 말해, 스트림에서 발생한 데이터 흐름을 우리가 원하는 결과물로 ‘수확’하는 단계라고 볼 수 있습니다.
주로 List, Set, Map 등 컬렉션 형태로 결과를 모을 때 사용하며, Collector와 함께 사용되죠.

예를 들어 다음과 같이 사용할 수 있습니다.

CODE BLOCK
List<String> names = 
    people.stream()
          .map(Person::getName)
          .collect(Collectors.toList());

위 코드는 people 리스트에서 이름만 추출한 후, 그 결과를 새로운 List로 수집하는 과정입니다.
스트림은 중간 연산(map, filter 등)과 최종 연산(collect 등)으로 나뉘며, collect()는 이 흐름의 마지막 단계에서 결과를 만들기 위한 도구입니다.

💎 핵심 포인트:
collect()는 스트림의 결과를 다양한 형태로 바꿔주는 ‘변환 도구’이며, 결과 수집은 Collector 인터페이스가 담당합니다.


🛠️ Collector의 기본 동작 방식

collect() 메서드는 내부적으로 Collector 인터페이스를 사용해 데이터 수집 방식을 정의합니다.
Collector는 결과가 어떤 형태로 만들어질지 결정하는 핵심 구성 요소이며, supplier, accumulator, combiner, finisher, characteristics 총 다섯 가지 요소로 구성되어 있습니다.

📌 Collector의 구성 요소 살펴보기

  • 🧰supplier: 결과 컨테이너를 초기화하는 함수
  • accumulator: 스트림 요소를 결과에 누적하는 함수
  • 🔗combiner: 병렬 처리 시 부분 결과를 합치는 함수
  • 🎯finisher: 최종 결과를 변환하는 함수 (보통 생략됨)
  • 🏷️characteristics: Collector의 특성 (예: 병렬 가능 여부)

다행히도 자주 사용하는 수집 작업은 Collectors 클래스에서 기본 구현을 제공하므로, 개발자가 직접 Collector를 만들 일은 드뭅니다.
하지만 동작 원리를 이해하면 복잡한 그룹화나 사용자 정의 수집에도 응용할 수 있습니다.

💡 TIP: 자바 8 이상에서는 Collector 구현체 대신 Collectors.toList()와 같은 팩토리 메서드를 사용해 간단하게 처리할 수 있습니다.







⚙️ 리스트와 맵으로 수집하기

스트림을 사용하다 보면 가장 자주 하게 되는 작업 중 하나가 바로 리스트(List)맵(Map)으로 수집하는 것입니다.
이러한 작업은 Collectors.toList() 또는 Collectors.toMap() 같은 팩토리 메서드를 활용하면 매우 간단하게 처리할 수 있습니다.

📌 리스트로 수집하는 예제

CODE BLOCK
List<String> names = people.stream()
                             .map(Person::getName)
                             .collect(Collectors.toList());

이 코드는 스트림을 통해 Person 객체에서 이름만 추출한 뒤, 새로운 리스트로 수집하는 구조입니다.

📌 맵으로 수집하는 예제

CODE BLOCK
Map<Long, String> idNameMap = people.stream()
    .collect(Collectors.toMap(Person::getId, Person::getName));

위 예제는 각 Person 객체의 ID를 키로, 이름을 값으로 하는 맵을 생성합니다.
중복 키가 없을 경우에는 문제가 없지만, 동일한 키가 발생할 수 있는 경우엔 충돌 처리 함수를 명시해야 합니다.

⚠️ 주의: toMap()은 키 충돌이 발생하면 예외를 발생시키므로, 충돌 가능성이 있다면 병합 함수(mergeFunction)를 꼭 지정해야 합니다.


🔌 groupingBy와 partitioningBy

자바에서 데이터를 조건에 따라 그룹화하거나 분할하고 싶을 때는 groupingBy()partitioningBy()를 사용합니다.
이 두 메서드는 Collectors 클래스에서 제공되며, 복잡한 분류 작업도 간단하게 처리할 수 있도록 도와줍니다.

📌 groupingBy: 특정 조건에 따른 그룹화

CODE BLOCK
Map<String, List<Person>> peopleByCity =
    people.stream()
          .collect(Collectors.groupingBy(Person::getCity));

이 코드는 사람들을 도시(city) 기준으로 묶어서, 도시명별로 그룹화된 맵을 생성합니다.
즉, 키는 도시 이름이고 값은 해당 도시에 사는 사람들의 리스트가 됩니다.

📌 partitioningBy: true/false에 따른 분할

CODE BLOCK
Map<Boolean, List<Person>> partitioned =
    people.stream()
          .collect(Collectors.partitioningBy(p -> p.getAge() >= 18));

partitioningBy는 조건식의 결과가 true/false 두 그룹으로만 나뉘기 때문에 이분법적 분류가 필요할 때 유용합니다.
예제에서는 나이가 18세 이상인 경우와 미만인 경우로 사람들을 나누고 있습니다.

💎 핵심 포인트:
groupingBy는 다수의 그룹을 만들고, partitioningBy는 두 개의 그룹으로만 나눕니다. 상황에 맞게 선택해서 사용하세요.







💡 통계 계산과 문자열 합치기

Collector를 활용하면 단순한 리스트나 맵 수집을 넘어서, 다양한 통계 계산과 문자열 결합도 손쉽게 처리할 수 있습니다.
Collectors 클래스는 이를 위해 counting(), summingInt(), averagingDouble()과 같은 메서드를 제공하며, joining()을 사용하면 문자열 연결도 가능합니다.

📌 개수, 합계, 평균 구하기

CODE BLOCK
long totalCount = people.stream()
                         .collect(Collectors.counting());

int totalAge = people.stream()
                     .collect(Collectors.summingInt(Person::getAge));

double avgAge = people.stream()
                      .collect(Collectors.averagingInt(Person::getAge));

위 코드는 스트림의 요소 개수, 나이 합계, 평균을 구하는 예제입니다.
Collector는 이런 통계 작업도 간편하게 처리할 수 있도록 다양한 헬퍼 메서드를 제공합니다.

📌 joining: 문자열 합치기

CODE BLOCK
String joinedNames = people.stream()
                            .map(Person::getName)
                            .collect(Collectors.joining(", "));

joining() 메서드는 각 요소를 문자열로 연결해주는 기능을 합니다.
위 코드에서는 이름들을 콤마와 공백(“, “)으로 구분하여 하나의 문자열로 합쳤습니다.

💡 TIP: joining()은 구분자 외에도 접두사와 접미사도 지정할 수 있습니다. Collectors.joining(", ", "[", "]") 형태로 사용해보세요.


자바 Collector 자주 묻는 질문 (FAQ)

collect()는 스트림 외에 사용할 수 있나요?
collect()는 스트림의 최종 연산으로만 동작하며, 일반 리스트나 배열에서는 사용할 수 없습니다.
Collector를 직접 구현해야 하는 경우도 있나요?
대부분의 상황에서는 Collectors 클래스의 메서드로 충분하지만, 특수한 수집 방식이 필요하다면 직접 Collector를 구현할 수도 있습니다.
Collectors.toList()와 new ArrayList<>()의 차이는 뭔가요?
toList()는 내부적으로 리스트 타입을 보장하지 않으므로, 반환된 리스트가 수정 불가능할 수도 있습니다. 명확한 타입이 필요하면 직접 리스트를 생성하세요.
groupingBy와 toMap의 차이점은 무엇인가요?
groupingBy는 동일 키에 여러 값을 리스트로 저장하고, toMap은 중복 키가 있을 경우 예외를 발생시킵니다.
toMap()에서 키가 중복될 경우 어떻게 처리하나요?
세 번째 인자로 병합 함수를 전달해야 합니다. 예: Collectors.toMap(k, v, (v1, v2) -> v1)
partitioningBy는 언제 사용하는 게 좋을까요?
true/false로 구분되는 조건이 있을 때 유용합니다. 예: 성인과 미성년자 구분, 유효 여부 판별 등.
joining()에서 구분자 외에도 다른 옵션이 있나요?
네, 접두사와 접미사도 지정할 수 있습니다. 예: joining(“, “, “[“, “]”)
병렬 스트림에서도 Collector를 그대로 사용할 수 있나요?
대부분 가능하지만, Collector의 characteristics에 따라 병렬 처리에 적합하지 않은 경우도 있으니 주의해야 합니다.



🧾 Stream 결과를 다루는 가장 강력한 무기, collect()

자바의 Stream API는 데이터를 선언적으로 다룰 수 있도록 도와주는 훌륭한 도구이며, 그 중심에는 collect() 메서드가 있습니다.
collect()는 단순히 리스트나 맵으로 수집하는 것을 넘어, 통계, 분할, 그룹화, 문자열 처리 등 다양한 기능을 처리할 수 있도록 도와줍니다.
특히 Collector를 함께 활용하면 복잡한 데이터 가공도 한 줄로 해결할 수 있기 때문에, 자바 개발자라면 반드시 익혀야 할 핵심 기능이라 할 수 있습니다.

이번 글을 통해 collect()의 기초적인 개념부터 실무에서 자주 쓰이는 고급 사용법까지 차근히 정리해보았습니다.
개념을 이해하고 직접 코드를 작성해보며 응용한다면, 스트림을 훨씬 유연하고 강력하게 사용할 수 있을 것입니다.
앞으로 Stream을 사용할 때마다 collect()를 적재적소에 활용해보세요.
개발 생산성도, 코드의 가독성도 눈에 띄게 향상될 것입니다.


🏷️ 관련 태그 : java stream, java collect, java collector, 자바스트림, 리스트수집, 맵수집, groupingBy, partitioningBy, joining, 스트림최종연산