Unit 8.3 — RecursiveTask

Psj·2026년 5월 26일

F-lab

목록 보기
154/240

Unit 8.3 — RecursiveTask

F-LAB JAVA · 4주차 · Phase 8 · 고급 비동기
🏆 Phase 8 완주 + 🎓 4주차 전체 완주 — 동시성/멀티스레딩 마스터


📌 학습 목표

이 Unit을 끝내면 다음을 답할 수 있어야 한다.

  • RecursiveTask 와 RecursiveAction 의 차이는?
  • compute() 메서드 의 구현 패턴은?
  • 임계값 (threshold) 기반 분할 중단 은?
  • fork / compute / join 순서 의 효율은?
  • RecursiveTask 의 결과 합치기 는?
  • RecursiveAction 의 부수 효과 처리는?
  • 임계값 선택 의 트레이드오프는?
  • ForkJoin 의 적합/부적합 작업은?
  • 4주차 동시성 전체 의 종합은?

🎯 핵심 한 문장

RecursiveTask 는 결과를 반환하는 ForkJoinTask, RecursiveAction 은 결과가 없는 ForkJoinTask 이며, 둘 다 compute() 메서드에서 임계값을 기준으로 직접 처리할지 분할할지 결정한다.
RecursiveTask<V> 는 V compute() 로 결과를 반환 (합산, 검색 등), RecursiveAction 은 void compute() 로 결과 없이 부수 효과만 수행한다 (정렬, 변환 등).
compute() 의 표준 패턴은 작업 크기가 임계값 이하면 직접 처리 (정복), 초과하면 둘로 분할하여 한쪽은 fork(), 다른 쪽은 compute(), 그리고 join() 으로 결과를 합치는 것이다.
효율적인 순서는 left.fork() → right.compute() → left.join() 으로, 한쪽을 비동기로 보내고 다른 쪽을 현재 스레드가 직접 처리한 뒤 합쳐 스레드 낭비를 줄인다.
임계값은 너무 작으면 분할·관리 오버헤드가 커지고 너무 크면 병렬성이 떨어지므로, 데이터 크기와 작업 특성에 맞게 조정한다.

비유 — 도서관 장서 정리

RecursiveTask / RecursiveAction = 장서 정리:

RecursiveTask (결과 있음):
  - "책 몇 권 있는지 세어줘" (합산)
  - 결과 보고 (숫자)

RecursiveAction (결과 없음):
  - "책 정리해줘" (정렬)
  - 결과 보고 X (정리만)

compute() 패턴:
  - 책장 1개? → 직접 (임계값)
  - 책장 많음? → 반으로 나눔 (분할)
    - 왼쪽: 다른 사서에게 (fork)
    - 오른쪽: 내가 직접 (compute)
    - 왼쪽 결과 받기 (join)
    - 합침

임계값:
  - 너무 작으면 (책장 1칸씩) → 나누느라 시간 낭비
  - 너무 크면 (절반씩만) → 협력 안 됨
  - 적당히 (책장 단위)

→ RecursiveTask (결과) / RecursiveAction (결과 X), compute (임계값 분할), fork/compute/join.


🧭 9개 섹션 로드맵

1. RecursiveTask vs RecursiveAction
2. compute() 구현 패턴
3. 임계값 기반 분할
4. fork / compute / join 순서
5. 결과 합치기 (RecursiveTask)
6. 부수 효과 (RecursiveAction)
7. 임계값 선택
8. Phase 8 완주 + 4주차 종합
9. 면접 + 자기 점검 + Phase 8 졸업 시험 + 4주차 완주

1️⃣ RecursiveTask vs RecursiveAction

1.1 두 클래스

ForkJoinTask 하위:

RecursiveTask<V>:
  - V compute()
  - 결과 반환

RecursiveAction:
  - void compute()
  - 결과 없음

1.2 RecursiveTask

// RecursiveTask — 결과 있음
class SumTask extends RecursiveTask<Long> {
    @Override
    protected Long compute() {
        // 결과 반환
        return sum;
    }
}

1.3 RecursiveAction

// RecursiveAction — 결과 없음
class SortAction extends RecursiveAction {
    @Override
    protected void compute() {
        // 부수 효과 (정렬, 변환)
        // 반환 X
    }
}

1.4 선택

선택:

RecursiveTask:
  - 합산, 카운트
  - 검색 결과
  - 변환 후 결과

RecursiveAction:
  - 정렬 (제자리)
  - 배열 변환 (제자리)
  - 부수 효과

