Unit 7.2 — Executor와 ExecutorService

Psj·5일 전

F-lab

목록 보기
146/197

Unit 7.2 — Executor와 ExecutorService

F-LAB JAVA · 4주차 · Phase 7 · Executor 프레임워크


📌 학습 목표

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

  • Executor 인터페이스 의 역할은?
  • ExecutorService 가 Executor 를 확장한 점은?
  • execute() vs submit() 의 차이는?
  • execute 의 예외 처리 와 submit 의 예외 처리 차이는?
  • Executors 팩토리 메서드 들은?
  • invokeAll() / invokeAny() 의 동작은?
  • ScheduledExecutorService (예약 실행) 은?
  • Executor 프레임워크의 계층 구조는?
  • 작업 제출과 실행의 분리 의미는?

🎯 핵심 한 문장

Executor 프레임워크는 작업 제출 (submission) 과 실행 (execution) 을 분리하는 추상화로, Executor (실행) → ExecutorService (생애주기·결과 관리) → ScheduledExecutorService (예약 실행) 계층으로 구성된다.
Executorexecute(Runnable) 하나만 가진 최소 인터페이스로 "작업을 어떻게 실행할지" 를 추상화하고, ExecutorService 는 여기에 submit (Future 반환), shutdown (종료), invokeAll/invokeAny (일괄 실행) 등을 추가한다.
execute() 는 Runnable 을 받아 반환값 없이 실행하고 (예외는 UncaughtExceptionHandler 로), submit() 은 Runnable/Callable 을 받아 Future 를 반환 하며 예외는 Future.get() 시 ExecutionException 으로 전달된다.
Executors 팩토리는 자주 쓰는 풀 (newFixedThreadPool, newCachedThreadPool, newSingleThreadExecutor, newScheduledThreadPool) 을 간단히 생성해주지만, 내부 설정이 숨겨져 있어 직접 ThreadPoolExecutor 설정이 권장되기도 한다.
이 분리 덕분에 작업 코드 (무엇을) 와 실행 정책 (어떻게: 스레드 풀, 순차, 예약) 이 독립적으로 바뀔 수 있다.

비유 — 음식점 주문 시스템

Executor 프레임워크 = 주문 시스템:

작업 제출 = 주문 (손님):
  - "이 요리 만들어줘" (작업)
  - 어떻게 만드는지 모름 (분리)

실행 = 주방 (Executor):
  - 주문을 어떻게 처리할지
  - 요리사 배정 (스레드)

Executor (최소):
  - execute(요리)
  - "만들어줘" (결과 안 받음)

ExecutorService (확장):
  - submit(요리) → 번호표 (Future)
  - "만들고 번호표 줘" (결과 추적)
  - 주방 마감 (shutdown)
  - 여러 주문 일괄 (invokeAll)

ScheduledExecutorService (예약):
  - "30분 후 만들어줘" (예약)
  - "매시간 만들어줘" (반복)

분리의 장점:
  - 손님은 주방 운영 몰라도 됨
  - 주방 정책 바꿔도 주문 그대로

→ Executor = 실행 추상화, 제출과 실행 분리, 계층 (Executor → Service → Scheduled).


🧭 9개 섹션 로드맵

1. Executor 인터페이스
2. ExecutorService 확장
3. execute() vs submit()
4. 예외 처리 차이
5. Executors 팩토리
6. invokeAll / invokeAny
7. ScheduledExecutorService
8. 제출과 실행의 분리
9. 면접 + 자기 점검

1️⃣ Executor 인터페이스

1.1 Executor

public interface Executor {
    void execute(Runnable command);
}
// 단 하나의 메서드

1.2 역할

Executor 역할:

  작업 실행을 추상화.
  - "어떻게 실행할지"
  - 작업 제출과 실행 분리

핵심:
  - execute(Runnable)
  - 최소 추상화

1.3 실행 정책 추상화

// 다양한 실행 정책
Executor direct = Runnable::run;   // 즉시 (현재 스레드)

Executor newThread = r -> new Thread(r).start();   // 새 스레드

Executor pool = Executors.newFixedThreadPool(10);   // 스레드 풀

// 모두 Executor (실행 방법만 다름)

1.4 사용

Executor executor = Executors.newFixedThreadPool(10);

executor.execute(() -> {
    doWork();
});
// Runnable 실행 (반환값 X)

1.5 단순함의 의미

