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 설정으로 등록해야한다.