1.5 실행

ForkJoinPool pool = new ForkJoinPool();

// RecursiveTask — 결과
Long result = pool.invoke(new SumTask(...));

// RecursiveAction — 결과 없음
pool.invoke(new SortAction(...));   // 반환 없음

1.6 ILIC 의 맥락

@Service
public class RecursiveTaskVsAction {
    
    private final ForkJoinPool pool = new ForkJoinPool();
    
    // RecursiveTask — 운임 합산 (결과)
    public BigDecimal sumFreight(List<Shipment> shipments) {
        return pool.invoke(new FreightSumTask(shipments, 0, shipments.size()));
    }
    
    // RecursiveAction — 배송 일괄 처리 (부수 효과)
    public void processAll(List<Shipment> shipments) {
        pool.invoke(new ProcessAction(shipments, 0, shipments.size()));
        // 결과 없음 (처리만)
    }
    
    static class FreightSumTask extends RecursiveTask<BigDecimal> {
        // ... compute 가 BigDecimal 반환
        protected BigDecimal compute() { return BigDecimal.ZERO; }
    }
    
    static class ProcessAction extends RecursiveAction {
        // ... compute 가 void
        protected void compute() { }
    }
}

1.7 자기 점검 답변

RecursiveTask와 RecursiveAction의 차이는?

답:
1. RecursiveTask:

  • V compute()
  • 결과 반환
  1. RecursiveAction:

    • void compute()
    • 결과 없음
  2. 선택:

    • Task: 합산, 검색
    • Action: 정렬, 변환
  3. 공통:

    • ForkJoinTask

2️⃣ compute() 구현 패턴

2.1 표준 패턴

protected V compute() {
    // 1. 임계값 확인
    if (작업 크기 <= THRESHOLD) {
        return 직접_처리();   // 정복
    }
    // 2. 분할
    Task left = new Task(왼쪽);
    Task right = new Task(오른쪽);
    // 3. fork / compute / join
    left.fork();
    V rightResult = right.compute();
    V leftResult = left.join();
    // 4. 결합
    return combine(leftResult, rightResult);
}

2.2 단계

compute 단계:

1. 임계값 확인
   - 작으면 직접 처리

2. 분할
   - 둘로 나눔

3. fork/compute/join
   - 비동기 + 직접 + 대기

4. 결합
   - 결과 합침

2.3 RecursiveTask 예시

class SumTask extends RecursiveTask<Long> {
    private final long[] array;
    private final int start, end;
    private static final int THRESHOLD = 10000;
    
    SumTask(long[] array, int start, int end) {
        this.array = array;
        this.start = start;
        this.end = end;
    }
    
    @Override
    protected Long compute() {
        if (end - start <= THRESHOLD) {
            // 직접 합산
            long sum = 0;
            for (int i = start; i < end; i++) {
                sum += array[i];
            }
            return sum;
        }
        // 분할
        int mid = (start + end) / 2;
        SumTask left = new SumTask(array, start, mid);
        SumTask right = new SumTask(array, mid, end);
        left.fork();
        long rightSum = right.compute();
        long leftSum = left.join();
        return leftSum + rightSum;   // 결합
    }
}

2.4 RecursiveAction 예시

class IncrementAction extends RecursiveAction {
    private final int[] array;
    private final int start, end;
    private static final int THRESHOLD = 10000;
    
    IncrementAction(int[] array, int start, int end) {
        this.array = array;
        this.start = start;
        this.end = end;
    }
    
    @Override
    protected void compute() {
        if (end - start <= THRESHOLD) {
            // 직접 처리
            for (int i = start; i < end; i++) {
                array[i]++;
            }
            return;
        }
        // 분할
        int mid = (start + end) / 2;
        IncrementAction left = new IncrementAction(array, start, mid);
        IncrementAction right = new IncrementAction(array, mid, end);
        invokeAll(left, right);   // 둘 다 (편의)
    }
}

2.5 invokeAll 편의

// invokeAll — fork/join 자동
invokeAll(left, right);
// 또는
invokeAll(List.of(t1, t2, t3));

// 내부적으로 fork + join
// RecursiveAction 에 편리

2.6 ILIC 의 맥락

@Service
public class ComputePattern {
    
    private final ForkJoinPool pool = new ForkJoinPool();
    
    // 표준 패턴
    static class ValidationTask extends RecursiveTask<Integer> {
        private final List<Shipment> shipments;
        private final int start, end;
        private static final int THRESHOLD = 1000;
        
