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 등)
ExecutorService
는 submit, invokeAll, invokeAny 같은 통일된 방식으로 작업 제출을 표준화Executor
는 Future 객체를 통해 작업 결과를 받아올 수 있고, 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 스레드가 생성되는지" 이걸 정확히 이해하는 게 정말 중요하다. 따라서 최대 스레드 수만큼 스레드가 어떻게 생성되는지 필히 이해하고 넘어가야 한다.
- corePoolSize 만큼 작업 실행
- corePoolSize 이상 작업이 들어오는 경우
BlockingQueue
에 저장 - BlockingQueue 가 꽉 찼을 경우, 새로 들어온 작업들은 새로운 스레드를 생성하여 스레드풀에서 관리 및 작업 수행
(주의할 점은 이미 큐에 저장된 작업을 새로운 스레드가 작업을 수행하는 것이 아닌 신규 요청 작업을 수행한다.) - 큐가 꽉 찼고, 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 클래스
Executors
는ExecutorService
구현체를 편하게 생성할 수 있도록 해주는 팩토리 클래스.- 직접
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