ABOUT

성능과 운영 안정성을 함께 끌어올리는 개발자입니다.

92% Positional Error Reduction
79% p95 Latency Improvement
90%+ Long Tasks Reduction

2022.02 · 한국장학재단

우수 멘티

한국장학재단 사회 리더 대학생 멘토링 IT

2022.10 · 동작구청

우수 인재상

동작구청 우수 SW 인재

2025.05 · (주) 그랩

프로그래밍 우수상

(주) 그랩 우수 프로그램 개발

2025.05 · AWSKRUG

AWS한국사용자모임 발표

AI agent 스크립트 튜닝 관련 발표

ComputerScience

Development

Engineering

Trouble Shooting

GUESTBOOK

첫 마음부터
함께 나누는 온기

방명록 작성하러 가기

SUBSCRIBE

최신소식을
편하게 만나보세요.

Executor

도입

Executor 는 작업을 직접 스레드로 실행하는 코드와, 그 작업을 언제 어떤 스레드에서 실행할지 결정하는 실행 정책을 분리하는 Java 동시성 인터페이스다.

Java에서 비동기 작업을 처리할 때 가장 단순한 방식은 new Thread(...).start() 를 직접 호출하는 것입니다. 하지만 작업이 많아질수록 스레드 생성, 재사용, 종료, 큐잉, 예외 처리, 거부 정책까지 직접 관리해야 합니다.

Executor 는 이 문제를 해결하기 위해 “작업을 제출하는 코드”와 “작업을 실행하는 방식”을 분리합니다. 호출자는 Runnable 을 넘기고, 실제로 새 스레드를 만들지, 풀에 넣을지, 호출자 스레드에서 바로 실행할지는 executor 구현이 결정합니다.

그래서 Executor를 이해한다는 것은 단순히 스레드 풀 문법을 아는 것이 아니라, Java 동시성에서 실행 정책을 어떻게 추상화하고 운영하는지 이해하는 일에 가깝습니다.

필요성

동시성 코드에서 중요한 것은 작업을 만드는 것보다, 그 작업을 얼마나 많은 스레드로 언제 실행하고 언제 종료할지 제어하는 것이다

직접 스레드를 만들면 작은 예제에서는 단순해 보입니다. 하지만 실제 서비스에서는 요청마다 스레드를 계속 만들면 생성 비용이 커지고, 동시에 너무 많은 스레드가 만들어져 CPU와 메모리를 압박할 수 있습니다.

Executor는 이 문제를 실행 정책으로 다룹니다. 작업은 Runnable 또는 Callable 로 표현하고, executor는 스레드 풀, 큐, 스케줄링, 거부 정책, 종료 절차를 관리합니다.

즉 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 계층은 Executor, ExecutorService, ScheduledExecutorService, ThreadPoolExecutor, Executors 를 구분해서 봐야 구조가 명확해진다
요소 역할 실무 해석
Executor Runnable 실행 추상화 작업 제출과 실행 방식을 분리하는 최소 인터페이스
ExecutorService 종료 관리와 Future 기반 결과 추적 추가 submit, shutdown, awaitTermination 을 사용
ScheduledExecutorService 지연 실행과 주기 실행 제공 스케줄러나 주기 작업에 사용
ThreadPoolExecutor 스레드 풀의 실제 동작을 구체화 pool size, queue, keepAlive, rejection 을 직접 제어
Executors Executor 생성용 팩토리/유틸리티 클래스 자주 쓰는 설정을 간단히 만들 수 있음
Future 비동기 작업 결과 추적 완료 대기, 결과 조회, 취소에 사용
ThreadFactory 스레드 생성 방식 커스터마이징 스레드 이름, daemon 여부, 우선순위 설정에 유용
RejectedExecutionHandler 작업 거부 정책 풀 종료 또는 포화 상태에서 어떻게 처리할지 결정

실행 생명주기

ExecutorService 를 사용한다는 것은 작업 제출뿐 아니라, 실행 중인 작업과 대기 중인 작업과 종료 절차까지 함께 관리한다는 뜻이다
단계 역할 실무 해석
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();
    }
}
실전 포인트
Executor를 만들었다면 종료도 책임져야 합니다. 사용하지 않는 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. 종료는 반드시 설계한다

ExecutorService 는 작업을 실행하는 도구이면서 동시에 종료해야 하는 리소스이므로, 생성 위치와 종료 위치를 함께 설계해야 한다

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

지연 실행이나 주기 실행이 필요하면 단순 ExecutorService 가 아니라 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

Java의 virtual thread 환경에서는 작업마다 새 virtual thread 를 시작하는 executor 를 통해, 스레드 관리와 작업 제출을 여전히 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());
}
핵심 포인트
Executor는 반드시 “고정 크기 스레드 풀”을 뜻하지 않습니다. 단일 스레드 executor, cached pool, scheduled pool, 직접 실행 executor, virtual-thread-per-task executor까지 모두 “작업 실행 정책”의 다른 구현일 뿐입니다.

한계와 주의점

Executor는 동시성 처리를 단순하게 만들어 주지만, 풀 크기와 큐 크기와 종료 정책을 잘못 잡으면 오히려 장애를 숨기거나 키울 수 있다

가장 흔한 문제는 무제한 큐나 무제한 스레드 증가입니다. 작업이 들어오는 속도가 처리 속도보다 계속 빠르면, 큐가 계속 쌓이거나 스레드가 과도하게 증가할 수 있습니다.

