java

Java 병렬처리 가이드: Thread, Executor, ForkJoinPool, Parallel Stream

개발에대해 2025. 10. 2. 09:00
반응형

 

Java 병렬처리 가이드: Thread, Executor, ForkJoinPool, Parallel Stream

오늘은 많은 개발자들이 헷갈려 하는 Java 병렬처리(Parallel Processing)에 대해 정리해보겠습니다.

병렬처리는 CPU의 멀티코어를 최대한 활용해서 대규모 데이터 처리고성능 연산을 빠르게 끝내는 핵심 기술이에요.

특히 최근 서버 애플리케이션이나 빅데이터 처리에서 빠질 수 없는 개념이죠.

 

1. 병렬처리와 동시성의 차이

먼저 개념부터 짚고 가야 해요.

  • 동시성(Concurrency): 여러 작업이 번갈아 가며 실행되는 것 (싱글코어에서도 가능)
  • 병렬성(Parallelism): 여러 작업이 실제로 동시에 실행되는 것 (멀티코어 필요)

Java에서는 Thread, ExecutorService, ForkJoinPool, 그리고 Parallel Stream 같은 기능을 활용해 병렬처리를 구현할 수 있습니다.

 

2. Thread 직접 사용하기

가장 기본적인 병렬처리 방식은 Thread를 직접 만드는 거예요.

하지만 스레드를 직접 관리하면 코드가 지저분해지고, 자원 관리가 어렵습니다.


public class ThreadExample {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            System.out.println("작업 1 실행: " + Thread.currentThread().getName());
        });
        Thread t2 = new Thread(() -> {
            System.out.println("작업 2 실행: " + Thread.currentThread().getName());
        });
        t1.start();
        t2.start();
    }
}

 

3. ExecutorService — 스레드 풀 관리

실제 현업에서는 ExecutorService를 더 많이 사용합니다.

스레드 풀(Thread Pool)을 운영하면서 필요한 만큼 스레드를 재사용하기 때문에 성능과 안정성이 좋아요.


import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ExecutorExample {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(3);

        for (int i = 0; i < 5; i++) {
            int taskId = i;
            executor.submit(() -> {
                System.out.println("작업 " + taskId + " 실행: " + Thread.currentThread().getName());
            });
        }
        executor.shutdown();
    }
}

 

4. ForkJoinPool — 분할 정복 병렬 처리

ForkJoinPool은 큰 작업을 작은 단위로 나누어 병렬로 처리하고, 다시 합치는 방식(분할 정복, Divide & Conquer)을 사용합니다. 대규모 데이터 연산에서 강력한 성능을 발휘합니다.


import java.util.concurrent.RecursiveTask;
import java.util.concurrent.ForkJoinPool;

public class ForkJoinSum extends RecursiveTask {
    private final long[] numbers;
    private final int start;
    private final int end;
    private static final int THRESHOLD = 10_000;

    public ForkJoinSum(long[] numbers, int start, int end) {
        this.numbers = numbers;
        this.start = start;
        this.end = end;
    }

    @Override
    protected Long compute() {
        int length = end - start;
        if (length <= THRESHOLD) {
            long sum = 0;
            for (int i = start; i < end; i++) {
                sum += numbers[i];
            }
            return sum;
        }
        int mid = start + length / 2;
        ForkJoinSum left = new ForkJoinSum(numbers, start, mid);
        ForkJoinSum right = new ForkJoinSum(numbers, mid, end);
        left.fork();
        return right.compute() + left.join();
    }

    public static void main(String[] args) {
        long[] numbers = new long[10_000_000];
        for (int i = 0; i < numbers.length; i++) {
            numbers[i] = i;
        }
        ForkJoinPool pool = new ForkJoinPool();
        long result = pool.invoke(new ForkJoinSum(numbers, 0, numbers.length));
        System.out.println("합계: " + result);
    }
}

 

5. Parallel Stream — 가장 간단한 병렬 처리

Java 8부터 도입된 Parallel Stream은 가장 간단하게 병렬처리를 적용하는 방법이에요.

다만 무작정 쓰면 오히려 성능이 떨어질 수 있으니, CPU 바운드 연산에 적합합니다.


import java.util.stream.LongStream;

public class ParallelStreamExample {
    public static void main(String[] args) {
        long sum = LongStream.rangeClosed(1, 1_000_000)
                             .parallel()
                             .sum();
        System.out.println("합계: " + sum);
    }
}

마무리

오늘은 Java 병렬처리의 다양한 방식들을 살펴봤습니다.

- Thread는 기본 개념 학습용

- ExecutorService는 실무에서 가장 많이 사용

- ForkJoinPool은 대규모 연산에 적합

- Parallel Stream은 간단하지만 상황에 따라 주의 필요 병렬처리는 잘만 쓰면 성능 향상에 엄청난 도움이 되지만, 잘못 쓰면 동기화 문제나 오히려 성능 저하를 불러올 수도 있어요. 따라서 항상 테스트와 튜닝을 병행하는 습관을 가지는 것이 중요합니다.

반응형