Executor 의 단순함:

  execute 하나:
    - 작업 어떻게 실행하나
    - 결과/생애주기 X

  → 가장 기본 추상화
  → ExecutorService 가 확장

1.6 ILIC 의 맥락

@Service
public class ExecutorBasics {
    
    private final Executor executor = Executors.newFixedThreadPool(10);
    
    public void process(Shipment shipment) {
        executor.execute(() -> {
            doProcess(shipment);   // Runnable 실행
        });
        // 반환값 없음 (단순 실행)
    }
    
    private void doProcess(Shipment s) { }
}

1.7 자기 점검 답변

Executor 인터페이스의 역할은?

:
1. 정의:

  • execute(Runnable)
  • 단일 메서드
  1. 역할:

    • 실행 추상화
    • 제출과 실행 분리
  2. 정책:

    • 즉시/새 스레드/풀
    • 모두 Executor
  3. 단순:

    • 최소 추상화

2️⃣ ExecutorService 확장

2.1 ExecutorService

public interface ExecutorService extends Executor {
    // 결과 있는 제출
    <T> Future<T> submit(Callable<T> task);
    Future<?> submit(Runnable task);
    <T> Future<T> submit(Runnable task, T result);
    
    // 일괄 실행
    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks);
    <T> T invokeAny(Collection<? extends Callable<T>> tasks);
    
    // 생애주기
    void shutdown();
    List<Runnable> shutdownNow();
    boolean isShutdown();
    boolean isTerminated();
    boolean awaitTermination(long timeout, TimeUnit unit);
}

2.2 추가 기능

ExecutorService 추가:

Executor (실행):
  - execute

ExecutorService (확장):
  - submit (Future 반환)
  - invokeAll/invokeAny (일괄)
  - shutdown (종료)
  - awaitTermination (종료 대기)

2.3 생애주기 관리

생애주기:

  Running (실행 중)
    ↓ shutdown()
  Shutting down (새 작업 거부, 기존 완료)
    ↓ (모든 작업 완료)
  Terminated (종료)

또는:
    ↓ shutdownNow()
  즉시 종료 시도 (인터럽트)

2.4 기본 사용

ExecutorService executor = Executors.newFixedThreadPool(10);

// 작업 제출
Future<Result> future = executor.submit(() -> compute());

// 결과
Result result = future.get();

// 종료
executor.shutdown();
executor.awaitTermination(30, TimeUnit.SECONDS);

2.5 계층 구조

Executor 프레임워크 계층:

Executor (execute)
   ↑
ExecutorService (submit, shutdown, invokeAll)
   ↑
ScheduledExecutorService (schedule)

구현:
  - ThreadPoolExecutor (ExecutorService)
  - ScheduledThreadPoolExecutor (ScheduledExecutorService)

2.6 ILIC 의 맥락

@Service
public class ExecutorServiceUsage {
    
    private final ExecutorService executor = Executors.newFixedThreadPool(10);
    
    // submit (Future)
    public Future<BigDecimal> calculateAsync(Shipment shipment) {
        return executor.submit(() -> calculateFreight(shipment));
    }
    
    // invokeAll (일괄)
    public List<Future<Result>> processBatch(List<Shipment> shipments) 
            throws InterruptedException {
        List<Callable<Result>> tasks = shipments.stream()
            .map(s -> (Callable<Result>) () -> process(s))
            .toList();
        return executor.invokeAll(tasks);
    }
    
    @PreDestroy
    public void shutdown() throws InterruptedException {
        executor.shutdown();
        if (!executor.awaitTermination(30, TimeUnit.SECONDS)) {
            executor.shutdownNow();
        }
    }
    
    private BigDecimal calculateFreight(Shipment s) { return s.getWeight(); }
    private Result process(Shipment s) { return new Result(); }
    record Result() {}
}

2.7 자기 점검 답변

ExecutorService가 Executor를 확장한 점은?

:
1. 확장:

  • submit (Future)
  • invokeAll/invokeAny
  • shutdown
  1. 생애주기:

    • Running → Shutting down → Terminated
  2. 계층:

    • Executor → ExecutorService → Scheduled
  3. 구현:

    • ThreadPoolExecutor

3️⃣ execute() vs submit()

3.1 비교

항목execute()submit()
인터페이스ExecutorExecutorService
파라미터RunnableRunnable/Callable
반환값voidFuture
결과 회수XO
예외UncaughtExceptionHandlerFuture.get()

