F-LAB JAVA · 4주차 · Phase 8 · 고급 비동기
🏆 Phase 8 완주 + 🎓 4주차 전체 완주 — 동시성/멀티스레딩 마스터
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
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.
1. RecursiveTask vs RecursiveAction
2. compute() 구현 패턴
3. 임계값 기반 분할
4. fork / compute / join 순서
5. 결과 합치기 (RecursiveTask)
6. 부수 효과 (RecursiveAction)
7. 임계값 선택
8. Phase 8 완주 + 4주차 종합
9. 면접 + 자기 점검 + Phase 8 졸업 시험 + 4주차 완주
ForkJoinTask 하위:
RecursiveTask<V>:
- V compute()
- 결과 반환
RecursiveAction:
- void compute()
- 결과 없음
// RecursiveTask — 결과 있음
class SumTask extends RecursiveTask<Long> {
@Override
protected Long compute() {
// 결과 반환
return sum;
}
}
// RecursiveAction — 결과 없음
class SortAction extends RecursiveAction {
@Override
protected void compute() {
// 부수 효과 (정렬, 변환)
// 반환 X
}
}
선택:
RecursiveTask:
- 합산, 카운트
- 검색 결과
- 변환 후 결과
RecursiveAction:
- 정렬 (제자리)
- 배열 변환 (제자리)
- 부수 효과
ForkJoinPool pool = new ForkJoinPool();
// RecursiveTask — 결과
Long result = pool.invoke(new SumTask(...));
// RecursiveAction — 결과 없음
pool.invoke(new SortAction(...)); // 반환 없음
@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() { }
}
}
RecursiveTask와 RecursiveAction의 차이는?
답:
1. RecursiveTask:
RecursiveAction:
선택:
공통:
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);
}
compute 단계:
1. 임계값 확인
- 작으면 직접 처리
2. 분할
- 둘로 나눔
3. fork/compute/join
- 비동기 + 직접 + 대기
4. 결합
- 결과 합침
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; // 결합
}
}
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); // 둘 다 (편의)
}
}
// invokeAll — fork/join 자동
invokeAll(left, right);
// 또는
invokeAll(List.of(t1, t2, t3));
// 내부적으로 fork + join
// RecursiveAction 에 편리
@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; }
}
}
compute() 메서드의 구현 패턴은?
답:
1. 패턴:
RecursiveTask:
RecursiveAction:
단계:
임계값 (Threshold):
분할을 멈추고 직접 처리하는 기준.
- 작업 크기 <= 임계값 → 직접
- 초과 → 분할
protected Long compute() {
if (end - start <= THRESHOLD) { // 임계값 확인
// 분할 중단, 직접 처리
return directCompute();
}
// 분할 계속
return divide();
}
임계값 역할:
분할 깊이 제어:
- 작으면 → 많은 분할 (오버헤드)
- 크면 → 적은 분할 (병렬성 ↓)
적절한 균형:
- 분할 오버헤드 vs 병렬성
임계값과 트리 깊이:
데이터 100만, 임계값 1만:
- 100만 / 1만 = 100 리프
- 트리 깊이 log2(100) ≈ 7
임계값 100:
- 100만 / 100 = 1만 리프
- 분할 오버헤드 큼
// 데이터 크기 기반 동적 임계값
int threshold = Math.max(
MIN_THRESHOLD,
totalSize / (Runtime.getRuntime().availableProcessors() * 4)
);
// 코어 수 × 4 정도의 작업 조각
@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());
}
}
}
임계값 기반 분할 중단은?
답:
1. 임계값:
역할:
균형:
동적:
// ✓ 효율적
left.fork(); // 1. 왼쪽 비동기
V rightResult = right.compute(); // 2. 오른쪽 직접
V leftResult = left.join(); // 3. 왼쪽 결과
return combine(leftResult, rightResult);
순서 이유:
left.fork():
- 왼쪽을 큐에 (다른 워커 가능)
right.compute():
- 오른쪽을 현재 스레드 직접
- 노는 시간 없음
left.join():
- 왼쪽 완료 대기 (이미 진행 중)
→ 현재 스레드 활용
// ❌ 둘 다 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();
// → 순차나 다름없음
효율적 순서:
현재 스레드:
left.fork() → [right.compute() 직접] → left.join()
↓ (동시)
다른 워커: [left 처리]
→ 현재 스레드 + 다른 워커 병렬
→ 효율
// invokeAll — 자동 (편의)
invokeAll(left, right);
// 내부: 효율적 fork/join
// RecursiveAction 에 편리
// RecursiveTask 는 명시적 fork/compute/join
// (결과 합침 필요)
@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); // 결합
}
}
}
fork / compute / join 순서의 효율은?
답:
1. 효율적:
이유:
비효율:
invokeAll:
RecursiveTask 결과 합치기:
하위 작업 결과를 합침.
- 합산: +
- 최대: max
- 리스트: addAll
- 등
// 합산
protected Long compute() {
if (작음) return directSum();
// 분할
left.fork();
long r = right.compute();
long l = left.join();
return l + r; // 합산
}
// 최댓값
protected Integer compute() {
if (작음) return directMax();
left.fork();
int r = right.compute();
int l = left.join();
return Math.max(l, r); // 최댓값
}
// 리스트 병합
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;
}
결합 함수 패턴:
return combine(leftResult, rightResult);
combine:
- 합산: a + b
- 최대: max(a, b)
- 병합: merge(a, b)
- 커스텀
@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);
}
}
}
결과 합치기는?
답:
1. 합치기:
방법:
결합 함수:
커스텀:
RecursiveAction 부수 효과:
결과 반환 없이 작업 수행.
- 배열 제자리 수정
- 정렬
- 변환
// 배열 제자리 수정
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));
}
}
// 병렬 머지 소트 (개념)
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 가 유사
RecursiveAction 주의:
여러 스레드가 같은 배열:
- 다른 영역만 수정 (안전)
- 같은 영역 공유 X
- 경쟁 조건 주의
→ 분할 영역 독립성 보장
Task vs Action 선택:
RecursiveTask:
- 결과 집계 (합산, 통계)
- 새 컬렉션 반환
RecursiveAction:
- 제자리 수정 (정렬, 변환)
- 부수 효과
- 결과 불필요
@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 }
}
부수 효과 처리는?
답:
1. 부수 효과:
예:
주의:
선택:
임계값 트레이드오프:
너무 작음:
- 많은 분할
- 관리 오버헤드 ↑
- 작은 작업 너무 많음
너무 큼:
- 적은 분할
- 병렬성 ↓
- 코어 활용 ↓
적정 기준:
- 코어 수 × N 개 조각
- 분할 오버헤드 < 처리 시간
- 작업 균형
경험적:
- 수백 ~ 수천
- 측정으로 튜닝
// 임계값 튜닝 (벤치마크)
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);
}
// 최적 임계값 측정
작업 특성별:
CPU 무거운 작업:
- 작은 임계값 OK (분할 가치)
CPU 가벼운 작업:
- 큰 임계값 (분할 오버헤드 회피)
→ 작업당 비용 고려
일반 권장:
- 순차가 빠르면 안 나눔 (작은 데이터)
- 분할 오버헤드 고려
- 코어 수 기반 동적
공식 (참고):
threshold = size / (cores × 4)
@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; }
}
}
임계값 선택의 트레이드오프는?
답:
1. 너무 작음:
너무 큼:
적정:
작은 데이터:
Phase 8 — 고급 비동기
Unit 8.1 — CompletableFuture (★ 마스터)
- 논블로킹 콜백
- thenApply/Compose/Combine
- allOf/anyOf
Unit 8.2 — ForkJoinPool
- 분할 정복
- work stealing
Unit 8.3 — RecursiveTask
- compute 분할
- 임계값
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
동시성 도구 계층 (저수준 → 고수준):
저수준:
- synchronized, volatile
- wait/notify
- LockSupport
중간:
- ReentrantLock, Condition
- Atomic
- 동시성 컬렉션
고수준:
- BlockingQueue
- ExecutorService
- CompletableFuture
- ForkJoinPool
→ 고수준 우선 권장
4주차 핵심 통찰 8가지:
1. 동시성 ≠ 병렬성
2. 경쟁 조건 (count++)
3. synchronized (원자성)
4. volatile (가시성)
5. Lock (정교한 제어)
6. 생산자-소비자 (협력)
7. 스레드 풀 (재사용)
8. CompletableFuture (비동기 조합)
실무 동시성 우선순위:
1. 무상태 (동기화 불필요)
2. 불변 객체
3. 동시성 컬렉션
4. Atomic
5. 고수준 도구 (Executor, CompletableFuture)
6. synchronized / Lock (필요 시)
→ 높은 추상화 우선
4주차 동시성 전체의 종합은?
답:
1. 8 Phase:
계층:
핵심:
권장:
| Q | 핵심 답변 |
|---|---|
| RecursiveTask vs Action? | 결과 O vs X |
| compute() 패턴? | 임계값 → 분할 → 결합 |
| 임계값? | 분할 중단 기준 |
| fork/compute/join? | 비동기/직접/대기 |
| 효율 순서? | fork → compute → join |
| 결과 합치기? | combine |
| 부수 효과? | 제자리 수정 |
| 임계값 선택? | 오버헤드 vs 병렬성 |
| invokeAll? | 자동 fork/join |
| ForkJoin 적합? | CPU 분할 정복 |
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 주의? → 블로킹 고갈
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 분할
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. 작은 데이터? → 순차
50 / 50 → Phase 8 마스터
45-49 → 거의 마스터
40-44 → 복습
< 40 → Unit 8.1 ~ 8.3 재학습
답:
답:
답:
답:
답:
🎓 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차)
1. RecursiveTask / RecursiveAction
2. compute() 패턴
3. 임계값
🚀 Phase 8 — 고급 비동기
✅ Unit 8.1 CompletableFuture (★ 마스터)
✅ Unit 8.2 ForkJoinPool
✅ Unit 8.3 RecursiveTask ← 여기, Phase 8 완주
→ CompletableFuture (비동기 조합)
→ ForkJoinPool (분할 정복)
→ RecursiveTask (재귀 분할)
🎓 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)