glenn-syj / more-effective-java

이펙티브 자바를 읽으며 자바를 더 효율적으로 공부합니다
4 stars 5 forks source link

[MEJ-010] parallel 메서드와 parallelStream 메서드의 차이 및 병렬 처리의 성능에 관하여 #184

Closed undeadtimo closed 3 months ago

undeadtimo commented 4 months ago

Based on : #182 by @yngbao97


저는 지금까지 Steam이라는 단어는 많이 보았지만, 그 개념에 대한 지식이 부족하여 코드를 이해하거나 다른 연관된 개념을 이해하는데 문제가 있었습니다.

그러나 Stream API에 대하여 정리해주셔서 Stream에 대한 개념을 확실히 잡을 수 있게 되었네요. 감사함을 전합니다.

특히 지연 평가에 대해 설명하는 부분에서 물이 흐르는 형태로 비유하여 지연 평가가 아닌 Stream의 데이터 처리 과정과 지연 평가인 Stream의 데이터 처리 과정을 쉽게 이해할 수 있었습니다.

마지막 병렬 처리 부분을 읽으면서 두 가지 의문이 생겼기에 제 나름 조사한 후 정리해보았습니다.

  1. parallel()과 parallelStream() 모두 병렬처리를 구현한다면 왜 메서드가 두 개나 존재하는 것일까? 두 메서드에 대한 차이가 존재하는 것일까?

  2. 경우에 따라서는 병렬(parallel) 처리가 순차(sequence) 처리보다 느릴 수 있다는데, 그것은 어떤 경우를 말하는 것일까?


먼저 첫 번째 의문에 대해 조사한 것을 정리해보겠습니다.

parallel() 메서드는 Java 8 버전에 등장한 BaseStream의 것이며, parallelStream() 메서드는 Collection 인터페이스의 것입니다.

먼저 parallelStream 메서드는 Spliterator 인터페이스를 통해 병렬 Stream을 생성합니다.

Spilterator 객체는 trySplit() 메서드를 통해서 병렬처리가 가능하도록 데이터를 분할합니다.


다음으로 parallel() 메서드는 병렬 Stream을 생성하는 pararllelStream과 다르게 이미 만들어져있는 순차 Stream을 병렬 Stream으로 변환하는 기능을 수행합니다.

두 메서드는 각각 다음과 같이 사용할 수 있습니다.

public class Book {
    private String name;
    private String author;
    private int yearPublished;

    // getters and setters
}

위와 같은 클래스가 있다고 가정할 때,

Collection<Book> EffectiveJava = new ArrayList<Book>();

Collection<Book> SearchForMeaning = new ArrayList<Book>();

int year = 2024;

/*
 EffectiveJava와 SearchForMeaning에 각각 Book 객체를 추가한다고 가정한다.
*/

// parallelStream()
EffectiveJava.parallelStream().forEach(book -> {
          if (book.getYearPublished() == year) {
              System.out.println(book.getName + "은(는) " + year + "해에 출판되었습니다.");
          }
      });

// parallel()
SearchForMeaning.stream().parallel().forEach(book -> {
          if (book.getYearPublished() == year) {
              System.out.println(book.getName + "은(는) " + year + "해에 출판되었습니다.");
          }
      });

위처럼 parallelStream() 메서드와 parallel 메서드를 사용할 수 있습니다.

위 경우, 만약 EffectiveJava와 SearchForMeaning에 같은 Book 객체들이 같은 순서로 들어가있다면 두 메서드는 같은 결과를 출력할 것입니다.

다만, parallelStream 메서드를 사용할 때 호출되는 Spliterator 인터페이스의 trySplit 메서드는 다뤄야 할 데이터 소스가 분할할 수 없을 정도로 작다고 판단하면 병렬 스트림 대신 순차 스트림을 반환합니다.

반면 parallel 메서드는 항상 병렬 스트림만을 반환하기에 이 부분에서 차이점을 갖는 것으로 확인할 수 있습니다.


다음으로 두 번째 의문인, 병렬 처리가 순차 처리보다 느린 경우에 대해 정리해보겠습니다.

  1. 스트림의 크기

    • 스트림의 크기가 충분히 크지 않으면 병렬 처리를 위한 초기 비용이 순차 처리를 수행하는 작업 비용보다 커지게 됩니다.
  2. 연산 집약

    • 만약 스트림의 작업이 큰 연산 작업을 요구하고 있다면, 스트림의 크기가 작더라도 병렬 처리가 순차 처리보다 더 좋은 성능을 보여줍니다.
  3. 쉽게 분할 가능한지

    • 스트림을 병렬 처리하기 위해 분할하는 비용이 크다면 순차 처리가 더 효과적입니다. 예를 들어, ArrayList, HashMap 또는 단순 배열과 같은 컬렉션은 효율적으로 분할할 수 있지만, LinkedList 또는 Input/Output 기반 데이터 소스는 분할이 비효율적입니다.

References:

[parallel 메서드와 parallelStream 메서드의 차이에 대한 글] https://www.baeldung.com/java-parallelstream-vs-stream-parallel

[병렬 처리와 순차 처리의 성능에 관한 글] https://blogs.oracle.com/javamagazine/post/java-parallel-streams-performance-benchmark

yngbao97 commented 4 months ago

parallel 메서드와 parallelStream 메서드가 단순히 스트림을 생성함과 동시에 병렬구조로 변환하는가, 이미 만들어진 스트림을 병렬구조로 변환하는가 순서의 차이로만 생각하고 있었는데요. 보다 구체적으로 조사하고 공유해주신 덕분에 예외적인 상황에 있어 반환 자료에 차이가 생길 수 있다는 것을 알게 되었습니다.

또, 병렬 처리의 비효율 가능성에 대해서도 스트림을 초기 비용에 대한 막연한 추측만 가지고 지나쳤던 부분이었는데, 연산 작업의 크기와 스트림의 크기 관계에 따라서도 차이가 생길 수 있다는 점을 탐구해주셔서 보다 확실히 이해할 수 있게 되었네요!

추가적인 탐구과 정리 감사합니다!