3.2 execute()

// execute — 반환값 없음
executor.execute(() -> {
    doWork();
    // 결과 못 받음
});

3.3 submit()

// submit — Future 반환
Future<Result> future = executor.submit(() -> {
    return compute();   // Callable (반환값)
});

Result result = future.get();   // 결과 회수

// Runnable 도 가능 (Future<?>, 결과 null)
Future<?> f = executor.submit(() -> doWork());
f.get();   // 완료 대기 (결과 null)

3.4 결과 회수

결과 회수:

execute:
  - 결과 회수 불가
  - 단순 실행

submit:
  - Future 반환
  - get() 으로 결과
  - 완료 확인 (isDone)
  - 취소 (cancel)

3.5 언제 어느 것

선택:

execute:
  - 결과 불필요
  - 단순 작업 (로그, 알림)

submit:
  - 결과 필요
  - 예외 처리
  - 작업 추적/취소

3.6 ILIC 의 맥락

@Service
public class ExecuteVsSubmit {
    
    private final ExecutorService executor = Executors.newFixedThreadPool(10);
    
    // execute — 결과 불필요 (알림)
    public void sendNotification(Shipment shipment) {
        executor.execute(() -> {
            emailService.send(shipment);
            // 결과 안 받음
        });
    }
    
    // submit — 결과 필요 (계산)
    public Future<BigDecimal> calculateFreight(Shipment shipment) {
        return executor.submit(() -> {
            return freightCalculator.calculate(shipment);   // 반환
        });
    }
    
    // submit — 작업 추적/취소
    public void processWithTracking(Shipment shipment) {
        Future<?> future = executor.submit(() -> process(shipment));
        // 나중에 취소 가능
        // future.cancel(true);
    }
    
    private void process(Shipment s) { }
}

3.7 자기 점검 답변

execute() vs submit() 차이는?

:
1. execute:

  • Executor
  • void (결과 X)
  1. submit:

    • ExecutorService
    • Future (결과 O)
  2. 결과:

    • execute: 회수 X
    • submit: get()
  3. 선택:

    • 결과 불필요: execute
    • 결과/추적: submit

4️⃣ 예외 처리 차이

4.1 핵심 차이

예외 처리 차이:

execute:
  - 예외 시 UncaughtExceptionHandler
  - 또는 스레드 종료
  - 스택 트레이스 출력

submit:
  - 예외를 Future 에 저장
  - Future.get() 시 ExecutionException
  - 조용히 (get 안 하면 모름)

4.2 execute 예외

// execute — 예외 즉시 표출
executor.execute(() -> {
    throw new RuntimeException("Error");
    // UncaughtExceptionHandler 또는
    // 스택 트레이스 출력
});

4.3 submit 예외

// submit — 예외 Future 에 저장
Future<?> future = executor.submit(() -> {
    throw new RuntimeException("Error");
});

// get 안 하면 예외 모름!
// get 하면:
try {
    future.get();   // ExecutionException
} catch (ExecutionException e) {
    Throwable cause = e.getCause();   // 원래 예외
    log.error("Task failed", cause);
}

4.4 submit 예외의 함정

// ❌ submit 예외 무시 (함정)
executor.submit(() -> {
    riskyOperation();   // 예외 발생
});
// get 안 함 → 예외 사라짐 (조용히)
// 디버깅 어려움

// ✓ get 으로 확인
Future<?> future = executor.submit(() -> riskyOperation());
try {
    future.get();
} catch (ExecutionException e) {
    handleError(e.getCause());
}

4.5 예외 처리 전략

// 작업 내부에서 처리 (권장)
executor.submit(() -> {
    try {
        riskyOperation();
    } catch (Exception e) {
        log.error("Failed", e);
        // 내부 처리 (Future 예외 X)
    }
});

// 또는 get 으로 회수
// 또는 UncaughtExceptionHandler (execute)

4.6 ILIC 의 맥락

@Service
public class ExceptionHandling {
    
    private final ExecutorService executor = Executors.newFixedThreadPool(10);
    
    // execute — 핸들러 (스레드 팩토리)
    private final ExecutorService executorWithHandler = 
        Executors.newFixedThreadPool(10, r -> {
            Thread t = new Thread(r);
            t.setUncaughtExceptionHandler((thread, ex) -> 
                log.error("Uncaught in {}", thread.getName(), ex));
            return t;
        });
    
