13. 스레드 풀과 Excutor 프레임워크2

임대일·2025년 5월 13일

Thread

목록 보기
13/13
post-thumbnail

[1단계] ExecutorService 우아한 종료 - 소개

문제 상황

  • 서버를 재시작해야 할 때, 진행 중인 작업이 갑자기 중단되면 문제가 발생할 수 있습니다.
  • 가장 이상적인 방식: 새로운 작업은 차단하고, 기존 진행 중인 작업은 모두 완료한 뒤 종료합니다.

개념 정리: 이런 방식의 종료를 우아한 종료 (Graceful Shutdown) 이라고 합니다.

[2단계] ExecutorService 종료 관련 메서드 정리

메서드설명
shutdown()새로운 작업은 받지 않음, 기존 작업은 완료 후 종료 (비차단)
shutdownNow()실행 중 작업에 interrupt, 큐 작업은 제외하고 즉시 종료
isShutdown()shutdown() 호출 여부 확인
isTerminated()모든 작업 종료 여부 확인
awaitTermination(timeout, unit)지정된 시간 동안 종료 대기 (차단)
close() (Java 19+)shutdown() + 무한 대기, interrupted 시 shutdownNow()

shutdown() 예시 흐름

1. 작업이 없는 경우

shutdown() 호출
→ 새로운 요청 거절
→ 스레드 풀 자원 정리
→ 완료

2. 작업이 진행 중인 경우

shutdown() 호출
→ 새로운 요청 거절
→ 기존 작업 완료 + 큐 작업도 수행
→ 완료

3. shutdownNow() 호출

→ 실행 중 작업: interrupt 시도
→ 큐에 대기 중인 작업: 제거 후 반환
→ 새로운 요청 거절

[3단계] 우아한 종료 구현 예제

static void shutdownAndAwaitTermination(ExecutorService es) {
    es.shutdown(); // 우아한 종료 시도
    try {
        if (!es.awaitTermination(10, TimeUnit.SECONDS)) {
            es.shutdownNow(); // 강제 종료
            if (!es.awaitTermination(10, TimeUnit.SECONDS)) {
                log("서비스가 종료되지 않았습니다.");
            }
        }
    } catch (InterruptedException e) {
        es.shutdownNow();
    }
}

실행 결과 핵심

작업소요 시간
taskA, taskB, taskC각각 1초
longTask100초

awaitTermination(10초) 동안 taskA~C는 완료
longTask는 인터럽트로 중단됨 → 강제 종료 성공

정리: 우아한 종료 전략

  1. 기본은 shutdown() 호출
  2. 일정 시간 대기 (awaitTermination)
  3. 시간 초과 시 shutdownNow() 호출
  4. 그래도 종료되지 않으면 → 로그를 남기고 원인 추적

[4단계] ThreadPoolExecutor 스레드 풀 관리 - 구조 이해

핵심 생성자 구성

public ThreadPoolExecutor(
    int corePoolSize,
    int maximumPoolSize,
    long keepAliveTime,
    TimeUnit unit,
    BlockingQueue<Runnable> workQueue
)
파라미터의미
corePoolSize항상 유지되는 기본 스레드 수
maximumPoolSize긴급 상황 시 생성 가능한 최대 스레드 수
keepAliveTime초과 스레드의 생존 시간
workQueue작업을 담아두는 대기 큐 (예: ArrayBlockingQueue)

[5단계] 스레드 풀 작동 순서 요약

1. 현재 스레드 수 < corePoolSize → 스레드 생성
2. corePoolSize 이상이면 → 큐에 저장
3. 큐가 가득 차면 → maximumPoolSize까지 스레드 생성
4. 스레드도 못 만들고 큐도 가득 차면 → 작업 거절

실습 코드: 다양한 작업 요청 흐름 테스트

ExecutorService es = new ThreadPoolExecutor(
    2, 4, 3000, TimeUnit.MILLISECONDS,
    new ArrayBlockingQueue<>(2)
);
  • corePoolSize = 2, maxPoolSize = 4, queue size = 2, keepAlive = 3초
  • 6개의 작업(task1 ~ task6) 제출 + 7번째(task7) 는 거절됩니다.

실행 흐름 정리

작업처리 방식
task1스레드 1 생성
task2스레드 2 생성
task3큐에 저장
task4큐에 저장
task5초과 스레드 3 생성
task6초과 스레드 4 생성
task7RejectedExecutionException 발생

[6단계] 초과 스레드 생존 시간 관리

  • 초과 스레드(task5, task6)는 3초 동안 대기 후 제거됩니다.
  • 이후 shutdown() 시 모든 스레드 종료됩니다.
poolExecutor.getPoolSize();        // 전체 스레드 수
poolExecutor.getActiveCount();     // 작업 중인 스레드 수
poolExecutor.getQueue().size();    // 대기 중인 작업 수
poolExecutor.getCompletedTaskCount(); // 완료된 작업 수

[7단계] 스레드 미리 생성하기 (응답 시간 단축 목적)

ThreadPoolExecutor poolExecutor = (ThreadPoolExecutor) es;
poolExecutor.prestartAllCoreThreads();
  • 서버 부팅 시 미리 스레드 생성 가능합니다.
  • ExecutorService에는 없음 → ThreadPoolExecutor 타입 캐스팅 필요합니다.

