Spring 비동기 작업처리

뾰족머리삼돌이·2025년 1월 12일
0

Spring

목록 보기
13/14

Spring Framework의 Task Execution and Scheduling 을 부분번역한 포스팅입니다.

TaskExcutor 추상화

TaskExcutor는 비동기적인 작업실행을 목적으로 Spring에서 제공하는 인터페이스다. Executor는 작업 실행 모델로써 스레드 풀 환경일수도 있고, 단일 스레드 환경일수도 있다. Spring의 TaskExcutorjava.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 추상화를 직접적으로 사용할 수 있다.

TaskExcutor 구현체

Spring에서는 사전에 만들어진 몇 가지의 TaskExcutor 구현체를 제공한다.
따라서, 직접적으로 해당 인터페이스를 구현하기보다는 이 구현체들을 적극적으로 사용하는게 좋다.

  • SyncTaskExecutor
    구현체 명칭에서 볼 수 있듯이, 동기적으로 동작하는 구현체다.
    호출한 스레드에서 작업이 이뤄지며 간단한 테스트 케이스처럼 멀티스레드가 필요없는 상황에서 사용된다.
  • SimpleAsyncTaskExecutor
    스레드를 재사용하지 않는 특징을 가진 구현체다.
    매 실행마다 새로운 스레드를 사용하며, 동시 실행 제한을 위한 옵션(concurrencyLimit)을 제공한다.
    해당 옵션의 값을 초과한 경우, 슬롯이 확보될 때까지 작업 실행을 차단하는 기능을 지원한다.
  • ConcurrentTaskExecutor
    java.util.concurrent.Executor를 위한 어댑터로, Executor 구성 매개변수를 Bean 설정으로 노출하는 대안(ThreadPoolTaskExecutor) 이 있다. 보통 이 구현체는 직접적으로 사용되지 않으며 ThreadPoolTaskExecutor가 사용자의 요구에 불충분하다면 사용이 고려된다.
  • ThreadPoolTaskExecutor
    가장 일반적으로 사용되는 구현체로 Bean 설정으로 옵션을 노출하여 java.util.concurrent.ThreadPoolExecutor를 설정하고, TaskExcutor 타입으로 감싼다. 다른 종류의 java.util.concurrent.Executor를 필요로한다면 ConcurrentTaskExecutor를 사용하는 것이 권장된다.
  • DefaultManagedTaskExecutor
    JSR-236을 지원하는 런타임 환경에서 JNDI로 획득한 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 네임스페이스를 이용해 설정하면 된다.

TaskDecorator

대부분의 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는 비동기 작업을 설정하는 용도로 사용하면 된다.

좀 더 세밀한 제어를 하고싶다면 SchedulingConfigurerAsyncConfigurer를 구현하면 된다.

Spring Boot의 자동설정에 의해 설정되는 TaskExecutorThreadPoolTaskExecutor다.
spring.threads.virtual.enabled=true 설정으로 가상 스레드가 활성화된 경우에만 SimpleAsyncTaskExecutor가 된다.

반면, Spring Framework에서 설정되는 기본 TaskExecutorSimpleAsyncTaskExecutor인 점을 주의하자.

특정 비동기 메서드에 대한 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 설정으로 등록해야한다.

출처 및 참고

0개의 댓글

관련 채용 정보