    // submit — get 으로 예외 확인
    public void processWithExceptionCheck(Shipment shipment) {
        Future<?> future = executor.submit(() -> process(shipment));
        try {
            future.get();   // 예외 회수
        } catch (ExecutionException e) {
            log.error("Processing failed for {}", shipment.getId(), e.getCause());
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
    
    // 권장 — 작업 내부 처리
    public void processSafe(Shipment shipment) {
        executor.submit(() -> {
            try {
                process(shipment);
            } catch (Exception e) {
                log.error("Failed for {}", shipment.getId(), e);
            }
        });
    }
    
    private void process(Shipment s) { }
}

4.7 자기 점검 답변

execute와 submit의 예외 처리 차이는?

:
1. execute:

  • UncaughtExceptionHandler
  • 즉시 표출
  1. submit:

    • Future 에 저장
    • get() 시 ExecutionException
  2. 함정:

    • submit 예외 get 안 하면 사라짐
  3. 전략:

    • 작업 내부 처리 (권장)
    • 또는 get 회수

5️⃣ Executors 팩토리

5.1 Executors

Executors:

  자주 쓰는 스레드 풀을
  간단히 생성하는 팩토리.

장점:
  - 간편
  - 일반적 설정

단점:
  - 내부 설정 숨김
  - 위험할 수도

5.2 주요 팩토리 메서드

// 고정 크기
ExecutorService fixed = Executors.newFixedThreadPool(10);

// 캐시 (동적)
ExecutorService cached = Executors.newCachedThreadPool();

// 단일
ExecutorService single = Executors.newSingleThreadExecutor();

// 예약
ScheduledExecutorService scheduled = 
    Executors.newScheduledThreadPool(4);

// 가상 스레드 (Java 21+)
ExecutorService virtual = Executors.newVirtualThreadPerTaskExecutor();

5.3 각 메서드 (Unit 7.5 정밀)

팩토리 메서드:

newFixedThreadPool(n):
  - 고정 n개
  - 무제한 큐

newCachedThreadPool:
  - 동적 (0 ~ 무제한)
  - SynchronousQueue
  - 위험 (무제한)

newSingleThreadExecutor:
  - 1개
  - 순차 보장

newScheduledThreadPool(n):
  - 예약/반복

5.4 Executors 의 위험

Executors 위험:

newFixedThreadPool:
  - 무제한 큐 (LinkedBlockingQueue)
  - 작업 쌓이면 OOM

newCachedThreadPool:
  - 무제한 스레드
  - 폭증 시 OOM

→ 직접 ThreadPoolExecutor 권장 (제한)

5.5 직접 생성 권장

// Executors (간편하지만 위험)
ExecutorService risky = Executors.newFixedThreadPool(10);
// 무제한 큐

// 직접 생성 (안전, 권장)
ExecutorService safe = new ThreadPoolExecutor(
    10, 10,
    0L, TimeUnit.MILLISECONDS,
    new LinkedBlockingQueue<>(1000),   // 제한된 큐
    new ThreadPoolExecutor.AbortPolicy()
);

5.6 ILIC 의 맥락

@Configuration
public class ExecutorFactoryConfig {
    
    private final int cores = Runtime.getRuntime().availableProcessors();
    
    // Executors (간편 — 작은 작업)
    @Bean("simpleExecutor")
    public ExecutorService simpleExecutor() {
        return Executors.newFixedThreadPool(cores);
        // 주의: 무제한 큐
    }
    
    // 직접 생성 (권장 — 운영)
    @Bean("productionExecutor")
    public ExecutorService productionExecutor() {
        return new ThreadPoolExecutor(
            cores,
            cores * 2,
            60L, TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(1000),   // 제한
            r -> new Thread(r, "ilic-worker-" + System.nanoTime()),
            new ThreadPoolExecutor.CallerRunsPolicy()
        );
    }
}

5.7 자기 점검 답변

Executors 팩토리 메서드들은?

:
1. 메서드:

  • newFixedThreadPool
  • newCachedThreadPool
  • newSingleThreadExecutor
  • newScheduledThreadPool
  1. 장점:

    • 간편
  2. 위험:

    • 무제한 큐/스레드
    • OOM
  3. 권장:

    • 직접 ThreadPoolExecutor (제한)

6️⃣ invokeAll / invokeAny

6.1 invokeAll

invokeAll:

  여러 Callable 일괄 제출.
  - 모두 완료까지 대기 (블로킹)
  - List<Future> 반환

6.2 invokeAll 사용

List<Callable<Result>> tasks = List.of(
    () -> task1(),
    () -> task2(),
    () -> task3()
);

List<Future<Result>> futures = executor.invokeAll(tasks);
// 모두 완료까지 대기

for (Future<Result> future : futures) {
    Result result = future.get();   // 이미 완료됨
    process(result);
}

6.3 invokeAny

invokeAny:

  여러 Callable 중 하나만 완료되면 반환.
  - 가장 먼저 성공한 결과
  - 나머지는 취소

용도:
  - 여러 소스 중 빠른 것
  - 중복 요청

6.4 invokeAny 사용

List<Callable<Result>> tasks = List.of(
    () -> fetchFromSource1(),
    () -> fetchFromSource2(),
    () -> fetchFromSource3()
);

Result result = executor.invokeAny(tasks);
// 가장 빠른 하나의 결과
// 나머지 취소

6.5 타임아웃

// invokeAll 타임아웃
List<Future<Result>> futures = 
    executor.invokeAll(tasks, 5, TimeUnit.SECONDS);
// 5초 내 미완료는 취소

// invokeAny 타임아웃
Result result = 
    executor.invokeAny(tasks, 5, TimeUnit.SECONDS);

6.6 ILIC 의 맥락

@Service
public class InvokeMethods {
    
    private final ExecutorService executor = Executors.newFixedThreadPool(10);
    
    // invokeAll — 모든 배송 처리
    public List<Result> processAll(List<Shipment> shipments) 
            throws InterruptedException {
        List<Callable<Result>> tasks = shipments.stream()
            .map(s -> (Callable<Result>) () -> process(s))
            .toList();
        
        List<Future<Result>> futures = executor.invokeAll(tasks);
        // 모두 완료 대기
        
        return futures.stream()
            .map(this::getResult)
            .toList();
    }
    
    // invokeAny — 여러 추적 소스 중 빠른 것
    public TrackingInfo fetchTracking(String blNo) 
            throws InterruptedException, ExecutionException {
        List<Callable<TrackingInfo>> sources = List.of(
            () -> trackingApi1.fetch(blNo),
            () -> trackingApi2.fetch(blNo),
            () -> trackingApi3.fetch(blNo)
        );
        return executor.invokeAny(sources);   // 가장 빠른 것
    }
    
    private Result process(Shipment s) { return new Result(); }
    private Result getResult(Future<Result> f) {
        try { return f.get(); } catch (Exception e) { throw new RuntimeException(e); }
    }
    record Result() {}
    record TrackingInfo() {}
}

6.7 자기 점검 답변

invokeAll / invokeAny의 동작은?

:
1. invokeAll:

  • 일괄 제출
  • 모두 완료 대기
  • List
  1. invokeAny:

    • 하나만 완료되면 반환
    • 가장 빠른 결과
    • 나머지 취소
  2. 타임아웃:

    • 둘 다 지원
  3. 용도:

    • invokeAll: 일괄 처리
    • invokeAny: 빠른 소스

7️⃣ ScheduledExecutorService

7.1 ScheduledExecutorService

public interface ScheduledExecutorService extends ExecutorService {
    // 지연 실행
    ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit);
    <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit);
    