        ValidationTask(List<Shipment> shipments, int start, int end) {
            this.shipments = shipments;
            this.start = start;
            this.end = end;
        }
        
        @Override
        protected Integer compute() {
            // 1. 임계값
            if (end - start <= THRESHOLD) {
                int valid = 0;
                for (int i = start; i < end; i++) {
                    if (isValid(shipments.get(i))) valid++;
                }
                return valid;
            }
            // 2. 분할
            int mid = (start + end) / 2;
            ValidationTask left = new ValidationTask(shipments, start, mid);
            ValidationTask right = new ValidationTask(shipments, mid, end);
            // 3. fork/compute/join
            left.fork();
            int rightValid = right.compute();
            int leftValid = left.join();
            // 4. 결합
            return leftValid + rightValid;
        }
        
        private boolean isValid(Shipment s) { return s.getWeight() != null; }
    }
}

2.7 자기 점검 답변

compute() 메서드의 구현 패턴은?

답:
1. 패턴:

  • 임계값 → 직접/분할
  • fork/compute/join
  • 결합
  1. RecursiveTask:

    • 결과 반환
  2. RecursiveAction:

    • invokeAll 편의
  3. 단계:

    • 확인 → 분할 → 실행 → 결합

3️⃣ 임계값 기반 분할

3.1 임계값

임계값 (Threshold):

  분할을 멈추고 직접 처리하는 기준.
  - 작업 크기 <= 임계값 → 직접
  - 초과 → 분할

3.2 분할 중단

protected Long compute() {
    if (end - start <= THRESHOLD) {   // 임계값 확인
        // 분할 중단, 직접 처리
        return directCompute();
    }
    // 분할 계속
    return divide();
}

3.3 임계값의 역할

임계값 역할:

  분할 깊이 제어:
    - 작으면 → 많은 분할 (오버헤드)
    - 크면 → 적은 분할 (병렬성 ↓)

  적절한 균형:
    - 분할 오버헤드 vs 병렬성

3.4 분할 트리 깊이

임계값과 트리 깊이:

데이터 100만, 임계값 1만:
  - 100만 / 1만 = 100 리프
  - 트리 깊이 log2(100) ≈ 7

임계값 100:
  - 100만 / 100 = 1만 리프
  - 분할 오버헤드 큼

3.5 동적 임계값

// 데이터 크기 기반 동적 임계값
int threshold = Math.max(
    MIN_THRESHOLD,
    totalSize / (Runtime.getRuntime().availableProcessors() * 4)
);
// 코어 수 × 4 정도의 작업 조각

3.6 ILIC 의 맥락

@Service
public class ThresholdExample {
    
    private final ForkJoinPool pool = new ForkJoinPool();
    private final int cores = Runtime.getRuntime().availableProcessors();
    
    public BigDecimal sumFreight(List<Shipment> shipments) {
        // 동적 임계값 (코어 수 기반)
        int threshold = Math.max(1000, shipments.size() / (cores * 4));
        return pool.invoke(new SumTask(shipments, 0, shipments.size(), threshold));
    }
    
    static class SumTask extends RecursiveTask<BigDecimal> {
        private final List<Shipment> shipments;
        private final int start, end, threshold;
        
        SumTask(List<Shipment> shipments, int start, int end, int threshold) {
            this.shipments = shipments;
            this.start = start;
            this.end = end;
            this.threshold = threshold;
        }
        
        @Override
        protected BigDecimal compute() {
            if (end - start <= threshold) {   // 동적 임계값
                BigDecimal sum = BigDecimal.ZERO;
                for (int i = start; i < end; i++) {
                    sum = sum.add(shipments.get(i).getWeight());
                }
                return sum;
            }
            int mid = (start + end) / 2;
            SumTask left = new SumTask(shipments, start, mid, threshold);
            SumTask right = new SumTask(shipments, mid, end, threshold);
            left.fork();
            return right.compute().add(left.join());
        }
    }
}

3.7 자기 점검 답변

임계값 기반 분할 중단은?

답:
1. 임계값:

  • 분할 중단 기준
  • <= 임계값 → 직접
  1. 역할:

    • 분할 깊이 제어
  2. 균형:

    • 오버헤드 vs 병렬성
  3. 동적:

    • 코어 수 기반

4️⃣ fork / compute / join 순서

4.1 효율적 순서

// ✓ 효율적
left.fork();                  // 1. 왼쪽 비동기
V rightResult = right.compute();   // 2. 오른쪽 직접
V leftResult = left.join();        // 3. 왼쪽 결과
return combine(leftResult, rightResult);

