단일 스트림

병렬 스트림을 이해하기 위해 먼저 단일 스트림과 직접 스레드를 제어해 보는 예제를 알아보자.

로거 클래스 - 어떤 스레드에서 작업이 실행되는지 로그로 출력

import java.time.LocalTime;
import java.time.format.DateTimeFormatter;

public class MyLogger {
    private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss.SSS");

    public static void log(Object obj) {
        String time = LocalTime.now().format(FORMATTER);
        System.out.printf("%s [%9s] %s\n", time, Thread.currentThread().getName(), obj);
    }
}

각 작업당 1초 정도 소요되는 작업을 수행하는 HeavyJob 클래스

import util.MyLogger;

public class HeavyJob {

    public static int heavyTask(int n) {
        MyLogger.log("calculate " + n + " -> " + (n * 10));
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        return n * 10;
    }

    public static int heavyTask(int n, String name) {
        MyLogger.log("[" + name + "] " + n + " -> " + (n * 10));
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        return n * 10;
    }
}

V1 - 단일 스트림

  • IntStream.rangeClosed(1, 8)을 사용해 1부터 8까지의 숫자 각각에 대해 heavyTask()를 순서대로 수행한다.

  • 1초씩 걸리는 작업을 8번 순차로 호출하므로 약 8초가 소요된다.

  • 또한 단일 스레드(main 스레드)에서 작업을 순차적으로 수행하기 때문에 로그에도 main 스레드만 표시된다.

V2 - 스레드 직접 사용

  • V1에서는 메인 스레드로 1에서 8의 범위를 모두 계산했다.

  • 각 스레드는 한번에 하나의 작업만 처리할 수 있다.

  • 스레드를 사용해서 1에서 8을 처리하는 큰 단위의 작업을 더 작은 단위의 작업으로 분할하도록 해보자.

  • 두 개의 스레드(thread-1, thread-2)가 작업을 분할해서 처리하기 때문에 기존에 8초가 걸리던 작업을 4초로 줄일 수 있었다.

  • 하지만 이렇게 스레드를 직접 사용하면 스레드 수가 늘어나면 코드도 복잡해지고, 예외 처리, 스레드 풀 관리 등 추가로 관리해야 할 부분들이 생기는 문제가 있다.

V3 - 스레드 풀 사용

이번에는 자바가 제공하는 ExecutorService를 사용해서 더 편리하게 병렬 처리를 해보자.

  • V2와 마찬가지로 2개의 스레드가 병렬로 계산을 처리하므로 약 4초가 소요된다.

  • Future로 반환값을 쉽게 받아올 수 있기 때문에 결과를 합산하는 과정이 더 편리해졌다.

  • 하지만 여전히 코드 레벨에서 분할 및 병합 로직을 직접 짜야 하고, 스레드 풀 생성과 관리도 개발자가 직접 해야 하는 문제가 있다.


Fork/Join 패턴

  • 스레드는 한번에 하나의 작업만 처리할 수 있다. 따라서 하나의 큰 작업을 여러 스레드가 처리할 수 있는 작은 단위의 작업으로 분할(Fork) 해야 한다.

  • 그리고 이렇게 분할한 작업을 각각의 스레드가 처리(Execute) 하고, 각 스레드의 분할된 작업 처리가 끝나면 분할된 결과를 하나로 모아야(Join) 한다.

  • 이렇게 분할(Fork) → 처리(Execute) → 모음(Join) 의 단계로 이루어진 멀티스레딩 패턴을 Fork/Join 패턴이라고 한다.

  • 이 패턴은 병렬 프로그래밍에서 매우 효율적인 방식으로, 복잡한 작업을 병렬적으로 처리할 수 있게 해준다.

img.png

자바는 Fork/Join 프레임워크를 제공하여 개발자가 이와 같은 패턴을 더 쉽게 구현할 수 있도록 지원한다.

Last updated