    // 고정 비율 반복
    ScheduledFuture<?> scheduleAtFixedRate(Runnable command, 
        long initialDelay, long period, TimeUnit unit);
    
    // 고정 지연 반복
    ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
        long initialDelay, long delay, TimeUnit unit);
}

7.2 지연 실행

ScheduledExecutorService scheduler = 
    Executors.newScheduledThreadPool(2);

// 5초 후 실행
scheduler.schedule(() -> {
    doWork();
}, 5, TimeUnit.SECONDS);

7.3 fixedRate vs fixedDelay

fixedRate vs fixedDelay:

scheduleAtFixedRate:
  - 시작 시점 기준 주기
  - 이전 작업 끝나기 전에도 다음
  - 작업 시간 > 주기면 밀림

scheduleWithFixedDelay:
  - 이전 작업 끝난 후 지연
  - 작업 완료 → 대기 → 다음

7.4 차이 시각화

fixedRate (주기 = 시작 간격):
  |작업1|---|작업2|---|작업3|
  0    5   10   15
  (시작 시점 5초 간격)

fixedDelay (지연 = 종료 후 간격):
  |작업1|---|작업2|---|작업3|
   완료후 5초    완료후 5초
  (완료 후 5초 간격)

7.5 코드

// fixedRate — 주기적 (5초마다 시작)
scheduler.scheduleAtFixedRate(() -> {
    collectMetrics();
}, 0, 5, TimeUnit.SECONDS);