4.2 왜 이 순서

순서 이유:

  left.fork():
    - 왼쪽을 큐에 (다른 워커 가능)

  right.compute():
    - 오른쪽을 현재 스레드 직접
    - 노는 시간 없음

  left.join():
    - 왼쪽 완료 대기 (이미 진행 중)

→ 현재 스레드 활용

4.3 비효율 순서

// ❌ 둘 다 fork (현재 스레드 놀음)
left.fork();
right.fork();
V l = left.join();
V r = right.join();
// 현재 스레드: fork 만 하고 대기
// → 비효율

// ❌ fork 후 즉시 join (병렬 X)
left.fork();
V l = left.join();   // 즉시 대기 (병렬 안 됨)
V r = right.compute();
// → 순차나 다름없음

4.4 시각화

효율적 순서:

현재 스레드:
  left.fork() → [right.compute() 직접] → left.join()
                       ↓ (동시)
다른 워커:        [left 처리]

  → 현재 스레드 + 다른 워커 병렬
  → 효율

4.5 invokeAll 대안

// invokeAll — 자동 (편의)
invokeAll(left, right);
// 내부: 효율적 fork/join
// RecursiveAction 에 편리

// RecursiveTask 는 명시적 fork/compute/join
// (결과 합침 필요)

4.6 ILIC 의 맥락

@Service
public class ForkComputeJoinOrder {
    
    static class OptimalTask extends RecursiveTask<BigDecimal> {
        private final List<Shipment> shipments;
        private final int start, end;
        private static final int THRESHOLD = 1000;
        
        OptimalTask(List<Shipment> shipments, int start, int end) {
            this.shipments = shipments;
            this.start = start;
            this.end = end;
        }
        
        @Override
        protected BigDecimal compute() {
            if (end - start <= THRESHOLD) {
                BigDecimal sum = BigDecimal.ZERO;
                for (int i = start; i < end; i++) {
                    sum = sum.add(shipments.get(i).getWeight());
                }
                return sum;
            }
            int mid = (start + end) / 2;
            OptimalTask left = new OptimalTask(shipments, start, mid);
            OptimalTask right = new OptimalTask(shipments, mid, end);
            
            // ✓ 효율적 순서
            left.fork();                              // 왼쪽 비동기
            BigDecimal rightResult = right.compute(); // 오른쪽 직접
            BigDecimal leftResult = left.join();      // 왼쪽 대기
            
            return leftResult.add(rightResult);       // 결합
        }
    }
}

4.7 자기 점검 답변

fork / compute / join 순서의 효율은?

답:
1. 효율적:

  • left.fork()
  • right.compute()
  • left.join()
  1. 이유:

    • 현재 스레드 활용
    • 한쪽 직접
  2. 비효율:

    • 둘 다 fork (놀음)
    • fork 후 즉시 join (순차)
  3. invokeAll:

    • 자동 (Action 편의)

5️⃣ 결과 합치기 (RecursiveTask)

5.1 결과 합치기

RecursiveTask 결과 합치기:

  하위 작업 결과를 합침.
  - 합산: +
  - 최대: max
  - 리스트: addAll
  - 등

5.2 합산

// 합산
protected Long compute() {
    if (작음) return directSum();
    // 분할
    left.fork();
    long r = right.compute();
    long l = left.join();
    return l + r;   // 합산
}

5.3 최댓값

// 최댓값
protected Integer compute() {
    if (작음) return directMax();
    left.fork();
    int r = right.compute();
    int l = left.join();
    return Math.max(l, r);   // 최댓값
}

5.4 리스트 병합

// 리스트 병합
protected List<Result> compute() {
    if (작음) return directProcess();
    left.fork();
    List<Result> r = right.compute();
    List<Result> l = left.join();
    List<Result> merged = new ArrayList<>(l);
    merged.addAll(r);   // 병합
    return merged;
}

5.5 결합 함수

결합 함수 패턴:

  return combine(leftResult, rightResult);

  combine:
    - 합산: a + b
    - 최대: max(a, b)
    - 병합: merge(a, b)
    - 커스텀

5.6 ILIC 의 맥락

@Service
public class ResultCombining {
    
    private final ForkJoinPool pool = new ForkJoinPool();
    
    // 통계 합치기
    static class StatsTask extends RecursiveTask<ShipmentStats> {
        private final List<Shipment> shipments;
        private final int start, end;
        private static final int THRESHOLD = 1000;
        
