Stream()과 parallelStream()은 둘 다 Java Stream API에서 제공하는 메소드로, 데이터를 처리하는 방법을 정의합니다.
두 메소드는 데이터의 처리 방식을 달리하며, 각각의 사용 방식과 차이점을 이해하는 것이 중요합니다.
여기서는 Stream()과 parallelStream()의 차이점과 사용 방법을 상세하게 설명하겠습니다.
1. Stream() (직렬 스트림)
Stream()은 기본적으로 직렬 스트림을 생성합니다. 즉, 하나의 쓰레드를 사용하여 순차적으로 데이터를 처리합니다.
특징:
- 순차적 처리: 모든 작업이 하나의 쓰레드에서 순차적으로 실행됩니다. 데이터가 하나씩 처리되는 방식입니다.
- 데이터 처리 순서 보장: 순차 스트림에서는 입력 데이터의 순서가 보장됩니다. 처리 결과도 원본 데이터의 순서와 동일하게 유지됩니다.
- 적은 오버헤드: 병렬 스트림에 비해 오버헤드가 적고, 비교적 작은 데이터 집합에서는 빠르게 처리할 수 있습니다.
사용법 예시:
import java.util.Arrays;
import java.util.List;
public class StreamExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// 직렬 스트림 사용 (stream())
numbers.stream()
.filter(n -> n % 2 == 0) // 짝수만 필터링
.map(n -> n * 2) // 2배로 변환
.forEach(System.out::println); // 출력
}
}
이 코드에서 stream()을 사용하면, 리스트에 있는 숫자들이 순차적으로 처리됩니다. filter()와 map() 연산이 하나씩 차례대로 적용되며, 결과가 출력됩니다.
2. parallelStream() (병렬 스트림)
parallelStream()은 병렬 스트림을 생성합니다. 즉, 데이터를 여러 쓰레드에서 병렬로 처리할 수 있게 해줍니다.
특징:
- 병렬 처리: 멀티 코어 프로세서를 활용하여 여러 쓰레드에서 데이터를 병렬로 처리합니다. 데이터가 동시에 여러 쓰레드에서 처리되어 속도가 빨라질 수 있습니다.
- 데이터 처리 순서 불확실: 병렬 스트림에서는 데이터 처리 순서가 보장되지 않으므로, 결과의 순서가 원본 데이터와 다를 수 있습니다.
- 높은 오버헤드: 병렬 처리는 여러 쓰레드를 관리하고 결과를 합치는 작업에 오버헤드가 발생합니다. 데이터 크기가 작거나 처리 시간이 짧은 경우에는 오히려 성능이 떨어질 수 있습니다.
사용법 예시:
import java.util.Arrays;
import java.util.List;
public class ParallelStreamExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// 병렬 스트림 사용 (parallelStream())
numbers.parallelStream()
.filter(n -> n % 2 == 0) // 짝수만 필터링
.map(n -> n * 2) // 2배로 변환
.forEach(System.out::println); // 출력
}
}
위 코드에서 parallelStream()을 사용하면, 짝수 필터링과 2배 변환 작업이 여러 쓰레드에서 동시에 실행됩니다. 이로 인해 데이터가 병렬로 처리되어 더 빠르게 결과를 얻을 수 있습니다.
3. Stream()과 parallelStream()의 차이점
특징 Stream() (직렬 스트림) parallelStream() (병렬 스트림)
처리 방식 | 순차적으로 하나의 쓰레드에서 처리 | 여러 쓰레드에서 병렬로 처리 |
데이터 순서 보장 | 입력 데이터의 순서를 그대로 유지 | 순서가 보장되지 않음 |
성능 | 작은 데이터 집합에서는 빠를 수 있음, 오버헤드 적음 | 큰 데이터 집합에서는 성능 향상 가능, 하지만 오버헤드 존재 |
오버헤드 | 오버헤드가 거의 없음 | 멀티 쓰레드 관리와 데이터 결합에 따른 오버헤드가 발생 |
적합한 상황 | 데이터 양이 적을 때, 작업이 복잡하지 않거나 상태 변경이 필요한 경우 | 데이터 양이 많고, 독립적인 연산이 필요한 경우 (CPU 집약적인 작업) |
4. Stream()과 parallelStream()을 선택할 때 고려할 점
- 작은 데이터 집합: 데이터 양이 적을 경우, Stream()이 더 효율적입니다. 병렬 스트림은 쓰레드를 관리하는 오버헤드가 크므로, 작은 데이터 집합에서는 성능 향상보다는 오히려 성능 저하를 초래할 수 있습니다.
- 큰 데이터 집합: 데이터 양이 많고 CPU 집약적인 연산을 수행할 경우 parallelStream()이 유리할 수 있습니다. 멀티 코어 프로세서를 활용하여 성능을 개선할 수 있습니다.
- 상태 변경이 없는 연산: 병렬 스트림은 상태를 변경하지 않는 연산에 적합합니다. 상태 변경이 필요한 연산은 동기화 문제를 일으킬 수 있으며, 이는 성능을 저하시킬 수 있습니다.
- 순서가 중요한 경우: 순차적으로 데이터를 처리해야 하거나 결과 순서가 중요한 경우 Stream()을 사용해야 합니다. parallelStream()에서는 순서가 보장되지 않기 때문에, 순서가 중요한 작업에는 부적합합니다.
5. 결론
- *Stream()*은 간단하고 작은 데이터 집합에 적합하며, 처리 순서가 중요하고 오버헤드가 적은 연산에 적합합니다.
- *parallelStream()*은 데이터가 많고 CPU 집약적인 작업을 할 때 성능을 향상시킬 수 있지만, 오버헤드가 있기 때문에 항상 성능이 좋아지는 것은 아닙니다. 데이터 처리 순서가 중요하지 않은 경우에 사용하는 것이 좋습니다.
**추가
병렬 스트림의 장점
- 성능 향상: 대량의 데이터를 처리할 때, 멀티 코어 프로세서를 활용하여 처리 성능을 높일 수 있습니다.
- 쉬운 병렬 처리: 기존의 직렬 스트림과 동일한 API를 사용하면서, 병렬 처리를 쉽게 구현할 수 있습니다.
병렬 스트림과 직렬 스트림의 차이
- 직렬 스트림: 데이터 처리 순서가 보장되며, 하나의 쓰레드에서 데이터를 처리합니다.
- 예: stream().forEach()
- 병렬 스트림: 데이터 처리 순서가 보장되지 않으며, 여러 쓰레드에서 데이터를 처리합니다.
- 예: parallelStream().forEach()
병렬 스트림 사용 시 고려 사항
- 상태 변경이 없는 작업에 적합: 병렬 스트림을 사용할 때 각 작업이 서로 영향을 미치지 않도록 해야 합니다. 예를 들어, 데이터를 동시에 수정하는 작업은 예기치 않은 결과를 초래할 수 있습니다.
- 작업의 복잡성: 너무 작은 작업은 병렬화할 때 오히려 성능이 떨어질 수 있습니다. 병렬 처리는 오버헤드가 발생하기 때문에, 작업 크기가 충분히 커야 성능이 향상됩니다.
비슷한 것들
- parallel(): Stream에 대한 병렬 처리 모드를 설정하는 방법입니다. stream()을 parallel()로 변경하면 병렬 스트림이 됩니다.
- java 복사편집 stream().parallel().forEach(System.out::println);
- ForkJoinPool: parallelStream()은 내부적으로 ForkJoinPool을 사용하여 멀티 쓰레드를 관리합니다.
유사한 것들
- CompletableFuture: 비동기 작업을 처리하는 클래스로, 병렬 처리를 지원합니다. parallelStream()과 유사하게 멀티 쓰레드를 사용하지만 더 복잡한 제어가 가능합니다.
- ExecutorService: 여러 쓰레드를 관리하고 작업을 분배하는 방법입니다. parallelStream()은 간단한 방법이고, ExecutorService는 더 세밀한 제어를 할 수 있습니다.
정반대되는 것
- forEachOrdered(): 병렬 스트림에서 forEach()는 순서를 보장하지 않지만, forEachOrdered()는 병렬 스트림에서도 원본 순서를 보장합니다.
- 직렬 처리: stream()은 순차적으로 한 쓰레드에서 처리되므로 멀티 코어 환경에서 성능 향상이 이루어지지 않습니다.
병렬 스트림을 사용할 때는 병렬화가 항상 성능 향상을 보장하지 않기 때문에,
적절한 상황에서만 사용하는 것이 중요합니다.
기본 사용 방법
parallelStream()은 기존의 stream() 메소드 대신 사용할 수 있습니다. 사용법은 간단하며, 데이터 컬렉션에 대해 parallelStream()을 호출하면 자동으로 병렬 처리가 됩니다.
예시 1: parallelStream() 사용
java
복사편집
import java.util.Arrays;
import java.util.List;
public class ParallelStreamExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// 병렬 스트림 사용
numbers.parallelStream()
.forEach(n -> System.out.println(Thread.currentThread().getName() + ": " + n));
}
}
위 코드는 parallelStream()을 사용하여 각 숫자를 출력하는 예제입니다. 각 출력은 다른 쓰레드에서 이루어지며, 실행 순서는 보장되지 않습니다.
주요 메소드
- forEach(): 스트림의 모든 요소에 대해 주어진 작업을 실행합니다. 병렬 스트림에서는 순서가 보장되지 않습니다.
- map(): 각 요소에 대해 변환 작업을 수행합니다.
- filter(): 조건을 만족하는 요소만 필터링합니다.
- reduce(): 스트림의 모든 요소를 하나로 결합합니다. 병렬 스트림에서는 병합 작업이 필요하므로 주의해야 합니다.
예시 2: map()과 filter() 사용
java
복사편집
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class ParallelStreamExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// 병렬 스트림을 사용하여 짝수만 필터링하고 각 값에 2를 곱함
List<Integer> result = numbers.parallelStream()
.filter(n -> n % 2 == 0)
.map(n -> n * 2)
.collect(Collectors.toList());
System.out.println(result); // [4, 8, 12, 16, 20]
}
}
사용 사례
병렬 스트림은 주로 CPU가 여러 개의 코어를 가지고 있을 때 성능을 개선할 수 있습니다. 적절한 사용 사례는 다음과 같습니다.
1. 대규모 데이터 처리
대량의 데이터를 처리할 때 병렬 스트림을 사용하면 작업을 여러 코어로 분산시켜 성능을 향상시킬 수 있습니다. 예를 들어, 대규모 로그 파일을 분석하거나, 대량의 데이터를 정렬할 때 유용합니다.
예시: 대규모 데이터 필터링
java
복사편집
import java.util.List;
import java.util.ArrayList;
import java.util.stream.Collectors;
public class ParallelStreamExample {
public static void main(String[] args) {
// 대규모 데이터 생성
List<Integer> numbers = new ArrayList<>();
for (int i = 1; i <= 1000000; i++) {
numbers.add(i);
}
// 병렬 스트림을 사용하여 짝수만 필터링
List<Integer> evenNumbers = numbers.parallelStream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
System.out.println("짝수의 개수: " + evenNumbers.size());
}
}
2. 데이터 병합
여러 개의 데이터를 병합하는 작업을 병렬 스트림을 통해 빠르게 처리할 수 있습니다. 예를 들어, 여러 개의 파일에서 데이터를 읽고 처리할 때 유용합니다.
예시: 여러 파일에서 데이터 처리
java
복사편집
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;
import java.util.stream.Collectors;
public class ParallelStreamExample {
public static void main(String[] args) throws Exception {
// 여러 파일의 경로 리스트
List<String> filePaths = List.of("file1.txt", "file2.txt", "file3.txt");
// 각 파일의 데이터를 병렬로 읽고 결합
List<String> allLines = filePaths.parallelStream()
.flatMap(filePath -> {
try {
return Files.lines(Paths.get(filePath));
} catch (Exception e) {
return Stream.empty();
}
})
.collect(Collectors.toList());
// 읽은 모든 데이터를 출력
allLines.forEach(System.out::println);
}
}
성능 고려사항
병렬 스트림이 항상 성능을 향상시키는 것은 아닙니다. 병렬 처리에서 오버헤드가 발생할 수 있으므로, 다음과 같은 상황에서는 병렬 스트림을 사용하지 않는 것이 더 효율적일 수 있습니다:
- 작은 데이터 집합: 데이터 양이 적은 경우, 병렬 처리가 오히려 성능을 저하시킬 수 있습니다.
- 상태 변경 작업: 병렬 스트림을 사용할 때 상태 변경이 없는 작업에만 적합합니다. 상태를 공유하는 작업은 동기화 문제를 발생시킬 수 있습니다.
결론
- *parallelStream()*은 대규모 데이터를 병렬로 처리하여 성능을 향상시킬 수 있는 기능입니다.
- 성능 개선은 데이터를 처리하는 CPU의 코어 수와 작업의 특성에 따라 달라지므로, 언제 병렬 스트림을 사용할지 잘 고려해야 합니다.
'개발공부 > Java(JPA)' 카테고리의 다른 글
Cache#2 캐시에 대한 이해 (1) | 2025.02.24 |
---|---|
Cache#1 스프링의 캐시 추상화(@Cacheable) (0) | 2025.02.24 |
[JPA] JPA save() 반복 호출로 DB 커넥션 과부하 해결 (0) | 2025.02.18 |
[JPA] for문으로 save() 할 경우 생기는 이슈 및 처리 (0) | 2025.02.18 |
[Java] Map 과 HashMap의 차이점! (0) | 2025.02.17 |