Java Stream에서 collect와 Collector 제대로 활용하는 방법
📌 리스트, 맵, 그룹핑까지 쉽게! 자바 스트림 수집 메서드 완벽 정리
자바에서 스트림(Stream)을 사용하다 보면 데이터를 원하는 형태로 변환하거나, 통계나 그룹화를 적용하는 상황이 자주 생깁니다.
그럴 때 가장 유용하게 쓰이는 메서드가 바로 collect()입니다.
이 메서드는 스트림의 요소들을 원하는 자료 구조로 모을 수 있게 해주며, Collector와 함께 활용하면 리스트나 맵으로 변환하는 것은 물론, 그룹화, 평균값, 합계 등 다양한 연산까지도 간편하게 처리할 수 있습니다.
이 글에서는 collect() 메서드의 기본적인 사용법부터 고급 Collector 활용법까지 실제 코드와 함께 하나씩 자세히 살펴보겠습니다.
Stream을 처음 접하는 분들도 쉽게 이해할 수 있도록 기초적인 예제부터 차근차근 설명드릴게요.
또한, 실무에서 많이 활용되는 groupingBy, partitioningBy, joining 등의 Collector 기능도 함께 소개하니, 자바 스트림을 제대로 활용하고 싶다면 꼭 끝까지 읽어보세요.
📋 목차
🔗 collect() 메서드란?
자바 스트림(Stream) API에서 collect()는 스트림의 최종 연산 중 하나로, 요소들을 특정한 형태로 수집할 때 사용됩니다.
간단히 말해, 스트림에서 발생한 데이터 흐름을 우리가 원하는 결과물로 ‘수확’하는 단계라고 볼 수 있습니다.
주로 List, Set, Map 등 컬렉션 형태로 결과를 모을 때 사용하며, Collector와 함께 사용되죠.
예를 들어 다음과 같이 사용할 수 있습니다.
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() 같은 팩토리 메서드를 활용하면 매우 간단하게 처리할 수 있습니다.
📌 리스트로 수집하는 예제
List<String> names = people.stream()
.map(Person::getName)
.collect(Collectors.toList());
이 코드는 스트림을 통해 Person 객체에서 이름만 추출한 뒤, 새로운 리스트로 수집하는 구조입니다.
📌 맵으로 수집하는 예제
Map<Long, String> idNameMap = people.stream()
.collect(Collectors.toMap(Person::getId, Person::getName));
위 예제는 각 Person 객체의 ID를 키로, 이름을 값으로 하는 맵을 생성합니다.
중복 키가 없을 경우에는 문제가 없지만, 동일한 키가 발생할 수 있는 경우엔 충돌 처리 함수를 명시해야 합니다.
⚠️ 주의: toMap()은 키 충돌이 발생하면 예외를 발생시키므로, 충돌 가능성이 있다면 병합 함수(mergeFunction)를 꼭 지정해야 합니다.
🔌 groupingBy와 partitioningBy
자바에서 데이터를 조건에 따라 그룹화하거나 분할하고 싶을 때는 groupingBy()와 partitioningBy()를 사용합니다.
이 두 메서드는 Collectors 클래스에서 제공되며, 복잡한 분류 작업도 간단하게 처리할 수 있도록 도와줍니다.
📌 groupingBy: 특정 조건에 따른 그룹화
Map<String, List<Person>> peopleByCity =
people.stream()
.collect(Collectors.groupingBy(Person::getCity));
이 코드는 사람들을 도시(city) 기준으로 묶어서, 도시명별로 그룹화된 맵을 생성합니다.
즉, 키는 도시 이름이고 값은 해당 도시에 사는 사람들의 리스트가 됩니다.
📌 partitioningBy: true/false에 따른 분할
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()을 사용하면 문자열 연결도 가능합니다.
📌 개수, 합계, 평균 구하기
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: 문자열 합치기
String joinedNames = people.stream()
.map(Person::getName)
.collect(Collectors.joining(", "));
joining() 메서드는 각 요소를 문자열로 연결해주는 기능을 합니다.
위 코드에서는 이름들을 콤마와 공백(“, “)으로 구분하여 하나의 문자열로 합쳤습니다.
💡 TIP: joining()은 구분자 외에도 접두사와 접미사도 지정할 수 있습니다. Collectors.joining(", ", "[", "]") 형태로 사용해보세요.
❓ 자바 Collector 자주 묻는 질문 (FAQ)
collect()는 스트림 외에 사용할 수 있나요?
Collector를 직접 구현해야 하는 경우도 있나요?
Collectors.toList()와 new ArrayList<>()의 차이는 뭔가요?
groupingBy와 toMap의 차이점은 무엇인가요?
toMap()에서 키가 중복될 경우 어떻게 처리하나요?
Collectors.toMap(k, v, (v1, v2) -> v1)
partitioningBy는 언제 사용하는 게 좋을까요?
joining()에서 구분자 외에도 다른 옵션이 있나요?
병렬 스트림에서도 Collector를 그대로 사용할 수 있나요?
🧾 Stream 결과를 다루는 가장 강력한 무기, collect()
자바의 Stream API는 데이터를 선언적으로 다룰 수 있도록 도와주는 훌륭한 도구이며, 그 중심에는 collect() 메서드가 있습니다.
collect()는 단순히 리스트나 맵으로 수집하는 것을 넘어, 통계, 분할, 그룹화, 문자열 처리 등 다양한 기능을 처리할 수 있도록 도와줍니다.
특히 Collector를 함께 활용하면 복잡한 데이터 가공도 한 줄로 해결할 수 있기 때문에, 자바 개발자라면 반드시 익혀야 할 핵심 기능이라 할 수 있습니다.
이번 글을 통해 collect()의 기초적인 개념부터 실무에서 자주 쓰이는 고급 사용법까지 차근히 정리해보았습니다.
개념을 이해하고 직접 코드를 작성해보며 응용한다면, 스트림을 훨씬 유연하고 강력하게 사용할 수 있을 것입니다.
앞으로 Stream을 사용할 때마다 collect()를 적재적소에 활용해보세요.
개발 생산성도, 코드의 가독성도 눈에 띄게 향상될 것입니다.
🏷️ 관련 태그 : java stream, java collect, java collector, 자바스트림, 리스트수집, 맵수집, groupingBy, partitioningBy, joining, 스트림최종연산