        StatsTask(List<Shipment> shipments, int start, int end) {
            this.shipments = shipments;
            this.start = start;
            this.end = end;
        }
        
        @Override
        protected ShipmentStats compute() {
            if (end - start <= THRESHOLD) {
                // 직접 통계
                BigDecimal total = BigDecimal.ZERO;
                int count = 0;
                for (int i = start; i < end; i++) {
                    total = total.add(shipments.get(i).getWeight());
                    count++;
                }
                return new ShipmentStats(total, count);
            }
            int mid = (start + end) / 2;
            StatsTask left = new StatsTask(shipments, start, mid);
            StatsTask right = new StatsTask(shipments, mid, end);
            left.fork();
            ShipmentStats rightStats = right.compute();
            ShipmentStats leftStats = left.join();
            return leftStats.merge(rightStats);   // 통계 합치기
        }
    }
    
    record ShipmentStats(BigDecimal total, int count) {
        ShipmentStats merge(ShipmentStats other) {
            return new ShipmentStats(
                total.add(other.total),
                count + other.count);
        }
    }
}

5.7 자기 점검 답변

결과 합치기는?

답:
1. 합치기:

  • 하위 결과 결합
  1. 방법:

    • 합산 (+)
    • 최대 (max)
    • 병합 (addAll)
  2. 결합 함수:

    • combine(l, r)
  3. 커스텀:

    • 통계 merge 등

6️⃣ 부수 효과 (RecursiveAction)

6.1 부수 효과

RecursiveAction 부수 효과:

  결과 반환 없이 작업 수행.
  - 배열 제자리 수정
  - 정렬
  - 변환

6.2 배열 수정

// 배열 제자리 수정
class TransformAction extends RecursiveAction {
    private final int[] array;
    private final int start, end;
    
    @Override
    protected void compute() {
        if (end - start <= THRESHOLD) {
            for (int i = start; i < end; i++) {
                array[i] = transform(array[i]);   // 제자리 수정
            }
            return;
        }
        int mid = (start + end) / 2;
        invokeAll(
            new TransformAction(array, start, mid),
            new TransformAction(array, mid, end));
    }
}

6.3 병렬 정렬

// 병렬 머지 소트 (개념)
class MergeSortAction extends RecursiveAction {
    @Override
    protected void compute() {
        if (작음) {
            sequentialSort();   // 직접 정렬
            return;
        }
        // 분할
        MergeSortAction left = new MergeSortAction(왼쪽);
        MergeSortAction right = new MergeSortAction(오른쪽);
        invokeAll(left, right);   // 병렬 정렬
        merge();   // 병합 (부수 효과)
    }
}
// Arrays.parallelSort 가 유사

6.4 주의 — 공유 상태

RecursiveAction 주의:

  여러 스레드가 같은 배열:
    - 다른 영역만 수정 (안전)
    - 같은 영역 공유 X
    - 경쟁 조건 주의

→ 분할 영역 독립성 보장

6.5 Task vs Action 선택

Task vs Action 선택:

RecursiveTask:
  - 결과 집계 (합산, 통계)
  - 새 컬렉션 반환

RecursiveAction:
  - 제자리 수정 (정렬, 변환)
  - 부수 효과
  - 결과 불필요

6.6 ILIC 의 맥락

@Service
public class SideEffectAction {
    
    private final ForkJoinPool pool = new ForkJoinPool();
    
    // 배송 상태 일괄 갱신 (부수 효과)
    public void updateAllStatus(List<Shipment> shipments, Status status) {
        pool.invoke(new UpdateAction(shipments, 0, shipments.size(), status));
    }
    
    static class UpdateAction extends RecursiveAction {
        private final List<Shipment> shipments;
        private final int start, end;
        private final Status status;
        private static final int THRESHOLD = 500;
        
        UpdateAction(List<Shipment> shipments, int start, int end, Status status) {
            this.shipments = shipments;
            this.start = start;
            this.end = end;
            this.status = status;
        }
        
        @Override
        protected void compute() {
            if (end - start <= THRESHOLD) {
                for (int i = start; i < end; i++) {
                    shipments.get(i).setStatus(status);   // 제자리 수정
                    // 각 인덱스 독립 (경쟁 X)
                }
                return;
            }
            int mid = (start + end) / 2;
            invokeAll(
                new UpdateAction(shipments, start, mid, status),
                new UpdateAction(shipments, mid, end, status));
        }
    }
    
    enum Status { CREATED, PROCESSING, DONE }
}

6.7 자기 점검 답변

