1. Question

병렬 스트림(Parallel Stream)이란?

2. Answer

병렬 스트림(Parallel Stream)은 Java에서 데이터를 병렬로 처리하여 성능을 향상시킬 수 있는 강력한 기능이다. 여러 CPU 코어를 활용하여 작업을 동시에 실행함으로써, 특히 대용량 데이터 처리에 있어서 시간을 절약할 수 있다. 그러나, 병렬 스트림의 사용은 적절한 상황에서 이루어져야 하며, 잘못 사용되었을 때는 오히려 성능 저하를 가져올 수 있다. 다음 코드에서와 같이, 순차 스트림에 parallel 메서드를 호출하면 기존의 함수형 리듀싱 연산(숫자 합계 계산)이 병렬로 처리된다.

public long parallelSum(long n) {
  return Stream.iterate(1L, i -> i + 1)
    .limit(n)
    .parallel() // 스트림을 병렬 스트림으로 변환
    .reduce(0L, Long::sum);
}

3. Detail

A. 내부 작동 원리

  • Fork/Join Framework: 병렬 스트림은 Java 7에 도입된 Fork/Join Framework를 기반으로 한다. 이 프레임워크는 큰 작업을 작은 작업으로 분할하고, 각각의 작업을 별도의 스레드에서 처리한 다음, 결과를 결합하는 방식으로 동작한다.

  • 작업 분할: 데이터 소스(예: 컬렉션, 배열 등)는 여러 청크로 분할된다. 이는 병렬 처리의 기본 단위가 되며, 각 청크는 별도의 스레드에서 처리된다.

  • 병렬 실행: 각 청크는 Java의 스레드 풀에서 관리되는 스레드에서 병렬로 처리된다. CPU 코어의 수에 따라, 동시에 실행될 수 있는 스레드의 수가 결정된다.

  • 결과 결합: 모든 청크의 처리가 완료되면, 각 청크의 결과는 최종 결과로 결합된다. 결합 과정은 연산의 종류(예: reduce, collect)에 따라 다를 수 있다.

B. 사용 방법

  • 컬렉션의 parallelStream() 메서드를 호출하거나, 일반 스트림에 대해 parallel() 메서드를 호출하여 병렬 스트림을 얻을 수 있다.
List<String> list = Arrays.asList("apple", "banana", "cherry");
Stream<String> parallelStream = list.parallelStream();
  • 병렬 스트림에서는 map, filter, reduce, collect 등의 연산을 사용할 수 있으며, 이러한 연산은 병렬로 실행된다.
int sum = list.parallelStream().mapToInt(Integer::parseInt).sum();

C. 사용 시 주의사항

  • 스레드 안정성: 병렬 스트림에서는 여러 스레드가 데이터에 동시에 액세스할 수 있으므로, 연산이 스레드 안전해야 한다. 특히, 상태를 공유하는 객체에 대한 접근은 동기화되거나 스레드 안전한 방법으로 처리되어야 한다.

  • 연산의 병렬화 적합성: 모든 작업이 병렬 처리에 적합한 것은 아니다. 작업이 CPU 바운드이고, 데이터가 충분히 크며, 작업 간에 의존성이 없는 경우에 병렬 스트림이 성능 향상을 가져올 수 있다.

  • 오버헤드 고려: 작업을 분할하고, 스레드에 할당하며, 결과를 결합하는 데는 오버헤드가 발생한다. 데이터 크기가 작거나 연산이 간단한 경우에는 병렬 스트림을 사용하는 것이 오히려 성능을 저하시킬 수 있다.

  • 순서 보장: 병렬 스트림에서는 요소 처리 순서가 보장되지 않는다. 순서가 중요한 작업에는 병렬 스트림의 사용을 신중히 고려해야 한다.

  • 컬렉션 선택: 데이터 소스의 종류에 따라 병렬 처리의 효율성이 달라질 수 있다. 예를 들어, ArrayList는 접근이 빠르므로 병렬 처리에 적합하지만, LinkedList는 요소 접근 시간이 길어 병렬 처리에는 적합하지 않다.

  • 직접 측정: 순차 스트림을 병렬 스트림으로 쉽게 바꿀 수 있지만, 무조건 병렬 스트림으로 바꾸는 것이 항상 좋은 선택은 아니다. 병렬 스트림과 순차 스트림 중 어느 것이 더 나은 성능을 제공하는지 확신이 서지 않는다면, 적절한 벤치마크를 사용하여 직접 성능을 측정하는 것이 바람직하다.

  • 박싱 주의: 자동 박싱과 언박싱은 성능을 크게 저하시킬 수 있다. Java 8은 박싱 동작을 피할 수 있도록 기본형 특화 스트림(IntStream, LongStream, DoubleStream)을 제공하므로, 가능한 한 이러한 스트림을 사용하는 것이 좋다.

  • 최종 연산의 병합 과정 비용 고려: 최종 연산에서 여러 서브스트림의 결과를 병합하는 과정, 예를 들어 Collectorcombiner 메서드에 의한 비용이 높다면, 병렬 스트림으로 얻은 성능 이익이 이 병합 과정에서 상쇄될 수 있다. 병렬 스트림을 사용하기 전에는 이러한 비용도 고려해야 한다.

ArrayList, IntStream.range => 분해성 ‘훌륭함’ HashSet, TreeSet => 분해성 ‘좋음’ LinkedList, Stream.iterate => 분해성 ‘나쁨’

4. Reference

  • “모던 자바 인 액션” (저자: 라울-게이브리얼 우르마, 마리오 푸스코, 앨런 마이크로프트)

태그:

카테고리:

업데이트:

댓글남기기