Spring Framework의 Task Execution and Scheduling 을 부분번역한 포스팅입니다.
TaskExcutor
는 비동기적인 작업실행을 목적으로 Spring에서 제공하는 인터페이스다. Executor는 작업 실행 모델로써 스레드 풀 환경일수도 있고, 단일 스레드 환경일수도 있다. Spring의 TaskExcutor
는 java.util.concurrent.Executor
인터페이스와 동일한데, 이는 본래의 목적이 스레드 풀을 사용할 때 Java 5 이전의 하위호환성을 지원하기 위함이다.
Spring은 TaskExcutor
추상화를 통해 Java SE와 Jakarta EE(구 Java EE) 환경 간 구현 세부사항을 추상화하여 개발자가 세부구현에 신경쓰지않고 작업을 처리할 수 있게 한다.
Java 5 이전에는
java.util.concurrent
패키지가 없었으므로,
하위호환성을 목적으로 Spring에서 제공하는 인터페이스가TaskExcutor
인 것이다현재에는 Java 5 미만 버전을 사용하는 경우가 거의 없으므로,
Spring이 환경과 독립적인 일관된 비동기 작업 실행 방식을 제공하려는 용도로 사용된다
@FunctionalInterface
public interface TaskExecutor extends Executor {
@Override
void execute(Runnable task);
}
해당 인터페이스는 execute
라는 단일 메서드로 구성된 함수형 인터페이스다. 해당 메서드는 스레드 풀의 동작 방식과 구성에 따라 실행할 작업을 전달받는다.
대략 정리하자면 Spring에서 멀티스레드 환경 작업을 처리할 때 이용하기 위한 추상화 인터페이스가 TaskExcutor
인 것이다.
ApplicationEventMulticaster
나 JMS의 AbstractMessageListenerContainer
, Quartz 통합 모두 해당 인터페이스의 추상화를 사용한다. 개발자의 판단에서 자신이 생성한 Bean에 스레드 풀링 동작이 필요하다면 TaskExcutor
추상화를 직접적으로 사용할 수 있다.
Spring에서는 사전에 만들어진 몇 가지의 TaskExcutor
구현체를 제공한다.
따라서, 직접적으로 해당 인터페이스를 구현하기보다는 이 구현체들을 적극적으로 사용하는게 좋다.
concurrencyLimit
)을 제공한다.java.util.concurrent.Executor
를 위한 어댑터로, Executor 구성 매개변수를 Bean 설정으로 노출하는 대안(ThreadPoolTaskExecutor
) 이 있다. 보통 이 구현체는 직접적으로 사용되지 않으며 ThreadPoolTaskExecutor
가 사용자의 요구에 불충분하다면 사용이 고려된다.java.util.concurrent.ThreadPoolExecutor
를 설정하고, TaskExcutor
타입으로 감싼다. 다른 종류의 java.util.concurrent.Executor
를 필요로한다면 ConcurrentTaskExecutor
를 사용하는 것이 권장된다.ManagedExecutorService
를 사용하며, 기존의 CommonJ WorkManager를 대체한다. 이를 통해 Jakarta EE 애플리케이션 서버에서 표준화된 방식으로 비동기 작업 실행을 지원한다.Jakarta EE는 구 Java EE를 뜻하며, 서버환경을 위한 Java 플랫폼을 의미한다.
서버 기반 애플리케이션을 개발할 때 필요한 서비스, API, 라이브러리들을 제공하며,
대규모 웹 애플리케이션이나 기업 시스템(ERP, CRM 등)을 구축할 때 사용된다.Java의 기본적인 동작이 모두 가능하기 때문에 Java SE의 확장으로 볼 수 있다.
private final TaskExecutor taskExecutor;
@Override
public void run(ApplicationArguments args) throws Exception {
System.out.println(taskExecutor.toString());
}
// org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor@...
Spring에서 기본적으로 제공하는 기능이기 때문에 Bean으로 주입받을 수 있으며,
기본설정에서 동작하는 구현체는 ThreadPoolTaskExecutor
다.
Spring Boot에서
@EnableScheduling
이 설정된 상태로TaskExecutor
를 주입받을 땐, 명시적으로 Bean을 지정해줘야한다.그 이유는
taskScheduler
의 자동설정에서 등록하는ThreadPoolTaskScheduler
가 내부적으로TaskExecutor
를 구현하기 때문이다.
스레드 관련 설정은 Java Configuration 클래스에서 설정하는 방법,
그리고 Boot환경에서 프로퍼티 파일에서 설정하는 방법이 있다.
@Bean
ThreadPoolTaskExecutor taskExecutor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.setCorePoolSize(5);
taskExecutor.setMaxPoolSize(10);
taskExecutor.setQueueCapacity(25);
return taskExecutor;
}
Java Config 파일에서는 위 형식으로 Bean 등록시에 직접 구현체 인스턴스를 생성하고 몇가지의 옵션을 설정하면 된다.
spring.task.execution.pool.max-threads=16
spring.task.execution.pool.queue-capacity=100
spring.task.execution.pool.keep-alive=10s
Boot환경에서 프로퍼티 파일을 이용해 설정할 때는 spring.task.execution
네임스페이스를 이용해 설정하면 된다.
대부분의 TaskExecutor
구현체는 TaskDecorator
로 작업 전후 동작을 설정하는게 가능하다.
public class LoggingTaskDecorator implements TaskDecorator {
private static final Log logger = LogFactory.getLog(LoggingTaskDecorator.class);
@Override
public Runnable decorate(Runnable runnable) {
return () -> {
logger.debug("Before execution of " + runnable);
runnable.run();
logger.debug("After execution of " + runnable);
};
}
}
// config 파일
@Bean
ThreadPoolTaskExecutor decoratedTaskExecutor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.setTaskDecorator(new LoggingTaskDecorator());
return taskExecutor;
}
TaskExecutor
를 Bean에 등록할 때, 위와 같은 형식으로 TaskDecorator
를 등록해야한다.
만약, 여러개의 사전작업을 따로 관리해야한다면 org.springframework.core.task.support.CompositeTaskDecorator
를 사용하면 된다.
직접적으로 TaskExecutor
를 사용하지 않아도 애노테이션을 사용해 간편하게 비동기 작업을 처리할 수도 있다.
@EnableAsync
와 @Async
애노테이션이 그 방법이다.
이는 앞선 TaskScheduler
와 함께 소개되는 경우가 많은데 @EnableScheduling
은 예약 작업을, @EnableAsync
는 비동기 작업을 설정하는 용도로 사용하면 된다.
좀 더 세밀한 제어를 하고싶다면
SchedulingConfigurer
와AsyncConfigurer
를 구현하면 된다.
Spring Boot의 자동설정에 의해 설정되는
TaskExecutor
는ThreadPoolTaskExecutor
다.
spring.threads.virtual.enabled=true
설정으로 가상 스레드가 활성화된 경우에만SimpleAsyncTaskExecutor
가 된다.반면, Spring Framework에서 설정되는 기본
TaskExecutor
는SimpleAsyncTaskExecutor
인 점을 주의하자.특정 비동기 메서드에 대한
TaskExecutor
를 설정하고 싶다면 애노테이션의value
옵션에 해당하는 Bean을 작성하면 된다.( ex :@Async("otherExecutor")
)
@Component @Slf4j
public class AsyncPrinter {
@Async
void printMessage(String message){
try{
Thread.sleep(1000L);
log.info(Thread.currentThread() + " " + message);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
@Component
@RequiredArgsConstructor
public class SampleRunner implements ApplicationRunner {
private final AsyncPrinter printer;
@Override
public void run(ApplicationArguments args) throws Exception {
for(int i = 0; i < 100; i++){
printer.printMessage(String.valueOf(i));
}
}
}
2025-01-12T15:24:05.846+09:00 INFO 30616 --- [vanilla] [task-1] com.example.vanilla.AsyncPrinter : Thread[task-1,5,main] 0
2025-01-12T15:24:05.857+09:00 INFO 30616 --- [vanilla] [task-5] com.example.vanilla.AsyncPrinter : Thread[task-5,5,main] 4
2025-01-12T15:24:05.857+09:00 INFO 30616 --- [vanilla] [task-2] com.example.vanilla.AsyncPrinter : Thread[task-2,5,main] 1
2025-01-12T15:24:05.857+09:00 INFO 30616 --- [vanilla] [task-8] com.example.vanilla.AsyncPrinter : Thread[task-8,5,main] 7
2025-01-12T15:24:05.857+09:00 INFO 30616 --- [vanilla] [task-7] com.example.vanilla.AsyncPrinter : Thread[task-7,5,main] 6
2025-01-12T15:24:05.857+09:00 INFO 30616 --- [vanilla] [task-3] com.example.vanilla.AsyncPrinter : Thread[task-3,5,main] 2
2025-01-12T15:24:05.857+09:00 INFO 30616 --- [vanilla] [task-4] com.example.vanilla.AsyncPrinter : Thread[task-4,5,main] 3
...
위 예시처럼 @Async
가 설정된 비동기 작업 메서드를 100회 실행시켰을 때, 서로 다른 스레드에서 각 작업들이 동시에 일어나는 것을 확인할 수 있다.
예시에서도 확인할 수 있듯이, @Async
메서드는 개발자가 코드를 통해 직접적으로 호출해야하기 때문에 @Scheduled
와 다르게 매개변수를 받을 수 있다. 또한 Future<V>
로 감싸는 경우에 한하여 리턴값을 가지는 것도 가능하다.
public class SampleBeanImpl implements SampleBean {
@Async
void doSomething() {
// ...
}
}
public class SampleBeanInitializer {
private final SampleBean bean;
public SampleBeanInitializer(SampleBean bean) {
this.bean = bean;
}
@PostConstruct
public void initialize() {
bean.doSomething();
}
}
주요 특징으로, @PostConstruct
와 같은 라이프사이클 콜백 메서드에는 @Async
를 적용할 수 없다.
Bean 생성자체를 비동기적으로 실행하고 싶다면 위의 예시코드처럼 별도의 초기화 Spring Bean에 비동기 메서드를 작성 및 호출해야한다.
비동기 작업메서드가
Future
타입의 반환값을 가지는 경우,get()
을 호출 할 때 예외가 발생한다.
하지만, 반환값이 없는 경우에는 예외 처리를 위해 별도의 핸들러를 제공해야한다.이 경우,
AsyncUncaughtExceptionHandler
를 구현하는 것으로 핸들러를 작성하면 되며,AsyncConfigurer
나 xml 설정으로 등록해야한다.