부수 효과 처리는?

답:
1. 부수 효과:

  • 결과 없이 작업
  • 제자리 수정
  1. 예:

    • 정렬, 변환
    • 상태 갱신
  2. 주의:

    • 분할 영역 독립
    • 경쟁 조건
  3. 선택:

    • 집계: Task
    • 제자리: Action

7️⃣ 임계값 선택

7.1 트레이드오프

임계값 트레이드오프:

너무 작음:
  - 많은 분할
  - 관리 오버헤드 ↑
  - 작은 작업 너무 많음

너무 큼:
  - 적은 분할
  - 병렬성 ↓
  - 코어 활용 ↓

7.2 적정 기준

적정 기준:

  - 코어 수 × N 개 조각
  - 분할 오버헤드 < 처리 시간
  - 작업 균형

경험적:
  - 수백 ~ 수천
  - 측정으로 튜닝

7.3 측정

// 임계값 튜닝 (벤치마크)
int[] thresholds = {100, 1000, 10000, 100000};
for (int threshold : thresholds) {
    long start = System.nanoTime();
    pool.invoke(new Task(data, 0, data.length, threshold));
    long elapsed = System.nanoTime() - start;
    log.info("Threshold {}: {}ms", threshold, elapsed / 1_000_000);
}
// 최적 임계값 측정

7.4 작업 특성

작업 특성별:

CPU 무거운 작업:
  - 작은 임계값 OK (분할 가치)

CPU 가벼운 작업:
  - 큰 임계값 (분할 오버헤드 회피)

→ 작업당 비용 고려

7.5 일반 권장

일반 권장:

  - 순차가 빠르면 안 나눔 (작은 데이터)
  - 분할 오버헤드 고려
  - 코어 수 기반 동적

공식 (참고):
  threshold = size / (cores × 4)

7.6 ILIC 의 맥락

@Service
public class ThresholdSelection {
    
    private final ForkJoinPool pool = new ForkJoinPool();
    private final int cores = Runtime.getRuntime().availableProcessors();
    
    public BigDecimal sumFreight(List<Shipment> shipments) {
        int size = shipments.size();
        
        // 작으면 순차 (분할 가치 X)
        if (size < 10000) {
            return shipments.stream()
                .map(Shipment::getWeight)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        }
        
        // 크면 ForkJoin (동적 임계값)
        int threshold = Math.max(1000, size / (cores * 4));
        return pool.invoke(new SumTask(shipments, 0, size, threshold));
    }
    
    static class SumTask extends RecursiveTask<BigDecimal> {
        // ... threshold 사용
        protected BigDecimal compute() { return BigDecimal.ZERO; }
    }
}

7.7 자기 점검 답변

임계값 선택의 트레이드오프는?

답:
1. 너무 작음:

  • 분할 오버헤드
  1. 너무 큼:

    • 병렬성 ↓
  2. 적정:

    • 코어 × N 조각
    • 측정 튜닝
  3. 작은 데이터:

    • 순차 (분할 X)

8️⃣ Phase 8 완주 + 4주차 종합

8.1 Phase 8 학습 종합

Phase 8 — 고급 비동기

Unit 8.1 — CompletableFuture (★ 마스터)
  - 논블로킹 콜백
  - thenApply/Compose/Combine
  - allOf/anyOf

Unit 8.2 — ForkJoinPool
  - 분할 정복
  - work stealing

Unit 8.3 — RecursiveTask
  - compute 분할
  - 임계값

8.2 4주차 전체 종합 (Phase 1~8)

4주차 — 동시성과 멀티스레딩

Phase 1 — 동시성의 기초
  - 멀티태스킹, 프로세스/스레드

Phase 2 — Sync/Async × Blocking/NonBlocking
  - 4분면 매트릭스

Phase 3 — 스레드 다루기
  - 생성, 상태, join, daemon

Phase 4 — synchronized & volatile (★ 1차 정점)
  - 임계 영역, 원자성, 가시성

Phase 5 — Lock 도구
  - ReentrantLock, tryLock

Phase 6 — 스레드 협력
  - 생산자-소비자, wait/notify

Phase 7 — Executor (★ 2차 정점)
  - 스레드 풀, ThreadPoolExecutor

Phase 8 — 고급 비동기
  - CompletableFuture, ForkJoinPool

8.3 동시성 도구 계층

동시성 도구 계층 (저수준 → 고수준):

저수준:
  - synchronized, volatile
  - wait/notify
  - LockSupport