// fixedDelay — 완료 후 지연 (완료 후 5초)
scheduler.scheduleWithFixedDelay(() -> {
    cleanup();
}, 0, 5, TimeUnit.SECONDS);

7.6 ILIC 의 맥락

@Service
public class ScheduledTasks {
    
    private final ScheduledExecutorService scheduler = 
        Executors.newScheduledThreadPool(2, r -> {
            Thread t = new Thread(r, "scheduler");
            t.setDaemon(true);
            return t;
        });
    
    @PostConstruct
    public void startScheduledTasks() {
        // 메트릭 수집 (5초마다 시작)
        scheduler.scheduleAtFixedRate(
            this::collectMetrics, 0, 5, TimeUnit.SECONDS);
        
        // 캐시 정리 (완료 후 1분)
        scheduler.scheduleWithFixedDelay(
            this::cleanupCache, 0, 60, TimeUnit.SECONDS);
        
        // 지연 실행 (10초 후 1회)
        scheduler.schedule(
            this::warmupCache, 10, TimeUnit.SECONDS);
    }
    
    @PreDestroy
    public void shutdown() {
        scheduler.shutdown();
    }
    
    private void collectMetrics() { }
    private void cleanupCache() { }
    private void warmupCache() { }
    
    // 실무: Spring @Scheduled 도 가능
}

7.7 자기 점검 답변

ScheduledExecutorService는?

:
1. 정의:

  • 예약 실행
  • ExecutorService 확장
  1. 메서드:

    • schedule (지연)
    • scheduleAtFixedRate (주기)
    • scheduleWithFixedDelay (지연)
  2. fixedRate vs fixedDelay:

    • 시작 간격 vs 완료 후 간격
  3. 용도:

    • 주기 작업, 예약

8️⃣ 제출과 실행의 분리

8.1 분리의 의미

제출과 실행의 분리:

  작업 코드 (무엇을):
    - Runnable/Callable
    - 비즈니스 로직

  실행 정책 (어떻게):
    - Executor
    - 스레드 풀, 순차, 예약

  → 독립적으로 변경

8.2 작업 코드

// 작업 코드 (무엇을 — 불변)
Callable<Result> task = () -> {
    return processShipment(shipment);
};

8.3 실행 정책 교체

// 같은 작업, 다른 실행 정책

// 1. 스레드 풀
Executor pool = Executors.newFixedThreadPool(10);
pool.execute(() -> task.call());

// 2. 단일 스레드
Executor single = Executors.newSingleThreadExecutor();
single.execute(() -> task.call());

// 3. 즉시 (현재 스레드)
Executor direct = Runnable::run;
direct.execute(() -> task.call());

// 작업은 그대로, 실행만 교체

8.4 장점

분리의 장점:

  - 작업과 실행 독립
  - 실행 정책 교체 쉬움
  - 테스트 용이 (직접 실행)
  - 관심사 분리

예:
  - 개발: 즉시 실행
  - 운영: 스레드 풀

8.5 테스트

// 테스트 — 동기 Executor
Executor syncExecutor = Runnable::run;   // 즉시 실행

@Test
void testProcess() {
    Service service = new Service(syncExecutor);   // 주입
    service.process(shipment);
    // 동기 실행 (테스트 쉬움)
}

// 운영 — 스레드 풀
Executor poolExecutor = Executors.newFixedThreadPool(10);
Service service = new Service(poolExecutor);

8.6 ILIC 의 맥락

@Service
public class SubmissionExecutionSeparation {
    
    private final Executor executor;
    
    // 생성자 주입 (실행 정책 주입)
    public SubmissionExecutionSeparation(Executor executor) {
        this.executor = executor;
    }
    
    // 작업 코드 (실행 정책 무관)
    public void process(Shipment shipment) {
        executor.execute(() -> {
            // 비즈니스 로직 (불변)
            calculateFreight(shipment);
            validateShipment(shipment);
            saveShipment(shipment);
        });
        // 실행 정책은 주입된 executor 가 결정
        // - 테스트: 동기
        // - 운영: 스레드 풀
    }
    
