48. 스트림 병렬화는 주의해서 적용하라
자바는 동시성 프로그래밍을 꾸준히 발전시켜왔다
자바 5: java.util.concurrent 라이브러리, 실행자(Executor) 프레임워크 도입
자바 7: 포크-조인(Fork-Join) 패키지 추가
자바 8: 스트림 API와 parallel() 메서드 도입
스트림 병렬화는 다중 코어 프로세서를 활용하여 작업을 여러 스레드에 분산시키는 방법이다
단 한 줄의 코드로 스트림 파이프라인을 병렬 실행할 수 있다
// 순차 스트림
list.stream().map(x -> x * 2).forEach(System.out::println);
// 병렬 스트림 - parallel() 메서드 하나만 추가
list.stream().parallel().map(x -> x * 2).forEach(System.out::println);
병렬화에 적합한 조건
충분히 큰 데이터셋: 데이터가 많을수록 병렬화의 이점이 커진다
효율적으로 분할 가능한 데이터 구조:
ArrayList, HashMap, HashSet, ConcurrentHashMap
배열(Array)
int 범위, long 범위
이 자료구조들은 두 가지 특성을 갖는다
데이터를 쉽게 나눌 수 있음 (Spliterator가 담당)
참조 지역성(locality of reference)이 뛰어남
계산 비용이 큰 연산: 요소당 처리 시간이 많이 걸릴수록 병렬화 효과가 커집니다.
병렬화가 효과적인 코드 예시
소수 계산 예제 - 병렬화에 적합한 경우:
// 순차적 소수 계산
static long pi(long n) {
return LongStream.rangeClosed(2, n)
.mapToObj(BigInteger::valueOf)
.filter(i -> i.isProbablePrime(50))
.count();
}
// 병렬 소수 계산
static long piParallel(long n) {
return LongStream.rangeClosed(2, n)
.parallel()
.mapToObj(BigInteger::valueOf)
.filter(i -> i.isProbablePrime(50))
.count();
}
이 예제에서는 병렬 처리가 순차 처리보다 약 3배 빠르다
병렬화를 피해야 하는 경우
분할하기 어려운 데이터 소스:
// 잘못된 병렬화 예시 (응답 불가 상태 발생 가능) Stream.iterate(BigInteger.ONE, n -> n.add(BigInteger.ONE)) .parallel() .map(p -> p.isProbablePrime(50)) .limit(20) .forEach(System.out::println);
Stream.iterate와 같은 소스는 이전 요소에 의존적이어서 분할이 어렵다
순서에 의존적인 연산: limit(), findFirst() 등은 병렬화와 상충된다
데이터가 적은 경우: 병렬화 오버헤드가 이득보다 클 수 있다
스트림 병렬화의 성능 비교
소규모 데이터 (100개)
5ms
15ms
순차 처리
대규모 데이터 (100만개)
120ms
40ms
병렬 처리
복잡한 연산 (소수 계산)
200ms
60ms
병렬 처리
단순 연산 (덧셈)
3ms
10ms
순차 처리
Stream.iterate + limit
8ms
무한대
순차 처리
병렬 스트림이 작동하는 방식
병렬 스트림은 내부적으로 포크-조인(ForkJoinPool) 프레임워크를 사용한다
병렬 스트림은 기본적으로 공통 ForkJoinPool의 스레드를 사용하므로, 잘못된 병렬화는 다른 부분에도 영향을 줄 수 있다
병렬화 시 주의해야 할 함수 요구사항
스트림 명세는 함수 객체에 대한 중요한 규약을 정의한다
결합법칙(associative)을 만족할 것 : (a op b) op c = a op (b op c)
간섭받지 않을 것(non-interfering) : 파이프라인 실행 중 데이터 소스 변경 금지
상태를 갖지 않을 것(stateless) : 이전 요소의 결과에 의존하지 않아야 함
이러한 요구사항을 지키지 않으면 병렬 실행 시 예상치 못한 결과가 발생할 수 있다
실용적인 병렬화 예제 코드
다양한 자료구조와 작업에 따른 병렬화 효과 비교:
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.IntStream;
public class ParallelStreamDemo {
public static void main(String[] args) {
// 대규모 데이터 준비 (1천만 개)
List<Integer> bigList = IntStream.rangeClosed(1, 10_000_000)
.boxed()
.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
// 간단한 연산 (합계)
System.out.println("간단한 연산 (합계):");
Instant start = Instant.now();
long sum = bigList.stream().mapToLong(i -> i).sum();
System.out.println("순차 처리: " + Duration.between(start, Instant.now()).toMillis() + "ms");
start = Instant.now();
long sumParallel = bigList.parallelStream().mapToLong(i -> i).sum();
System.out.println("병렬 처리: " + Duration.between(start, Instant.now()).toMillis() + "ms");
// 복잡한 연산 (소수 판별)
System.out.println("\n복잡한 연산 (소수 판별):");
start = Instant.now();
long primeCount = bigList.stream()
.limit(100_000)
.filter(ParallelStreamDemo::isPrime)
.count();
System.out.println("순차 처리: " + Duration.between(start, Instant.now()).toMillis() + "ms");
start = Instant.now();
long primeCountParallel = bigList.parallelStream()
.limit(100_000)
.filter(ParallelStreamDemo::isPrime)
.count();
System.out.println("병렬 처리: " + Duration.between(start, Instant.now()).toMillis() + "ms");
}
// 소수 판별 메서드 (계산 비용이 큰 연산)
static boolean isPrime(int n) {
if (n <= 1) return false;
if (n <= 3) return true;
if (n % 2 == 0 || n % 3 == 0) return false;
for (int i = 5; i * i <= n; i += 6) {
if (n % i == 0 || n % (i + 2) == 0) return false;
}
return true;
}
}
핵심 정리
병렬화는 모든 상황에서 성능 향상을 보장하지 않는다
적용 전후 반드시 성능 테스트를 수행하고 결과가 정확한지 확인해야 한다
병렬화에 적합한 자료구조와 연산을 선택해야 한다
공통 ForkJoinPool
에 영향을 주므로 신중하게 적용해야 한다
단순히 .parallel()
을 호출하는 것보다 적절한 상황에서의 적용이 중요하다
| "계산의 정확성과 성능 향상이 확실할 때만 스트림 병렬화를 적용하라."
🧩 어려웠던 점
병렬화가 항상 성능 향상으로 이어지지 않는 이유를 이해하는 데 시간이 필요했다
자료구조별 병렬화 효율성 차이의 원인(분할 용이성과 참조 지역성)을 이해하는 과정이 복잡했다
병렬 스트림에서 함수 요구사항(결합법칙, 비간섭성, 무상태)의 중요성을 인식하는 것이 어려웠다
💭 느낀 점
단순히 새로운 기능을 사용하는 것보다 그 원리와 적용 조건을 이해하는 것이 중요하다
자료구조와 알고리즘에 대한 이해가 효율적인 병렬 처리에 필수적이다
기술의 장단점을 정확히 파악하고 적절히 활용하는 것이 좋은 개발자의 자세라고 생각하게 되었다
Last updated