newFixedThreadPool 은 고정 스레드 수를 제공하지만 내부적으로 unbounded queue를 사용합니다. 따라서 처리 지연이 누적되면 스레드가 늘어나지는 않지만 큐가 계속 커질 수 있습니다.

newCachedThreadPool 은 필요에 따라 새 스레드를 만들고 기존 스레드를 재사용하는 구조라 단기 burst에는 편하지만, 작업 유입이 계속 많고 처리 시간이 길면 스레드 증가 위험을 의식해야 합니다.

주의해야 할 지점
  • Executor 는 반드시 비동기 실행을 보장하는 인터페이스가 아님
  • ExecutorService 는 사용 후 반드시 종료해야 하는 리소스임
  • unbounded queue 는 지연과 메모리 사용량 증가를 숨길 수 있음
  • unbounded thread growth 는 CPU와 메모리 압박을 만들 수 있음
  • 작업이 blocking I/O 인지 CPU-bound 인지에 따라 풀 크기 전략이 달라져야 함

자주 하는 실수

Executor를 어렵게 만드는 가장 흔한 원인은 실행 추상화를 스레드 풀 하나로만 생각하거나, 종료와 포화 상태를 설계하지 않는 데 있다
  • ExecutorExecutorService 를 같은 것으로 생각함
  • executesubmit 의 결과 추적 차이를 모름
  • 작업 완료 후 shutdown() 을 호출하지 않음
  • newFixedThreadPool 의 unbounded queue 특성을 놓침
  • 거부 정책 없이 큐와 풀 포화를 나중 문제로 미룸
  • CPU-bound 작업과 I/O-bound 작업을 같은 풀에서 섞어 처리함
  • 스레드 이름을 지정하지 않아 장애 분석 시 어떤 풀인지 알기 어려움

실무 루틴

Executor를 설계할 때는 먼저 작업 성격을 분류하고, 그다음 풀 크기·큐 크기·거부 정책·종료 정책·모니터링 항목을 함께 정하는 편이 안전하다
  1. 먼저 작업이 CPU-bound 인지 I/O-bound 인지 구분한다.
  2. 작업 결과가 필요하면 submitFuture 를 사용하고, 단순 실행이면 execute 를 고려한다.
  3. 직접 풀을 만들 때는 corePoolSize, maximumPoolSize, workQueue 를 함께 설계한다.
  4. 큐는 가능하면 무제한보다 bounded queue 를 검토하고, 거부 정책을 명시한다.
  5. ThreadFactory 로 스레드 이름을 지정해 운영 중 식별 가능하게 만든다.
  6. 생성한 executor 는 소유자가 명확히 종료하도록 shutdown 루틴을 둔다.
  7. active count, queue size, completed task count 같은 운영 지표를 모니터링한다.

디버깅

Executor 문제를 디버깅할 때는 작업 코드만 볼 것이 아니라, 풀 크기·큐 상태·거부 정책·종료 상태를 함께 확인해야 한다
1
먼저 executor 가 shutdown 상태인지, 새 작업을 받을 수 있는 상태인지 확인한다.
2
작업이 실행되지 않는다면 queue 에 쌓여 있는지, rejection 된 것인지, worker thread 가 부족한 것인지 나눠 본다.
3
지연이 크다면 작업 시간이 긴지, 큐가 긴지, 스레드 수가 부족한지, 외부 I/O 대기인지 구분한다.
4
submit 으로 제출한 작업의 예외는 Future.get() 시점에 드러날 수 있으므로 Future 를 방치하지 않았는지 확인한다.
5
애플리케이션이 종료되지 않는다면 executor 스레드가 아직 살아 있고 shutdown 되지 않았는지 확인한다.
점검 체크리스트
- executor 가 shutdown 되었는가
- active thread 수와 queue size 는 얼마인가
- 작업이 실행 중인가, 대기 중인가, 거부됐는가
- Future.get() 을 통해 예외를 확인했는가
- ThreadFactory 로 식별 가능한 스레드 이름을 부여했는가
- executor 종료 루틴이 있는가

요약

Executor의 핵심은 작업을 실행하는 코드를 스레드 생성과 스케줄링 정책에서 분리하고, ExecutorService·ThreadPoolExecutor·ScheduledExecutorService 를 통해 결과 추적, 종료, 큐잉, 스케줄링, 거부 정책까지 관리하는 데 있다
  • ExecutorRunnable 작업 실행을 추상화하는 가장 기본 인터페이스다.
  • Executor 는 반드시 비동기를 의미하지 않으며, 호출자 스레드에서 바로 실행하는 구현도 가능하다.
  • ExecutorService 는 종료 관리와 Future 기반 결과 추적을 제공한다.
  • execute 는 fire-and-forget 성격이고, submitFuture 를 반환한다.
  • ThreadPoolExecutor 는 pool size, queue, keepAlive, ThreadFactory, rejection policy 를 직접 다룬다.
  • ScheduledExecutorService 는 지연 실행과 주기 실행에 사용한다.
  • newVirtualThreadPerTaskExecutor 는 작업마다 새 virtual thread 를 시작하는 executor 를 제공한다.
  • ✅ 실무에서는 풀 크기보다도 큐 크기, 거부 정책, 종료 정책, 모니터링을 함께 설계해야 한다.

 

 

728x90