Unit 8.3 — RecursiveTask

Psj·6일 전

F-lab

목록 보기
154/197

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() 로 결과를 반환 (합산, 검색 등), RecursiveActionvoid 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개의 댓글