    private void calculateFreight(Shipment s) { }
    private void validateShipment(Shipment s) { }
    private void saveShipment(Shipment s) { }
}

8.7 자기 점검 답변

제출과 실행의 분리 의미는?

:
1. 분리:

  • 작업 (무엇을)
  • 실행 (어떻게)
  1. 작업 코드:

    • Runnable/Callable
    • 불변
  2. 실행 정책:

    • Executor
    • 교체 가능
  3. 장점:

    • 독립 변경
    • 테스트 용이

9️⃣ 면접 + 자기 점검

9.1 면접 단골 질문 매핑

Q핵심 답변
Executor?execute (실행 추상화)
ExecutorService?submit, shutdown 등
execute vs submit?void vs Future
예외 처리?핸들러 vs Future.get
Executors?팩토리 (위험)
invokeAll?모두 완료 대기
invokeAny?하나만
ScheduledExecutor?예약/반복
fixedRate vs fixedDelay?시작 vs 완료 간격
분리 의미?작업 vs 실행 정책

9.2 자기 점검 체크리스트

Executor

  • execute
  • 추상화

ExecutorService

  • submit
  • 생애주기

execute vs submit

  • 반환값
  • 예외

Executors

  • 팩토리
  • 위험

invoke

  • invokeAll
  • invokeAny

Scheduled

  • schedule
  • fixedRate/Delay

분리

  • 작업/실행
  • 장점

9.3 추가 심화 질문

Q1: submit(Runnable) 의 Future 결과?

답:

  • Future<?> 반환
  • get() 은 null (Runnable 결과 없음)
  • 완료 확인 용도
  • submit(Runnable, result) 로 결과 지정 가능

Q2: Executors 가 권장되지 않는 이유?

답:

  • newFixedThreadPool: 무제한 큐 (OOM)
  • newCachedThreadPool: 무제한 스레드 (OOM)
  • 내부 설정 숨김
  • 직접 ThreadPoolExecutor (제한) 권장

Q3: invokeAny 실패 시?

답:

  • 모두 실패하면 ExecutionException
  • 하나라도 성공하면 그 결과
  • 성공 후 나머지 인터럽트
  • 타임아웃 시 TimeoutException

Q4: ScheduledExecutor 예외?

답:

  • 반복 작업 중 예외 시
  • 해당 작업 중단 (반복 멈춤!)
  • try-catch 필수
  • 예외 안 잡으면 스케줄 죽음

Q5: CompletionService?

답:

  • 완료 순서대로 결과 회수
  • ExecutorCompletionService
  • take() 로 완료된 것부터
  • invokeAll 보다 유연

🎯 핵심 요약 — 3줄 정리

1. 계층

  • Executor (execute) → ExecutorService (submit/shutdown) → Scheduled (예약)
  • 제출과 실행 분리

2. execute vs submit

  • execute: void, 예외 핸들러
  • submit: Future, 예외 get() 시

3. 주요 기능

  • Executors 팩토리 (위험, 직접 생성 권장)
  • invokeAll/invokeAny, ScheduledExecutor (fixedRate/Delay)

📚 다음으로...

Unit 7.3 — Future와 Callable

이번 Unit에서 Executor 를 봤다면, 다음은 Future 와 Callable (결과 회수).

  • Callable (반환값 + 예외)
  • Future (비동기 결과)
  • get / cancel / isDone

Phase 7 진행 상황

🚀 Phase 7 — Executor 프레임워크 (★ 2차 정점)
  ✅ Unit 7.1 스레드 풀의 필요성
  ✅ Unit 7.2 Executor와 ExecutorService ← 여기
  ⏭ Unit 7.3 Future와 Callable
  ⏭ Unit 7.4 ThreadPoolExecutor 내부 (★ 마스터)
  ⏭ Unit 7.5 스레드 풀 종류
  ⏭ Unit 7.6 작업 큐와 거부 정책
  ⏭ Unit 7.7 스레드 풀 종료

4주차 누적 진행

✅ Phase 1~6 (25 Unit, 1차 정점 완료)
🚀 Phase 7 — Executor (2/7 진행) ★ 2차 정점

총: 27/35 Unit
profile
Software Developer

0개의 댓글