[8단계] ThreadPoolExecutor 전략별 구성

자바는 Executors 유틸리티 클래스를 통해 다양한 스레드 풀 구성 전략을 제공합니다.
각 전략은 내부적으로 ThreadPoolExecutor를 사용하며, 목적에 따라 다르게 설정됩니다.

1. 고정 크기 스레드 풀 (Fixed Thread Pool)

ExecutorService es = Executors.newFixedThreadPool(4);
항목
corePoolSize4
maximumPoolSize4
workQueue무한 큐 (LinkedBlockingQueue)
keepAliveTime0

고정 크기 스레드 풀 특징

  • 항상 4개의 스레드만 사용 (초과 작업은 큐에 저장)
  • 서버 자원 제어에 유리
  • CPU 바운드 작업에 적합

2. 캐시 풀 (Cached Thread Pool)

ExecutorService es = Executors.newCachedThreadPool();
항목
corePoolSize0
maximumPoolSizeInteger.MAX_VALUE
workQueueSynchronousQueue (대기 큐 없음)
keepAliveTime60초

캐시 풀 특징

  • 요청마다 새로운 스레드 생성 (단, 재사용 가능)
  • 짧고 빈번한 작업에 적합
  • 과도한 요청이 들어오면 폭발적으로 스레드 증가 가능 → 주의 필요

3. 사용자 정의 전략

ExecutorService es = new ThreadPoolExecutor(
    2, 4, 3000, TimeUnit.MILLISECONDS,
    new ArrayBlockingQueue<>(2)
);

전략 조합 가능

  • core = 2, max = 4, queue = 2
  • 유휴 초과 스레드는 3초 후 제거

실무에서는 서비스 성격에 맞게 직접 구성하는 경우도 많습니다.

각 전략 비교 요약

전략설명적합한 상황
FixedThreadPool고정된 스레드 수 + 무한 큐CPU 바운드
CachedThreadPool무제한 스레드 생성 + 즉시 실행짧고 많은 요청
사용자 정의조절 가능한 풀 구성세밀한 자원 제어가 필요한 경우

전략 선택 팁

상황추천 전략
요청 수 제한 + 예측 가능FixedThreadPool
burst(폭발)성 트래픽 대응CachedThreadPool
복잡한 조건 및 제한 제어ThreadPoolExecutor 수동 구성

[9단계] 실무 적용 시 주의사항

1. ExecutorService는 반드시 종료할 것

  • shutdown() 또는 shutdownNow()를 호출하지 않으면:
    • JVM 종료 안 됨 (데몬 아님)
    • 자원 누수 발생
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
    executor.shutdown();
}));

🔹 2. 예외 누락 방지

  • Runnable은 예외를 던지지 못하기 때문에 로그 누락 가능성 높음
  • 반드시 try-catch로 감싸거나 Callable 사용 권장
executor.submit(() -> {
    try {
        ... // 작업
    } catch (Exception e) {
        log.error("작업 중 예외", e);
    }
});

3. get()은 호출 시점 중요

  • 너무 빨리 호출하면 병렬성 무효화
  • 작업 제출 → 충분히 실행 대기 → 결과 수신 흐름 유지

4. 스레드 풀 설정은 서비스 성격 기반으로

조건설정 가이드
CPU 바운드FixedThreadPool (CPU 수와 유사)
I/O 바운드캐시 풀 또는 넉넉한 풀 크기
제한적 자원큐 크기 제한 + 거절 정책 설정

5. RejectedExecutionHandler 반드시 설정

  • 작업이 거절될 때 기본은 예외 발생 (AbortPolicy)
  • 전략에 따라 다음 중 하나 선택:
정책설명
AbortPolicy기본, 예외 발생
CallerRunsPolicy현재 호출한 스레드가 직접 실행
DiscardPolicy조용히 버림
DiscardOldestPolicy큐에서 가장 오래된 작업 제거 후 새 작업 삽입

6. 적절한 큐 선택

큐 종류특성
LinkedBlockingQueue무한 큐, 디폴트
ArrayBlockingQueue크기 지정 가능
SynchronousQueue직접 전달, 캐시 풀에 사용
PriorityBlockingQueue우선순위 지정 가능

[10단계] 실전에서의 스레드 풀 예시

웹 서버 작업 처리

ExecutorService es = new ThreadPoolExecutor(
    10, 50, 60L, TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(100),
    new ThreadPoolExecutor.CallerRunsPolicy()
);
  • core: 10
  • max: 50
  • queue: 100
  • overload 시 요청한 사용자가 직접 실행 → 서버 응답 지연은 있지만 서비스는 지속 가능

백그라운드 로그 비동기 처리

ExecutorService es = Executors.newSingleThreadExecutor();
  • 단일 스레드로 순차적으로 처리
  • 장애 시 전체 서비스 영향 최소화

전체 요약 (한 장 요약)

개념요약
ExecutorService스레드 풀 관리 핵심 인터페이스
shutdown()우아한 종료
submit()작업 제출 + Future 반환
Callable결과 반환 + 예외 처리 가능
Future작업 결과 조회 + 취소
invokeAll/Any여러 작업 처리 유틸
전략 구성Fixed / Cached / 사용자 지정
실무 주의자원 정리, 예외 처리, 큐 설정, 정책 설정 필수
profile
🤔오늘의 복습은❓

0개의 댓글