스트림이란 무엇인가?
스트림(Stream)은 자바 8에서 도입된 기능으로, 데이터를 처리하는 일련의 연산들을 하나의 흐름으로 연결하여 수행할 수 있게 해주는 추상화된 데이터 처리 방식이다. 주로 컬렉션이나 배열 같은 데이터 소스를 기반으로 데이터를 필터링, 변환, 집계하는 작업을 효율적으로 수행할 수 있도록 도와준다.
스트림의 핵심 특징
스트림을 이해하기 위해 몇 가지 중요한 특징을 알아두자.
- 데이터 소스 불변성
- 스트림은 데이터 소스를 변경하지 않는다. 스트림의 모든 연산은 새로운 스트림을 반환하며, 원본 데이터는 그대로 유지된다.
- 지연 처리(Lazy Evaluation)
- 스트림의 중간 연산(필터링, 매핑 등)은 실제로 즉시 실행되지 않는다. 최종 연산(forEach, collect 등)이 호출될 때 한꺼번에 처리된다.
- 단일 사용
- 스트림은 한 번 사용되면 재사용할 수 없다. 다시 동일한 작업을 수행하려면 새로운 스트림을 생성해야 한다.
- 병렬 처리 지원
- 스트림은 손쉽게 병렬 처리를 지원한다. parallelStream()을 사용하면 병렬 처리를 통해 성능을 향상시킬 수 있다.
스트림의 주요 연산
스트림 연산은 크게 두 가지로 나눌 수 있다: 중간 연산(Intermediate Operations)과 최종 연산(Terminal Operations)이다.
1. 중간 연산 (Intermediate Operations)
중간 연산은 여러 번 연결해서 사용할 수 있으며, 결과적으로 새로운 스트림을 반환한다. 대표적인 중간 연산으로는 다음과 같은 것들이 있다:
- filter(): 조건에 맞는 요소만 필터링한다.
- map(): 각 요소를 변환하거나 매핑한다.
- sorted(): 스트림의 요소를 정렬한다.
- distinct(): 중복된 요소를 제거한다.
2. 최종 연산 (Terminal Operations)
최종 연산은 스트림의 처리를 마무리하는 연산이다. 최종 연산이 호출되면 즉시 처리가 이루어지며, 스트림은 더 이상 사용할 수 없다.
- forEach(): 각 요소에 대해 작업을 수행한다.
- collect(): 스트림의 결과를 컬렉션(리스트, 세트 등)으로 반환한다.
- count(): 스트림의 요소 개수를 센다.
- reduce(): 스트림의 요소를 하나의 값으로 결합한다.
스트림 코드 예제
이제 스트림을 사용한 실제 코드 예제를 살펴보자. 아래는 짝수만 골라내고, 각 숫자에 2를 곱한 후, 그 결과를 리스트로 반환하는 간단한 스트림 예제이다.
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class StreamExample {
public static void main(String[] args) {
// 숫자 리스트 생성
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// 스트림을 사용한 연산
List<Integer> result = numbers.stream() // 스트림 생성
.filter(n -> n % 2 == 0) // 짝수만 필터링
.map(n -> n * 2) // 각 숫자에 2를 곱함
.sorted() // 정렬
.collect(Collectors.toList()); // 최종 연산: 리스트로 변환
// 결과 출력
System.out.println(result); // [4, 8, 12, 16, 20]
}
}
코드 설명
- numbers.stream(): 리스트로부터 스트림을 생성한다.
- filter(n -> n % 2 == 0): 짝수만 필터링하는 중간 연산이다.
- map(n -> n * 2): 각 요소에 2를 곱하는 변환 작업이다.
- sorted(): 숫자를 오름차순으로 정렬한다.
- collect(Collectors.toList()): 최종 연산으로 결과를 리스트로 반환한다.
병렬 스트림(Parallel Stream)
스트림의 큰 장점 중 하나는 병렬 처리가 가능하다는 것이다. 병렬 처리를 통해 대량의 데이터를 빠르게 처리할 수 있다. parallelStream()을 사용하면 병렬로 데이터를 처리할 수 있다.
List<Integer> parallelResult = numbers.parallelStream()
.filter(n -> n % 2 == 0)
.map(n -> n * 2)
.sorted()
.collect(Collectors.toList());
병렬 스트림은 내부적으로 스레드를 사용해 작업을 나눠서 처리한다. 다만, 병렬 처리의 경우 항상 성능이 향상되는 것은 아니므로, 데이터 양이나 작업 특성에 따라 병렬 스트림을 사용하는 것이 더 적합한지를 판단해야 한다.
스트림 재사용 불가
스트림은 한 번 최종 연산을 수행하면 재사용할 수 없다. 같은 스트림을 다시 사용하려면 새로운 스트림을 생성해야 한다. 아래 코드는 스트림 재사용 시 발생하는 문제를 보여준다.
Stream<Integer> stream = numbers.stream();
stream.filter(n -> n % 2 == 0).forEach(System.out::println); // 첫 번째 사용
stream.filter(n -> n % 2 == 0).forEach(System.out::println); // 예외 발생 (IllegalStateException)
위 코드처럼 한 번 사용된 스트림을 다시 사용하면 IllegalStateException이 발생한다. 따라서 스트림은 항상 한 번만 사용하고, 필요할 경우 새롭게 생성해야 한다.
결론
스트림은 자바에서 데이터를 처리하는 방식을 간결하고 직관적으로 만들어준다. 중간 연산과 최종 연산을 체인으로 연결해 가독성 높은 코드를 작성할 수 있으며, 병렬 처리를 지원해 성능 개선에도 도움을 준다. 다만 스트림은 재사용이 불가능하므로, 여러 번 사용해야 할 경우 새로 생성하는 것이 중요하다.
'JAVA' 카테고리의 다른 글
Java에서 원시 타입과 래퍼 클래스의 이해 (0) | 2024.10.14 |
---|---|
자바 람다식 쉽게 이해하기 (1) | 2024.09.25 |
StringBuilder에 대한 가이드 (0) | 2024.08.07 |
Iterator: 컬렉션을 안전하게 순회하기 (0) | 2024.08.05 |
쓰레드(Thread)란? (0) | 2024.07.25 |