중간:
  - ReentrantLock, Condition
  - Atomic
  - 동시성 컬렉션

고수준:
  - BlockingQueue
  - ExecutorService
  - CompletableFuture
  - ForkJoinPool

→ 고수준 우선 권장

8.4 4주차 핵심 통찰

4주차 핵심 통찰 8가지:

1. 동시성 ≠ 병렬성
2. 경쟁 조건 (count++)
3. synchronized (원자성)
4. volatile (가시성)
5. Lock (정교한 제어)
6. 생산자-소비자 (협력)
7. 스레드 풀 (재사용)
8. CompletableFuture (비동기 조합)

8.5 실무 권장 우선순위

실무 동시성 우선순위:

1. 무상태 (동기화 불필요)
2. 불변 객체
3. 동시성 컬렉션
4. Atomic
5. 고수준 도구 (Executor, CompletableFuture)
6. synchronized / Lock (필요 시)

→ 높은 추상화 우선

8.6 자기 점검 답변

4주차 동시성 전체의 종합은?

답:
1. 8 Phase:

  • 기초 → 4분면 → 스레드
  • sync/volatile → Lock → 협력
  • Executor → 고급 비동기
  1. 계층:

    • 저수준 → 고수준
  2. 핵심:

    • 원자성, 가시성
    • 협력, 풀, 비동기
  3. 권장:

    • 고수준 우선

9️⃣ 면접 + 자기 점검 + Phase 8 졸업 시험 + 4주차 완주

9.1 면접 단골 질문 매핑

Q핵심 답변
RecursiveTask vs Action?결과 O vs X
compute() 패턴?임계값 → 분할 → 결합
임계값?분할 중단 기준
fork/compute/join?비동기/직접/대기
효율 순서?fork → compute → join
결과 합치기?combine
부수 효과?제자리 수정
임계값 선택?오버헤드 vs 병렬성
invokeAll?자동 fork/join
ForkJoin 적합?CPU 분할 정복

9.2 Phase 8 졸업 시험 50문항

CompletableFuture (15문항)

Q1. CompletableFuture? → 논블로킹 콜백
Q2. Future 극복? → 블로킹, 조합
Q3. supplyAsync? → Supplier (결과)
Q4. runAsync? → Runnable (결과 X)
Q5. thenApply? → 변환
Q6. thenAccept? → 소비
Q7. thenRun? → 무관
Q8. thenCompose? → 의존 (flatMap)
Q9. thenCombine? → 독립 (zip)
Q10. exceptionally? → 대체값
Q11. handle? → 결과 + 예외
Q12. allOf? → 모두 완료
Q13. anyOf? → 하나
Q14. thenApplyAsync? → 별도 스레드
Q15. commonPool 주의? → 블로킹 고갈

ForkJoinPool (18문항)

Q16. ForkJoinPool? → 분할 정복
Q17. 분할 정복? → 분할/정복/결합
Q18. work stealing? → 자기 큐 + 훔치기
Q19. deque? → 양방향, 워커별
Q20. 자기 LIFO? → 한쪽 끝
Q21. 훔치기 FIFO? → 반대쪽 끝
Q22. fork()? → 비동기 push
Q23. join()? → 결과 대기
Q24. ForkJoinTask? → 작업 단위
Q25. commonPool? → 전역 공유
Q26. 병렬 스트림? → commonPool
Q27. vs ThreadPoolExecutor? → 분산 큐
Q28. work stealing 효율? → 부하 분산
Q29. LIFO 이유? → 캐시 지역성
Q30. 블로킹? → 부적합
Q31. 병렬도? → 코어 수
Q32. invoke? → 동기 실행
Q33. 적합? → CPU 분할

RecursiveTask (17문항)

Q34. RecursiveTask? → 결과 있음
Q35. RecursiveAction? → 결과 없음
Q36. compute()? → 분할 로직
Q37. 임계값? → 분할 중단
Q38. compute 패턴? → 임계값/분할/결합
Q39. fork/compute/join 순서? → 효율
Q40. left.fork()? → 왼쪽 비동기
Q41. right.compute()? → 오른쪽 직접
Q42. left.join()? → 왼쪽 대기
Q43. 둘 다 fork? → 비효율
Q44. 결과 합치기? → combine
Q45. invokeAll? → 자동 fork/join
Q46. 부수 효과? → 제자리 수정
Q47. 임계값 작음? → 오버헤드
Q48. 임계값 큼? → 병렬성 ↓
Q49. 동적 임계값? → 코어 기반
Q50. 작은 데이터? → 순차

