포크/조인 프레임워크 개념
(Java 7 이후부터)포크/조인 프레임워크는 java.util.concurrent 패키지에 속하며, 분할 정복 알고리즘을 기반으로 대규모 작업을 효율적으로 병렬 처리하여 멀티 코어 시스템의 성능을 극대화하는 것을 목표로 한다. 프레임워크는 작업을 작은 단위로 재귀적으로 분할하고, 그 서브 작업들의 결과를 합치는 방식으로 전체 결과를 도출한다.
-
포크(Fork): 처리할 큰 작업을 CPU가 처리하기에 충분히 작고 독립적인 서브 작업으로 반복해서 나눈다. 작업 크기가 미리 정의된 임계값 이하로 작아지면, 더 이상 분할하지 않고 해당 작업을 순차적으로 처리한다.
-
조인(Join): 분할된 모든 서브 작업들이 완룓될 때까지 기다렸다가, 각각의 결과를 모아 최종 결과로 통합한다.
포크/조인 태스크
포크/조인 프레임워크를 사용하려면 ForkJoinTask 안의 두 가지 주요 서브 클래스 중 하나를 구현해야 한다. 두 클래스 모두 compute() 추상 메소드를 구현하여 작업 논리를 정의한다.
| 클래스 | 역할 | 메소드 구조 |
|---|---|---|
| RecursiveAction | 결과를 반환하지 않는 void 타입의 작업 | protected void compute() |
RecursiveTask<R> |
결과를 반환하는 작업(R 인자는 결과 형식이다.) | protected abstract R Compute() |
대부분의 compute() 메소드는 다음과 같은 형식을 유지한다.
if (length <= THRESHOLD) {
return computeSequentially(); // 순차적 처리
}
else {
// 태스크 분할(Fork)
SubTask leftTask = new SubTask(...);
leftTask.fork(); // 비동기 실행 요청
// 오른쪽 태스크 처리(재귀 호출)
RightResult rightResult = rightTask.compute();
// 왼쪽 태스크 결과 대기 및 결합(Join)
LeftResult leftResult = leftTask.join();
return leftResult + rightResult;
}
- 주의 사항: 두 서브 태스크 중 하나는 compute() 메소드를 직접 재귀 호출하고, 다른 하나에만 fork() 메소드를 호출하여 같은 스레드를 재사용하는 것이 효율적이다. 두 작업 모두에 fork() 메소드를 호출하면 불필요한 태스크 할당 오버헤드가 발생할 수 있다. 또한, join() 메소드는 반드시 두 작업이 모두 시작된 후에 호출해야 한다.
작업 훔치기 알고리즘
포크/조인 풀은 ExecutorService 구현체로, Fork/Join 작업을 실행하는 워커 스레드를 관리하는 스레드 풀이며, 핵심은 작업 부하 균형을 맞추는 작업 훔치기(Work-Stealing) 알고리즘이다.
-
분산 태스크 관리: 풀 내의 각 워커 스레드는 작업을 저장하는 자신만의 로컬 큐를 가진다. 스레드는 일반적으로 자신의 큐 헤더(Header)에서 작업을 가져온다.
-
작업 훔치기: 한 스레드가 자신이 큐가 비어 유휴 상태가 되면, 다른 바쁜 스레드의 큐 꼬리(Tail)에서 작업을 훔쳐와 처리한다.
-
효과: 모든 CPU 코어가 지속적으로 작업을 처리하도록 보장하여 스레드 활용도를 높이고 대기 시간을 최소화한다.
공용 풀
공용 풀(Common Fork/Join Pool)은 JVM 기본 풀로 애플리케이션 실행 시 자동으로 생성하고 관리하는 단 하나의 공유 인스턴스이다. (Java 8 이후부터)병렬 스트림과 명시적 Executor 키워드를 지정하지 않은 CompletableFuture 작업의 기본 실행 풀로 사용되며, 기본적으로 시스템의 사용 가능한 모든 프로세서 수를 기준으로 스레드 수가 결정된다.
ForkJoinPool vs ExecutorService
포크/조인 풀과 ExecutorService 구현체의 특징은 다음과 같다.
| 특징 | ForkJoinPool | ExecutorService(ex. ThreadPoolExecutor) |
|---|---|---|
| 목적 | 재귀적 분할 정복 및 계산 집약적 작업에 최적화 | 단순 독립 작업 및 I/O 집중적 작업에 일반적 |
| 부하 분산 | Work-Stealing 알고리즘을 사용, 동적이고 효율적인 부하 균형 | 정적인 작업 할당, 중앙 큐 사용 |
| I/O 작업 | Blocking 시 풀 전체 성능 저하를 야기할 수 있어 비효율적이다. | 스레드 수를 I/O 대기 시간에 맞게 조정하여 사용 가능하다. |
- 결론: 포크/조인 프레임워크는 복잡하고 계산 집약적 알고리즘에 활용할 수 있으나, 디스크 I/O나 네트워크 요청이 주를 이루는 작업에는 일반 스레드 풀을 사용하는 것이 적절하다.