Java Stream API는 Java 8에서 도입된 기능으로, 데이터 처리를 간결하고 효율적으로 할 수 있도록 설계되었습니다. Stream API는 대량의 데이터 처리, 병렬 작업, 필터링, 매핑 등의 기능을 제공하며, 코드의 가독성과 유지보수성을 크게 향상시킵니다. 이번 글에서는 Stream API의 심화 개념과 활용 사례를 살펴보겠습니다.
1. Stream API란?
Stream은 컬렉션 데이터를 처리하기 위한 연속된 요소들의 흐름을 나타냅니다. Stream API는 데이터를 처리하기 위한 선언적 방식을 제공하며, 데이터를 필터링하고, 변환하고, 수집할 수 있습니다.
Stream의 주요 특징:
- 선언형 코드 스타일: 데이터 처리 로직을 간결하게 표현합니다.
- 데이터 변경 없음: Stream은 데이터 원본을 변경하지 않습니다.
- Lazy Evaluation: 필요한 시점에만 연산을 실행합니다.
- 병렬 처리 지원: 대규모 데이터를 효율적으로 처리합니다.
Stream 생성 예:
import java.util.*;
import java.util.stream.*;
public class StreamExample {
public static void main(String[] args) {
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
names.stream().forEach(System.out::println);
}
}
2. Stream API의 주요 연산
Stream API는 데이터를 처리하기 위한 두 가지 주요 연산을 제공합니다:
- 중간 연산(Intermediate Operations):
- Stream을 변환하거나 필터링합니다.
- Lazy Evaluation을 사용하며, 최종 연산이 호출될 때까지 실행되지 않습니다.
주요 중간 연산:
filter
: 조건에 따라 요소를 필터링합니다.map
: 요소를 변환합니다.sorted
: 요소를 정렬합니다.distinct
: 중복을 제거합니다.
예:
List<String> filteredNames = names.stream() .filter(name -> name.startsWith("A")) .collect(Collectors.toList());
- 최종 연산(Terminal Operations):
- Stream의 데이터를 처리하고 결과를 반환합니다.
- 최종 연산이 호출되면 Stream은 더 이상 사용할 수 없습니다.
주요 최종 연산:
collect
: 결과를 List, Set 등으로 수집합니다.forEach
: 각 요소를 처리합니다.reduce
: 요소를 하나의 값으로 결합합니다.count
: 요소의 개수를 반환합니다.
예:
long count = names.stream() .filter(name -> name.length() > 3) .count(); System.out.println("이름의 길이가 3보다 큰 요소의 개수: " + count);
3. 고급 Stream API 기능
- FlatMap:
- 여러 컬렉션을 하나의 Stream으로 평탄화합니다.
예:
List<List<String>> nestedList = Arrays.asList( Arrays.asList("A", "B"), Arrays.asList("C", "D") ); List<String> flatList = nestedList.stream() .flatMap(Collection::stream) .collect(Collectors.toList()); System.out.println(flatList); // [A, B, C, D]
- Parallel Streams:
- 데이터를 병렬로 처리하여 성능을 최적화합니다.
예:
List<Integer> numbers = IntStream.range(1, 1000) .boxed() .collect(Collectors.toList()); long sum = numbers.parallelStream() .reduce(0, Integer::sum); System.out.println("병렬 처리 결과: " + sum);
- Collectors API:
- Stream 결과를 List, Set, Map으로 변환하거나 집계 작업을 수행합니다.
예:
Map<Integer, List<String>> groupedByLength = names.stream() .collect(Collectors.groupingBy(String::length)); System.out.println(groupedByLength);
- Custom Collector:
- 사용자 정의 수집기를 만들어 특정 로직을 구현할 수 있습니다.
예:
Collector<String, StringBuilder, String> customCollector = Collector.of( StringBuilder::new, StringBuilder::append, StringBuilder::append, StringBuilder::toString ); String result = names.stream().collect(customCollector); System.out.println(result);
4. Stream API 활용 사례
- 데이터 필터링:
- 대량의 데이터에서 조건에 맞는 요소만 추출합니다.
예:
List<String> emails = users.stream() .filter(user -> user.getAge() > 18) .map(User::getEmail) .collect(Collectors.toList());
- 데이터 집계:
- 데이터의 합계, 평균, 최대값 등을 계산합니다.
예:
double averageAge = users.stream() .collect(Collectors.averagingInt(User::getAge)); System.out.println("평균 나이: " + averageAge);
- 데이터 정렬:
- 데이터를 특정 기준으로 정렬합니다.
예:
List<String> sortedNames = names.stream() .sorted() .collect(Collectors.toList()); System.out.println(sortedNames);
5. Stream API 사용 시 주의사항
- 불필요한 병렬 스트림 사용:
- 병렬 스트림은 데이터 크기가 작거나 작업량이 적을 경우 오히려 성능이 저하될 수 있습니다.
- Stream 재사용 금지:
- Stream은 한 번의 최종 연산 후에는 재사용할 수 없습니다.
- 메모리 관리:
- 대규모 데이터 처리 시 메모리 사용량을 고려해야 합니다.
결론
Stream API는 데이터를 선언적으로 처리할 수 있는 강력한 도구를 제공합니다. 중간 연산과 최종 연산을 조합하여 복잡한 데이터 처리 로직을 간결하고 효율적으로 작성할 수 있으며, 병렬 스트림을 활용하면 성능 최적화도 가능합니다. Stream API의 다양한 기능을 익혀 Java 애플리케이션에서 더욱 생산적인 코드를 작성해 보세요.