Backend/Java

[Java] ☕ Executor 프레임 워크

제이동 개발자 2025. 4. 27. 22:07
728x90

[Java] ☕ Executor 프레임 워크

Executor 프레임워크는 Java에서 스레드를 생성하고 관리하는 표준화된 방법을 제공한다.
전통적으로 우리는 new Thread()를 사용해 스레드를 하나하나 직접 만들었을 때 잘못사용하게 되면 다음과 같은 문제점들이 있었다.

  • 리소스 낭비 (새 스레드를 만들 때마다 메모리/CPU 소모)
  • 스레드 수 관리 실패 (수천 개 스레드가 생성되어 시스템 다운)
  • 코드 복잡도 증가 (어디서 누가 스레드를 돌리고 있는지 추적하기 힘듦)
  • 직접 만든 스레드는 작업이 끝나도 계속 살아있거나 제대로 종료되지 않아서 리소스를 점유한다.
  • 직접 만든 스레드에서는 예외를 잡기 어렵고, 작업 결과를 받아오기 힘듦

 

Executor 프레임워크는 이 모든 문제를 해결하기 위해 등장했다. Executor는 스레드 풀을 미리 만들어 놓고, 요청할 때마다 적절한 스레드를 배정해 작업을 수행시킨다. 

  • 스레드 풀에서 재사용하면 생성/소멸 비용이 없고, CPU 캐시 최적화도 가능해져 성능이 크게 향상
  • Thread Pool을 미리 만들어 두고, 갯수를 제한(시스템 다운 방지)
  • Executor는 스레드를 필요할 때만 생성하고, 작업이 끝나면 풀에 반환해서 재사용하거나 필요시 종료(shutdown, keepAliveTime 등)
  • ExecutorServicesubmit, invokeAll, invokeAny 같은 통일된 방식으로 작업 제출을 표준화
  • ExecutorFuture 객체를 통해 작업 결과를 받아올 수 있고, get() 호출 시 예외도 관리

 

 

1. 핵심 인터페이스

Executor 프레임 워크에서는 핵심 인터페이스로 Executor, ExecutorService, ScheduledExecutorService를 제공하며 각각의 인터페이스 분리 원칙(ISP)의 개념을 생각하며 보면 이해가 편리하다.

 

1-1. Executor

  • 인터페이스 분리 원칙에 맞게 등록된 "작업을 실행"하는 책임을 가진 인터페이스
  • 가장 기본적인 인터페이스로, execute(Runnable command) 메서드 하나만 정의

 

1-2 ExecutorService

  • 인터페이스 분리 원칙에 맞게 "작업을 등록"하는 책임과 Executor 인터페이스를 상속받아 "작업을 실행"하는 책임도 갖는 인터페이스
  • 대표 구현체 : ThreadPoolExecutor

 

주요 메서드

분류 메서드명 설명
종료 제어 void shutdown() 새 작업을 받지 않고,
현재 작업 완료 후 정상 종료
List<Runnable> shutdownNow() 실행 중 작업을 인터럽트하고,
대기 작업을 리스트로 반환
boolean isShutdown() Executor가 shutdown 상태인지 확인
boolean isTerminated() 모든 작업이 종료되었는지 확인
boolean awaitTermination(
       long timeout,
       TimeUnit unit
)
shutdown() 호출 이후,
정해진 시간 안에 Executor가 종료될 때까지 대기
작업 제출 <T> Future<T> submit(Callable<T> task) Callable 작업을 제출하고 결과를 받을 Future 반환
<T> Future<T> submit(Runnable task, T result) Runnable 작업 제출 후
지정된 결과를 가진 Future 반환
Future<?> submit(Runnable task) Runnable 작업 제출 후
완료 여부를 확인할 수 있는 Future 반환
<T> List<Future<T>> invokeAll(
        Collection<? extends Callable<T>> tasks
)
여러 Callable 작업 모두 실행 후
Future 리스트 반환
(모두 완료될 때까지 블로킹)
<T> List<Future<T>> invokeAll(
        Collection<? extends Callable<T>> tasks,
        long timeout,
        TimeUnit unit
)
제한 시간 내 여러 작업 실행 후
Future 리스트 반환
<T> T invokeAny(
        Collection<? extends Callable<T>> tasks
)
여러 작업 중
가장 먼저 끝난 작업 결과 반환
(나머지는 취소)
<T> T invokeAny(
        Collection<? extends Callable<T>> tasks,
        long timeout,
        TimeUnit unit
)
제한 시간 내
가장 먼저 끝난 작업 결과 반환
(시간 초과 시 예외)

 

