도입
Executor 는 작업을 직접 스레드로 실행하는 코드와, 그 작업을 언제 어떤 스레드에서 실행할지 결정하는 실행 정책을 분리하는 Java 동시성 인터페이스다.Java에서 비동기 작업을 처리할 때 가장 단순한 방식은 new Thread(...).start() 를 직접 호출하는 것입니다. 하지만 작업이 많아질수록 스레드 생성, 재사용, 종료, 큐잉, 예외 처리, 거부 정책까지 직접 관리해야 합니다.
Executor 는 이 문제를 해결하기 위해 “작업을 제출하는 코드”와 “작업을 실행하는 방식”을 분리합니다. 호출자는 Runnable 을 넘기고, 실제로 새 스레드를 만들지, 풀에 넣을지, 호출자 스레드에서 바로 실행할지는 executor 구현이 결정합니다.
그래서 Executor를 이해한다는 것은 단순히 스레드 풀 문법을 아는 것이 아니라, Java 동시성에서 실행 정책을 어떻게 추상화하고 운영하는지 이해하는 일에 가깝습니다.
필요성
직접 스레드를 만들면 작은 예제에서는 단순해 보입니다. 하지만 실제 서비스에서는 요청마다 스레드를 계속 만들면 생성 비용이 커지고, 동시에 너무 많은 스레드가 만들어져 CPU와 메모리를 압박할 수 있습니다.
Executor는 이 문제를 실행 정책으로 다룹니다. 작업은 Runnable 또는 Callable 로 표현하고, executor는 스레드 풀, 큐, 스케줄링, 거부 정책, 종료 절차를 관리합니다.
즉 Executor의 목적은 “비동기 실행” 하나에만 있지 않습니다. 더 정확히는 작업 실행의 책임을 한 곳에 모아 스레드 자원을 통제 가능하게 만드는 데 있습니다.
- 반복적으로 많은 비동기 작업을 실행해야 할 때
- 스레드 수와 큐 크기, 거부 정책을 통제해야 할 때
- 작업 결과를
Future로 추적하거나 취소해야 할 때 - 지연 실행, 주기 실행, 병렬 작업 실행 정책을 명확히 분리하고 싶을 때
정의
Executor 는 제출된 Runnable 작업을 실행하는 객체이며, 작업 제출과 실제 실행 메커니즘을 분리하는 가장 기본적인 실행 추상화다Executor 인터페이스 자체는 매우 작습니다. 핵심 메서드는 execute(Runnable command) 하나뿐입니다.
하지만 이 작은 인터페이스가 중요한 이유는, 실행 방식이 구현체마다 달라질 수 있기 때문입니다. 어떤 executor는 호출자 스레드에서 바로 실행할 수 있고, 어떤 executor는 새 스레드를 만들 수 있고, 어떤 executor는 스레드 풀과 큐를 사용합니다.
즉 Executor 는 “반드시 비동기 실행”을 뜻하는 것이 아니라, 작업 실행 방식을 호출자 코드에서 분리하는 실행 정책 인터페이스라고 보는 편이 정확합니다.
"Executor 의 본질은 스레드 풀 그 자체가 아니라
작업 제출과 작업 실행 방식을 분리하는 실행 정책 추상화에 있습니다."
핵심 원리
Executor 를 사용하면 호출자는 “이 작업을 실행해 달라”고만 말합니다. 그 작업을 언제 실행할지, 어떤 스레드에서 실행할지, 큐에 넣을지, 거부할지는 executor 구현이 판단합니다.
ExecutorService 로 확장하면 결과 추적과 종료 관리까지 가능해집니다. submit() 은 Future 를 반환하고, shutdown(), shutdownNow(), awaitTermination() 으로 executor 생명주기를 관리할 수 있습니다.
즉 Executor 계열의 핵심은 단순 병렬 실행이 아니라, “작업”, “실행 리소스”, “결과 추적”, “종료”를 분리해서 관리하는 구조에 있습니다.
Executor Thinking
1) 작업은 Runnable 또는 Callable 로 표현한다
2) 작업 제출자는 실행 방식 자체를 알 필요가 없다
3) Executor 가 실행 정책을 담당한다
4) ExecutorService 는 Future 와 shutdown 관리를 추가한다
5) ThreadPoolExecutor 는 실제 pool / queue / rejection 정책을 구체화한다
구성 요소
| 요소 | 역할 | 실무 해석 |
|---|---|---|
| Executor | Runnable 실행 추상화 |
작업 제출과 실행 방식을 분리하는 최소 인터페이스 |
| ExecutorService | 종료 관리와 Future 기반 결과 추적 추가 |
submit, shutdown, awaitTermination 을 사용 |
| ScheduledExecutorService | 지연 실행과 주기 실행 제공 | 스케줄러나 주기 작업에 사용 |
| ThreadPoolExecutor | 스레드 풀의 실제 동작을 구체화 | pool size, queue, keepAlive, rejection 을 직접 제어 |
| Executors | Executor 생성용 팩토리/유틸리티 클래스 | 자주 쓰는 설정을 간단히 만들 수 있음 |
| Future | 비동기 작업 결과 추적 | 완료 대기, 결과 조회, 취소에 사용 |
| ThreadFactory | 스레드 생성 방식 커스터마이징 | 스레드 이름, daemon 여부, 우선순위 설정에 유용 |
| RejectedExecutionHandler | 작업 거부 정책 | 풀 종료 또는 포화 상태에서 어떻게 처리할지 결정 |
실행 생명주기
| 단계 | 역할 | 실무 해석 |
|---|---|---|
| Create | executor 생성 | 풀 크기, 큐, ThreadFactory, rejection 정책 결정 |
| Submit | 작업 제출 | execute 또는 submit 사용 |
| Queue / Run | 작업 대기 또는 실행 | core thread, queue, max thread 규칙에 따라 처리 |
| Track | 결과 추적 | Future.get(), cancel(), 상태 확인 |
| Shutdown | 종료 요청 | shutdown() 또는 shutdownNow() |
| Terminate | 완전 종료 | 실행 중/대기 중 작업이 없어지고 새 작업도 받지 않음 |
기본 구현
ExecutorService 를 만들고 작업을 제출한 뒤, 반드시 종료시키는 것이다import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class ExecutorExample {
public static void main(String[] args) throws Exception {
ExecutorService executor = Executors.newFixedThreadPool(4);
Future<Integer> future = executor.submit(() -> {
Thread.sleep(500);
return 42;
});
Integer result = future.get();
System.out.println(result);
executor.shutdown();
}
}
ExecutorService 를 종료하지 않으면 스레드와 큐 같은 자원이 계속 남아 애플리케이션 종료나 리소스 회수에 문제가 생길 수 있습니다.패턴 1. execute 와 submit 구분
execute 는 작업을 던지는 데 집중하고, submit 은 작업 결과와 예외를 Future 로 추적하는 데 집중한다execute(Runnable) 는 Executor 의 기본 메서드입니다. 결과를 반환하지 않고, 작업 실행을 executor에 위임합니다.
submit() 은 ExecutorService 에서 제공하며, Future 를 반환합니다. 이 Future 를 통해 작업 완료를 기다리거나, 결과를 가져오거나, 취소할 수 있습니다.
executor.execute(() -> {
System.out.println("fire and forget");
});
Future<String> future = executor.submit(() -> {
return "result";
});
String result = future.get();
패턴 2. ThreadPoolExecutor 설정 이해
ThreadPoolExecutor 의 실제 동작은 corePoolSize, maximumPoolSize, workQueue 의 상호작용으로 결정된다ThreadPoolExecutor 는 Executor 계열에서 가장 핵심적인 구현체 중 하나입니다. 스레드 풀 크기, 대기 큐, keep-alive 시간, 스레드 생성 방식, 거부 정책을 직접 조절할 수 있습니다.
새 작업이 들어오면 먼저 core thread 수가 부족한지 보고, 그다음 큐에 넣을 수 있는지 보고, 큐가 꽉 찼다면 maximum pool size까지 새 스레드를 만들 수 있는지 판단합니다. 그래도 처리할 수 없으면 거부 정책이 동작합니다.
import java.util.concurrent.*;
ExecutorService executor = new ThreadPoolExecutor(
4, // corePoolSize
8, // maximumPoolSize
60, TimeUnit.SECONDS, // keepAliveTime
new ArrayBlockingQueue<>(100), // workQueue
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy()
);
| 설정 | 역할 | 실무 해석 |
|---|---|---|
| corePoolSize | 기본 유지 스레드 수 | 작업이 들어오면 이 수까지는 큐보다 스레드 생성을 우선 |
| maximumPoolSize | 최대 스레드 수 | 큐가 꽉 찼을 때 추가 스레드를 만들 수 있는 상한 |
| keepAliveTime | 초과 스레드 유휴 유지 시간 | core 보다 많은 스레드가 놀 때 얼마나 기다릴지 |
| workQueue | 실행 대기 작업 큐 | 큐 종류와 크기가 처리량과 backpressure에 직접 영향 |
| ThreadFactory | 스레드 생성 전략 | 스레드 이름, daemon, priority 설정에 사용 |
| RejectedExecutionHandler | 작업 거부 정책 | 포화 상태에서 버릴지, 예외를 던질지, 호출자에게 실행시킬지 결정 |
패턴 3. 종료는 반드시 설계한다
shutdown() 은 새 작업 제출을 막고, 이미 제출된 작업은 계속 실행하게 둡니다. 반면 shutdownNow() 는 대기 중인 작업을 멈추려 하고, 실행 중인 작업에 interrupt를 시도합니다.
종료를 요청한 뒤에는 awaitTermination() 으로 일정 시간 기다리고, 필요하면 강제 종료로 전환하는 식의 패턴이 자주 사용됩니다.
void shutdownAndAwaitTermination(ExecutorService pool) {
pool.shutdown();
try {
if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {
pool.shutdownNow();
if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {
System.err.println("Executor did not terminate");
}
}
} catch (InterruptedException e) {
pool.shutdownNow();
Thread.currentThread().interrupt();
}
}
패턴 4. ScheduledExecutorService
ScheduledExecutorService 는 특정 지연 후 실행하거나, 일정 주기로 반복 실행하는 작업에 사용합니다.
scheduleAtFixedRate() 는 시작 시점 기준의 고정 주기에 가깝고, scheduleWithFixedDelay() 는 이전 실행이 끝난 뒤 일정 지연을 두고 다음 실행을 시작하는 방식입니다.
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);
scheduler.schedule(() -> {
System.out.println("run once after delay");
}, 1, TimeUnit.SECONDS);
scheduler.scheduleAtFixedRate(() -> {
System.out.println("fixed rate");
}, 0, 5, TimeUnit.SECONDS);
scheduler.scheduleWithFixedDelay(() -> {
System.out.println("fixed delay");
}, 0, 5, TimeUnit.SECONDS);
패턴 5. Virtual Thread Executor
JDK 문서는 virtual thread가 더 빠른 스레드가 아니라, 많은 수의 동시 작업이 대기하는 고처리량 서버 애플리케이션에서 scale을 제공하기 위한 스레드라고 설명합니다.
Executors.newVirtualThreadPerTaskExecutor() 는 작업이 제출될 때마다 새 virtual thread를 생성하고 실행하는 ExecutorService 를 만듭니다. I/O 대기가 많은 작업에서는 전통적인 고정 크기 플랫폼 스레드 풀과 다른 설계 선택지가 될 수 있습니다.
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
Future<String> future = executor.submit(() -> {
// blocking I/O 같은 작업
return "done";
});
System.out.println(future.get());
}
한계와 주의점
가장 흔한 문제는 무제한 큐나 무제한 스레드 증가입니다. 작업이 들어오는 속도가 처리 속도보다 계속 빠르면, 큐가 계속 쌓이거나 스레드가 과도하게 증가할 수 있습니다.
newFixedThreadPool 은 고정 스레드 수를 제공하지만 내부적으로 unbounded queue를 사용합니다. 따라서 처리 지연이 누적되면 스레드가 늘어나지는 않지만 큐가 계속 커질 수 있습니다.
newCachedThreadPool 은 필요에 따라 새 스레드를 만들고 기존 스레드를 재사용하는 구조라 단기 burst에는 편하지만, 작업 유입이 계속 많고 처리 시간이 길면 스레드 증가 위험을 의식해야 합니다.
Executor는 반드시 비동기 실행을 보장하는 인터페이스가 아님ExecutorService는 사용 후 반드시 종료해야 하는 리소스임- unbounded queue 는 지연과 메모리 사용량 증가를 숨길 수 있음
- unbounded thread growth 는 CPU와 메모리 압박을 만들 수 있음
- 작업이 blocking I/O 인지 CPU-bound 인지에 따라 풀 크기 전략이 달라져야 함
자주 하는 실수
Executor와ExecutorService를 같은 것으로 생각함execute와submit의 결과 추적 차이를 모름- 작업 완료 후
shutdown()을 호출하지 않음 newFixedThreadPool의 unbounded queue 특성을 놓침- 거부 정책 없이 큐와 풀 포화를 나중 문제로 미룸
- CPU-bound 작업과 I/O-bound 작업을 같은 풀에서 섞어 처리함
- 스레드 이름을 지정하지 않아 장애 분석 시 어떤 풀인지 알기 어려움
실무 루틴
- 먼저 작업이 CPU-bound 인지 I/O-bound 인지 구분한다.
- 작업 결과가 필요하면
submit과Future를 사용하고, 단순 실행이면execute를 고려한다. - 직접 풀을 만들 때는
corePoolSize,maximumPoolSize,workQueue를 함께 설계한다. - 큐는 가능하면 무제한보다 bounded queue 를 검토하고, 거부 정책을 명시한다.
ThreadFactory로 스레드 이름을 지정해 운영 중 식별 가능하게 만든다.- 생성한 executor 는 소유자가 명확히 종료하도록
shutdown루틴을 둔다. - active count, queue size, completed task count 같은 운영 지표를 모니터링한다.
디버깅
submit 으로 제출한 작업의 예외는 Future.get() 시점에 드러날 수 있으므로 Future 를 방치하지 않았는지 확인한다.점검 체크리스트
- executor 가 shutdown 되었는가
- active thread 수와 queue size 는 얼마인가
- 작업이 실행 중인가, 대기 중인가, 거부됐는가
- Future.get() 을 통해 예외를 확인했는가
- ThreadFactory 로 식별 가능한 스레드 이름을 부여했는가
- executor 종료 루틴이 있는가
요약
- ✅
Executor는Runnable작업 실행을 추상화하는 가장 기본 인터페이스다. - ✅
Executor는 반드시 비동기를 의미하지 않으며, 호출자 스레드에서 바로 실행하는 구현도 가능하다. - ✅
ExecutorService는 종료 관리와Future기반 결과 추적을 제공한다. - ✅
execute는 fire-and-forget 성격이고,submit은Future를 반환한다. - ✅
ThreadPoolExecutor는 pool size, queue, keepAlive, ThreadFactory, rejection policy 를 직접 다룬다. - ✅
ScheduledExecutorService는 지연 실행과 주기 실행에 사용한다. - ✅
newVirtualThreadPerTaskExecutor는 작업마다 새 virtual thread 를 시작하는 executor 를 제공한다. - ✅ 실무에서는 풀 크기보다도 큐 크기, 거부 정책, 종료 정책, 모니터링을 함께 설계해야 한다.