9.3 채점

50 / 50 → Phase 8 마스터
45-49   → 거의 마스터
40-44   → 복습
< 40    → Unit 8.1 ~ 8.3 재학습

9.4 추가 심화 질문

Q1: Arrays.parallelSort 의 내부?

답:

  • ForkJoinPool 사용
  • 분할 정복 머지 소트
  • 임계값 (MIN_ARRAY_SORT_GRAN)
  • 병렬 정렬

Q2: ForkJoin 의 RecursiveTask 결과 전파?

답:

  • compute 반환값
  • join 으로 부모에
  • 트리 따라 합산
  • 루트에서 최종

Q3: CompletableFuture + ForkJoinPool?

답:

  • CF 기본 commonPool (ForkJoinPool)
  • 커스텀 Executor 지정 가능
  • 분할 정복은 ForkJoinPool
  • 일반 비동기는 ThreadPoolExecutor

Q4: 병렬 스트림 vs ForkJoin 직접?

답:

  • 병렬 스트림: 간편 (자동 분할)
  • ForkJoin 직접: 세밀한 제어
  • 대부분 병렬 스트림 충분
  • 복잡한 분할은 직접

Q5: Virtual Thread 시대의 ForkJoin?

답:

  • ForkJoin: CPU 집약 여전히 유효
  • Virtual Thread: I/O 바운드
  • 용도 다름
  • 분할 정복은 ForkJoin

9.5 4주차 완주 정리

🎓 4주차 — 동시성과 멀티스레딩 완주!

Phase 1~8, 총 35 Unit:
  ✅ Phase 1 — 동시성의 기초 (4)
  ✅ Phase 2 — 4분면 매트릭스 (3, 2.3 ★마스터)
  ✅ Phase 3 — 스레드 다루기 (5)
  ✅ Phase 4 — synchronized & volatile (5, 4.4·4.5 ★마스터, ★1차 정점)
  ✅ Phase 5 — Lock 도구 (4, 5.4 ★마스터)
  ✅ Phase 6 — 스레드 협력 (4)
  ✅ Phase 7 — Executor (7, 7.4 ★마스터, ★2차 정점)
  ✅ Phase 8 — 고급 비동기 (3, 8.1 ★마스터)

마스터 Unit: 2.3, 4.4, 4.5, 5.4, 7.4, 8.1 (6개)
정점: Phase 4 (1차), Phase 7 (2차)

🎯 핵심 요약 — 3줄 정리

1. RecursiveTask / RecursiveAction

  • RecursiveTask: 결과 있음 (합산)
  • RecursiveAction: 결과 없음 (정렬)

2. compute() 패턴

  • 임계값 이하 → 직접, 초과 → 분할
  • left.fork() → right.compute() → left.join()

3. 임계값

  • 너무 작으면 오버헤드, 너무 크면 병렬성 ↓
  • 코어 수 기반, 작은 데이터는 순차

🏆 Phase 8 완주 — 고급 비동기 마스터

🚀 Phase 8 — 고급 비동기
  ✅ Unit 8.1 CompletableFuture (★ 마스터)
  ✅ Unit 8.2 ForkJoinPool
  ✅ Unit 8.3 RecursiveTask ← 여기, Phase 8 완주

→ CompletableFuture (비동기 조합)
→ ForkJoinPool (분할 정복)
→ RecursiveTask (재귀 분할)

🎓 4주차 전체 완주 — 동시성과 멀티스레딩 마스터

🎓 4주차 — 동시성과 멀티스레딩 (Phase 1~8, 35 Unit) 완주!

✅ Phase 1 — 동시성의 기초
✅ Phase 2 — 4분면 매트릭스 (★마스터 2.3)
✅ Phase 3 — 스레드 다루기
✅ Phase 4 — synchronized & volatile (★1차 정점, ★마스터 4.4·4.5)
✅ Phase 5 — Lock 도구 (★마스터 5.4)
✅ Phase 6 — 스레드 협력
✅ Phase 7 — Executor (★2차 정점, ★마스터 7.4)
✅ Phase 8 — 고급 비동기 (★마스터 8.1)

→ 동시성 기초부터 고급 비동기까지
→ synchronized/volatile/Lock/Executor/CompletableFuture/ForkJoin
→ 동시성과 멀티스레딩 완전 정복

🏆 Phase 8 완주 — 고급 비동기 마스터
🎓 4주차 전체 완주 — 동시성과 멀티스레딩 마스터 (35 Unit)

profile
Software Developer

0개의 댓글