각 메서드에 대한 예제 코드는 깃허브 에서 확인가능합니다.

 

1-3. ScheduledExecutorService

  • ExecutorService를 상속받은 인터페이스
  • 작업을 예약하거나 주기적으로 실행할 수 있는 기능 추가

 

 

2. 주요 구현체

2-1. ThreadPoolExecutor

  • ExecutorService의 기본이자 핵심 구현체.
  • "스레드 풀" 개념을 가장 직접적으로 구현했다.
  • 대부분의 ExecutorService는 내부적으로 ThreadPoolExecutor를 사용한다.

주요 속성

타입 속성명 설명
int corePoolSize 스레드 풀에서 관리되는 기본 스레드의 수
int maximumPoolSize 스레드 풀에서 관리되는 최대 스레드 수
long, TimeUnit keepAliveTime, unit 기본 스레드 수를 초과해서 만들어진 스레드가
생존할 수 있는 대기 시간,
이 시간 동안 처리할 작업이 없다면 초과 스레드는 제거된다.
BlockingQueue workQueue 작업을 보관할 블로킹 큐

 

 

ThreadPoolExecutor 내부에서 corePoolSize, maximumPoolSize, BlockingQueue가 어떻게 상호작용하여
"언제 core 스레드가 생성되고, 어떤 시점에 maximum 스레드가 생성되는지" 이걸 정확히 이해하는 게 정말 중요하다. 따라서 최대 스레드 수만큼 스레드가 어떻게 생성되는지 필히 이해하고 넘어가야 한다.

 

  1. corePoolSize 만큼 작업 실행
  2. corePoolSize 이상 작업이 들어오는 경우 BlockingQueue에 저장
  3. BlockingQueue 가 꽉 찼을 경우, 새로 들어온 작업들은 새로운 스레드를 생성하여 스레드풀에서 관리 및 작업 수행
    (주의할 점은 이미 큐에 저장된 작업을 새로운 스레드가 작업을 수행하는 것이 아닌 신규 요청 작업을 수행한다.)
  4. 큐가 꽉 찼고, maximumPoolSize 만큼 작업이 수행 중일 때 새로운 작업이 들어오면 RejectedExecutionException 발생

 

2-2. ScheduledThreadPoolExecutor

  • ScheduledExecutorService 인터페이스를 구현한 대표 클래스.
  • 일정 지연 후 실행하거나, 주기적으로 반복 실행할 수 있다.
  • 단순한 스레드 풀처럼 동작하면서 시간 기반 스케줄링 기능이 추가된 버전
  • schedule(), scheduleAtFixedRate(), scheduleWithFixedDelay() 같은 메서드를 제공한다.

 

2-3. ForkJoinPool

  • 병렬 분할/정복 작업(parallel divide-and-conquer)에 최적화된 스레드 풀.
  • Java 7부터 도입.
  • ForkJoinTask를 사용해서 작업을 쪼개고(fork) 결과를 합친다(join).
  • 작업을 작은 단위로 나누고, 가능한 많은 CPU 코어를 활용해 병렬로 처리한다.
  • 워크 스틸링(work-stealing) 알고리즘 사용

 

3. Executors 클래스

  • ExecutorsExecutorService 구현체를 편하게 생성할 수 있도록 해주는 팩토리 클래스.
  • 직접 ThreadPoolExecutor를 new로 만들 필요 없이, 메서드 한 줄로 다양한 풀을 만들 수 있다.

 

대표 메서드

// 고정 크기의 스레드 풀
ExecutorService executorService = Executors.newFixedThreadPool(1);
public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

// 요청마다 새 스레드를 만들고, 재사용 가능한 스레드가 있으면 재사용 (idle timeout 있음)
ExecutorService executorService = Executors.newCachedThreadPool();
public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}


// 단일 스레드 풀 (순차적 작업 처리)
ExecutorService executorService = Executors.newSingleThreadExecutor();
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
    return new AutoShutdownDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>(),
                                threadFactory));
}

// 주기적 실행이 가능한 스케줄링 스레드 풀
ExecutorService executorService = Executors.newScheduledThreadPool(5);
public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE,
          DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
          new DelayedWorkQueue());
}

 

 

 

 

728x90