자바(SE)

[Stream] 스트림의 병렬 처리

은서파 2024. 7. 8. 18:06

이번 포스트에서는 스트림의 병렬 처리에 대해 살펴보자.

 

병렬 처리

 

동시성(Concurrency)와  병렬성(Parallelism)

스레드를 사용하다 보면 은근 헷갈리게 사용하는 용어가 동시성과 병렬성이라는 개념인데 둘의 목적이 살짝 다르다.

동시성여러 작업들이 동시에 실행되는 것처럼 보이도록 하는 개념이다. 한 사람이 요리를 하면서 동시에 라디오도 듣고, 전화 통화도 한다. 사실 여러 작업을 동시에 하는 것 처럼 보이지만 매우 빠르게 작업을 전환하면서 진행하는 것이다. 동시성의 목적은 시스템의 자원을 효율적으로 사용하기 위한 것으로 일반적으로 Thread를 만들어서 동작 시키는 것은 동시성을 위한 행위이다.

반면 병렬성실제로 여러 작업들이 동시에 진행되는 것이다. 어떤 사람은 요리를 하고 어떤 사람은 라디오를 듣고 또 어떤 사람은 전화 통화를 한다. 병렬성의 목적은 여러 작업이 실제로 동시에 처리해서 작업 시간을 줄이는데 있다. 병렬성을 구현하기 위해는 Thread와 더불어 여러개의 CPU 코어가 필요하다.

 

병렬성의 종류

병렬성에는 크게 데이터 병렬성과 작업 병렬성이 있다.

데이터 병렬성이란 전체 데이터를 쪼개서 서브 데이터로 만들고 각각의 서브데이터를 여러개의 스레드에서 병렬로 처리하게 해서 작업을 빨리 끝내는 것을 말한다. parallelStream()을 이용해서 스트림을 구성하는 경우가 데이터 병렬성에 해당한다.

작업 병렬성이란 서로 다른 작업의 덩어리를 여러개로 쪼개서 병렬로 수행하는 방식이다. 이때 나눠진 각 작업은 다른 작업과는 독립적으로 수행되며 여러 프로세서나 코어가 동시에 처리한다. 이것의 대표적인 예로는 웹 서버를 들 수 있는데 각 클라이언트의 요청은 독립적인 작업으로 간주되서 병렬로 처리된다.

 

parallelStream을 이용한 데이터 병렬성

 

포크조인을 통한 병렬성 동작

이런 병렬성은 분할 정복을 이용하는 포크조인(Fork/Join) 프레임워크 기반으로 동작한다.

  • Fork: 큰 데이터를 여러 개의 작은 데이터로 나누는 과정
  • Join: Fork에 의해 분리된 데이터들이 병렬로 처리된 후 다시 합쳐지는 과정

 

[출처: https://en.wikipedia.org/wiki/File:Fork_join.svg]

자바의 Stream API에서 제공되는 parallelStream()은 위의 방식을 이용해서 큰 데이터를 쪼개서 처리하고 다시 합치는 과정을 거친다.

parallelStream 의 성능

그럼 parallelStream()을 이용해서 병렬 스트림을 사용하면 순차 스트림을 사용하는 것에 비해 빠른 성능을 보일까? 원래의 목적이 빠른 처리니까 당연해야 할것 처럼 보인다. 

당연히 아니다. 목적이 그렇다는 것이지. 얼른 봐도 병렬 스트림을 사용하면 fork 과정과 join 과정, 그리고 중간에 처리할 thread 생성 등 오버헤드가 발생할 수 있다. 만약 이런 비용이 순차 스트림에서의 비용을 초과한다면 오히려 느려질 것이다.

일반적으로 병렬스트림을 써야할지 고민할 때는 다음의 요소들을 생각해보자.

  • 데이터 수와 데이터 처리 시간: 데이터의 수가 적고 데이터를 처리하는데 걸리는 시간이 짧으면 오히려 순차 처리가 병렬 처리보다 유리할 수 있다.
  • 스트림 소스의 종류: ArrayList나 배열등은 인덱스로 요소를 관리하므로 fork 단계에서 쉽게 분리가 가능하고 성능도 좋다. 반면 Set이나 LinkedList등은 요소를 추적하기가 쉽지 않아 분리가 어렵고 상대적으로 성능이 좋지 않다.
  • 코어의 수: 일반적으로 코어의 수가 많으면 병렬 스트림이 빠를 수 있다.

어떻든 위의 항목들은 일반적이다. 중요한 점은 "병렬 스트림을 사용하면 성능이 언제나 좋아질 것이다"라는 환상에서 벗어나서 꼭 진행중인 시스템에서 충분한 테스트를 거친 후에 사용하는 것이 권장된다.