모던자바인액션 - 7

이건희·2023년 7월 23일
1

모던자바인액션

목록 보기
7/9

이번 챕터는 Java 8 관점에서 바라본 병렬성에 관한 내용이다. 사실 아직 병렬성이 필요한 코딩을 해보지 않았고, 어떠한 상황에서 쓰이는지 잘 몰라 완전히 와 닿거나 이해되지 않은 것 같다. 그래도 이해한 부분까지 정리를 해보려고 한다. 추후에 병렬성이 필요하다고 생각될 때 다시 한번 챕터 7을 정독하는 것도 좋은 방법일 것 같다.

병렬 스트림

이전 챕터에서부터 지속적으로 스트림을 손쉽게 병렬로 처리 할 수 있다고 하였다.

병렬 스트림이란?

  • 컬렉션에 parallelStream 호출 시 병렬 스트림 생성
  • 각각의 스레드에서 처리할 수 있도록 스트림 요소를 여러 청크로 분할한 스트림
  • 모든 멀티코어 프로세서가 각각의 청크를 처리하도록 할당 가능

숫자 n을 인수로 받아서 1부터 n까지 모든 숫자의 합계를 반환하는 메서드를 구현한다고 하자. 우선적으로 아래 코드처럼 순차적으로 코드를 구성할 수 있을 것이다.

public long sequentialSum(long n) {
	return Stream.iterate(1L, i -> i + 1) //무한 스트림 생성
    			 .limit(n)
                 .reduce(0L, Long::sum); //리듀싱 연산으로 모든 숫자 더하기
}

이러한 순차 스트림에서 parallel 메서드를 호출하면 손쉽게 병렬로 처리 가능하다.

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

위 코드는 리듀싱 연산으로 모든 숫자의 합계를 더하는 것은 같지만 스트림이 여러 청크로 분할 되어 있다는 점이 다르다.

여러 청크에 병렬로 리듀싱 연산을 수행하고 생성된 부분 결과를 다시 리듀싱 연산으로 합쳐 결과를 도출할 수 있다.


실행 시간 비교

한번 이렇게 만든 코드를 비교해보자.

  1. 순차 스트림
  2. 병렬 스트림

위 예시처럼 합계를 구하는 코드에서는 순차 스트림이 병렬 스트림보다 훨씬 빨랐다.

병렬 스트림 사용 시 오히려 속도가 느려졌다. 왜 그럴까? 다양한 이유가 있을 수 있다.

  • 반복 결과로 박싱된 객체가 만들어지므로 숫자를 더하려면 언박싱을 해야한다.

  • 반복 작업은 병렬로 수행할 수 있는 독립 단위로 나누기가 어렵다.

  • 리듀싱 과정을 시작하는 시점에 전체 숫자 리스트가 준비되지 않았으므로 스트림을 병렬로 처리할 수 있도록 청크로 분할할 수 없다.

  • 합계가 각각 다른 스레드에서 진행 되었지만 순차 처리 방식과 크게 다른 점이 없으므로 스레드를 할당하는 오버헤드만 증가하였다.

여기서 중요한 점은 병렬로 처리한다고 무조건 성능이 좋아지는 것이 아니라는 점이다.

이후 위 병렬 코드를 더 특화된 메서드(LongStream.rangeClosed)를 사용하여 속도를 높일 수 있다. 이를 사용 시, 박싱과 언박싱 오버헤드가 사라지고, 쉽게 청크로 분할할 수 있는 숫자 범위를 생성(ex. 1-20 범위를 1-5,6-10,11-15 etc...)한다.

여기서 얻을 수 있는 교훈은 다음과 같다.

  1. 병렬화가 완전 공짜는 아니다.

  2. 병렬화를 이용하려면 스트림을 재귀적으로 분리하고, 각 서브 스트림을 서로 다른 스레드의 리듀싱 연산으로 할당하고, 이들을 하나의 값으로 합쳐야 한다.

  3. 멀티코어간 데이터 이동은 생각보다 비싸다.

  4. 코어 간 데이터 전송 시간보다 훨씬 오래 걸리는 작업만 병렬로 다른 코어에서 수행하는 것이 바람직하다.

  5. 어떤 알고리즘을 병렬화 하는 것보다 적절한 자료구조를 선택하는 것이 중요하다.

그러면 어떠한 상황에서 병렬화를 사용해야 할까?


병렬 스트림의 올바른 사용법

병렬 스트림에서 많은 문제는 공유된 상태를 바꾸는 알고리즘을 사용하기 때문에 일어난다.

공유된 상태를 바꾸는 알고리즘 사용 시, 속도는 고사하고 원치 않은 결과를 낼 수 있다. 그러니 다른 것들 보다도 최우선 적으로 공유된 상태를 바꾸는 알고리즘 사용 시, 병렬 스트림의 사용은 피하자.

책에서는 어떠한 상황에서 병렬 스트림을 사용하는 것이 좋은지 여러가지 제안을 제시한다.

  • 무조건 병렬 스트림을 사용하는 것이 좋은게 아니므로 확신이 서지 않으면 직접 측정하자

  • 박싱을 주의하자. 요소의 순서에 의존하는 연산을 병렬 스트림으로 수행 시, 속도가 오히려 느려질 수 있다.

  • 스트림에서 수행하는 전체 파이프라인 연산 비용을 고려하자. 연산이 복잡하고 시간이 오래 걸릴 때 병렬 스트림 사용을 고려해보자.

  • 소량에 데이터에선 병렬 스트림이 도움 되지 않는다.

  • 스트림을 구성하는 자료구조가 적절한지 확인하자. 예를 들어, LinkedList에서는 분할하려면 모든 요소를 탐색해야 하지만 ArrayList 사용 시 요소를 탐색하지 않고도 리스트를 분할할 수 있다.

  • 스트림의 특성과 파이프라인의 중간 연산이 스트림의 특성을 어떻게 바꾸는지에 따라 성능이 달라질 수 있다. 예를 들어 SIZED 스트림은 정확히 같은 크기로 스트림을 분해 가능해 효과적으로 병렬 처리를 할 수 있지만, filter 연산이 있으면 길이를 예측할 수 없으므로 병렬 처리가 효과적인지 알 수 없다.

  • 최종 연산의 병합 과정 비용을 살펴보자. 병합 과정의 비용이 비싸다면 병렬 스트림으로 얻은 성능 이익이 서브스트림의 부분 결과를 합치는 과정에서 상쇄될 수 있다.


이후 책에서는 포크/조인 프레임워크와 Spliterator를 설명한다. 하지만 아직 내 수준에서 이를 완벽히 이해하기는 힘들다고 판단하였다. 3번정도 정독하였지만 완전히 이해가 가지 않아 이를 정리하기는 힘들다고 생각하여 이는 이후 실력이 더 늘었을때 정리하는게 맞다고 판단하였다.

profile
백엔드 개발자가 되겠어요

2개의 댓글

comment-user-thumbnail
2023년 7월 23일

잘 봤습니다. 좋은 글 감사합니다.

1개의 답글