[Java] Stream
자바의 Stream API는 자바 8에서 도입된 함수형 프로그래밍 개념을 지원하는 기능으로, 데이터 컬렉션(예: 배열, 리스트 등)을 효율적이고 간결하게 처리할 수 있게 해줍니다. 스트림은 데이터의 흐름을 처리하기 위한 추상화된 계층으로, 주로 데이터 필터링, 변환, 집계 같은 작업을 수행할 때 사용됩니다.
1. Stream의 개념
• Stream은 데이터 소스를 기반으로 한 데이터 처리 연산의 흐름입니다. Stream은 컬렉션(예: List, Set, Map)이나 배열을 처리하는 데 사용됩니다.
• 스트림은 데이터를 처리할 때 원본 데이터를 변경하지 않고, 데이터 흐름을 함수형 방식으로 처리합니다. 즉, 기존 데이터를 변환하여 새로운 컬렉션을 생성하거나, 값을 집계하는 작업을 수행합니다.
2. Stream의 특징
• 함수형 스타일의 연산: 스트림은 함수형 프로그래밍의 핵심 기능인 맵(Map), 필터(Filter), 리듀스(Reduce) 같은 연산을 제공합니다.
• 지연 연산(Lazy Evaluation): 스트림의 연산은 필요할 때만 실행됩니다. 즉, 스트림에서 최종 연산(Terminal Operation)이 호출될 때까지 중간 연산은 수행되지 않습니다.
• 병렬 처리: 스트림은 기본적으로 순차적으로 처리되지만, **병렬 스트림(Parallel Stream)**을 사용하면 멀티코어 프로세서를 이용한 병렬 처리를 간단하게 구현할 수 있습니다.
• 불변성: 스트림은 원본 데이터를 변경하지 않습니다. 모든 연산은 데이터를 변환한 새로운 스트림을 반환합니다.
3. Stream의 주요 구성 요소
스트림 연산은 크게 두 가지로 나뉩니다:
1. 중간 연산(Intermediate Operation): 지연된 연산으로, 결과를 반환하지 않고 새로운 스트림을 반환합니다. 여러 중간 연산을 체인으로 연결할 수 있습니다.
2. 최종 연산(Terminal Operation): 즉시 실행되는 연산으로, 최종 연산이 호출될 때 스트림의 데이터 처리 흐름이 실행됩니다. 최종 연산은 스트림의 데이터 처리를 종료하고, 결과를 반환하거나, 데이터를 집계합니다.
Stream 연산의 예시
• 중간 연산: map(), filter(), sorted(), distinct(), limit(), skip()
• 최종 연산: forEach(), collect(), count(), reduce(), findFirst(), findAny()
4. Stream API 사용 예시
4.1 기본적인 Stream 사용 예시
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class StreamExample {
public static void main(String[] args) {
List<String> names = Arrays.asList("John", "Jane", "Jack", "Doe", "John");
// 중복을 제거하고 대문자로 변환한 후 출력
List<String> uniqueNames = names.stream()
.distinct() // 중복 제거
.map(String::toUpperCase) // 대문자로 변환
.collect(Collectors.toList()); // 리스트로 수집
System.out.println(uniqueNames); // 출력: [JOHN, JANE, JACK, DOE]
}
}
• stream(): 리스트에서 스트림을 생성합니다.
• distinct(): 리스트의 중복을 제거합니다.
• map(): 각 요소를 대문자로 변환합니다.
• collect(): 스트림의 결과를 리스트로 수집합니다.
5. Stream 연산의 종류
5.1 중간 연산 (Intermediate Operation)
• filter(): 조건을 만족하는 요소들만을 필터링합니다.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> evenNumbers = numbers.stream()
.filter(n -> n % 2 == 0) // 짝수만 필터링
.collect(Collectors.toList());
System.out.println(evenNumbers); // 출력: [2, 4]
• map(): 요소를 변환합니다. 즉, 스트림의 각 요소에 함수를 적용하여 변환된 값을 반환합니다.
List<String> names = Arrays.asList("John", "Jane", "Jack");
List<Integer> nameLengths = names.stream()
.map(String::length) // 이름의 길이를 반환
.collect(Collectors.toList());
System.out.println(nameLengths); // 출력: [4, 4, 4]
• sorted(): 스트림의 요소들을 정렬합니다. 기본적으로 자연 정렬(숫자, 알파벳) 기준으로 정렬하며, Comparator를 사용하여 맞춤형 정렬도 가능합니다.
List<String> names = Arrays.asList("John", "Jane", "Jack");
List<String> sortedNames = names.stream()
.sorted() // 기본 정렬 (알파벳 순)
.collect(Collectors.toList());
System.out.println(sortedNames); // 출력: [Jack, Jane, John]
5.2 최종 연산 (Terminal Operation)
• collect(): 스트림의 데이터를 수집합니다. 보통 리스트나 맵으로 데이터를 모을 때 사용됩니다.
List<String> names = Arrays.asList("John", "Jane", "Jack");
List<String> collectedNames = names.stream()
.collect(Collectors.toList()); // 스트림을 리스트로 변환
• forEach(): 스트림의 각 요소에 대해 동작을 수행합니다. 주로 출력하거나 로그 기록을 할 때 사용됩니다.
List<String> names = Arrays.asList("John", "Jane", "Jack");
names.stream().forEach(System.out::println); // 각 이름을 출력
• reduce(): 스트림의 요소들을 하나의 값으로 집계할 때 사용됩니다. 예를 들어, 숫자의 합, 곱 등을 구할 수 있습니다.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
.reduce(0, (a, b) -> a + b); // 숫자의 합
System.out.println(sum); // 출력: 15
• count(): 스트림의 요소 개수를 반환합니다.
List<String> names = Arrays.asList("John", "Jane", "Jack");
long count = names.stream().count();
System.out.println(count); // 출력: 3
• findFirst(): 스트림에서 첫 번째 요소를 반환합니다. Optional로 반환되며, 값이 없을 경우 안전하게 처리할 수 있습니다.
List<String> names = Arrays.asList("John", "Jane", "Jack");
Optional<String> first = names.stream().findFirst();
first.ifPresent(System.out::println); // 출력: John
6. 병렬 스트림 (Parallel Stream)
스트림은 기본적으로 순차적으로 실행되지만, 병렬 스트림을 사용하면 병렬 처리가 가능합니다. 자바에서는 병렬 스트림을 사용하여 여러 코어를 활용해 대규모 데이터를 빠르게 처리할 수 있습니다.
병렬 스트림 사용 예시
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
// 병렬 스트림으로 처리
numbers.parallelStream()
.map(n -> n * 2)
.forEach(System.out::println);
• parallelStream(): 스트림을 병렬로 처리하도록 전환합니다. 병렬 처리는 멀티코어 프로세서를 활용하여 데이터 처리 속도를 높일 수 있습니다.
• 병렬 스트림을 사용할 때는 순서와 상호작용을 고려해야 하며, 잘못 사용하면 예상치 못한 결과를 초래할 수 있습니다.
7. Stream 사용 시 주의사항
1. 지연 실행(Lazy Evaluation):
• 스트림의 중간 연산은 최종 연산이 호출될 때까지 실행되지 않습니다. 즉, 최종 연산을 호출해야 데이터 처리가 이루어집니다.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
numbers.stream().map(n -> n * 2); // 실제로 실행되지 않음
2. 스트림은 1회성:
• 스트림은 한 번 사용하면 재사용할 수 없습니다. 스트림을 여러 번 사용하려면 다시 생성해야 합니다.
Stream<String> stream = names.stream();
stream.forEach(System.out::println); // 정상 실행
stream.forEach(System.out::println); // 오류 발생 (스트림은 이미 사용됨)
3. 병렬 스트림 사용 시 주의:
• 병렬 스트림은 멀티스레드 환경에서 실행되기 때문에, 데이터 처리 순서나 스레드 안전성이 중요한 작업에서는 신중히 사용해야 합니다.
8. 결론
Stream API는 자바에서 데이터 컬렉션을 함수형 스타일로 처리할 수 있는 강력한 도구입니다. 이를 통해 간결하고 읽기 쉬운 코드를 작성할 수 있으며, 특히 데이터 필터링, 변환, 집계 작업에서 큰 장점을 제공합니다. 스트림의 핵심은 지연 실행과 불변성으로, 원본 데이터를 변경하지 않고 새로운 데이터를 처리하는 방식이기 때문에, 안전하고 효율적인 데이터 처리가 가능합니다.
자바에서 스트림을 잘 활용하면 코드의 가독성을 높이고, 병렬 처리를 통한 성능 최적화도 쉽게 구현할 수 있습니다.