본문 바로가기
dev/spring

[Spring] @Async와 Virtual Thread

by igooo 2024. 9. 14.
728x90

개요

Java 21에 Virtual Thread와 Spring @Async 어노테이션을 조합하여 비동기 프로그래밍 방법을 알아본다.

 

Spring @Async

  • @Async 어노테이션을 Spring의 AOP를 사용하여 비동기 메서드 실행을 제공한다.
  • @Async는 기본적으로 AOP로 실행되며 Proxy Parttern의 한계점을 가진다.
    • public 메서드로만 사용가능
    • self-invocation 불가
  • 메서드 리턴 타입은 void로 설정하거나, Future / ListenableFuture / CompletableFuture로 설정하여 비동기 처리를 할 수 있다.
    • void 리턴 타압의 경우 Exception 처리를 위하여 AsyncUncaughtExceptionHandler를 사용하여 처리할 수 있다.

 

프로젝트 구조

HTTP API를 호출하여 응답하는 API 서버 프로젝트를 생성하고 API 호출하는 부분만 비동기로 처리한다.

@EnableAsync
@Configuration
class AsyncConfig {
	@Bean
	ExecutorService executorService() {
		var factory = Thread.ofVirtual().name("v-thread-", 1).uncaughtExceptionHandler((t, e) -> {
			// TODO
		}).factory();

		return Executors.newThreadPerTaskExecutor(factory);
	}

	@Bean
	AsyncTaskExecutor taskExecutor(ExecutorService executorService) {
		return new TaskExecutorAdapter(executorService);
	}

	@Bean
	TomcatProtocolHandlerCustomizer<?> protocolHandlerVirtualThreadExecutorCustomizer() {
		return protoclHandler -> protoclHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
	}
}
  • @EnableAsync 어노테이션을 추가해야 @Async 어노테이션이 비동기로 동작된다. (추가하지 않으면 일반적으로 동기로 실행됨)
  • ExecutorService는 Virtual Thread 기반으로 생성하고 v-thread-{index}로 쓰레드 이름을 설정한다.
  • AsyncTaskExecutor는 @Async 어노테이션으로 설정된 메소드가 어떤 Thread로 동작할지 설정하는 Bean이다.

 

@Service
public class ApiService {
	private final RestClient restClient;

	public ApiService(RestClient restClient) {
		this.restClient = restClient;
	}

	@Async
	public CompletableFuture<Map<String, Object>> apiCall() {
		try {
			var result = this.restClient.get().uri("/api/test").retrieve().body(Map.class);
			return CompletableFuture.completedFuture(result);
		} catch (Exception e) {
			return CompletableFuture.failedFuture(e);
		}
	}
}
  • 비동기로 실행할 메서드에는 @Async 어노테이션을 추가한다.
  • CompletableFuture를 사용하여 정상, 실패 응답에 대하여 리턴한다.

 

@RestController
public class ApiController {
	private final ApiService apService;

	public ApiController(ApiService apService) {
		this.apService = apService;
	}

	@GetMapping("/test")
	Map<String, Object> test() throws InterruptedException, ExecutionException {
		var future1 = this.apService.apiCall();
		var future2 = this.apService.apiCall();
		var future3 = this.apService.apiCall();

		var result1 = future1.get();
		var result2 = future2.get();
		var result3 = future3.get();

		return Map.of("data", (result1.get("data") + " " + result2.get("data") + " " + result3.get("data")));
	}
}
  • @Async 어노테이션이 적용된 apiCall() 메서드는 동기적으로 처리되지 않고 비동기로 처리된다.
  • apiCall() 메서드는 비동기로 처리되어  HTTP API 요청이 시작되고, 종료되기전  apiCall() 메서드는 Future 객체를 리턴한다.
  • future1.get()을 호출하면 API응답이 완료되면 완료된 객체가 리턴되고, HTTP API 응답이 완료되지 않으면 대기 후 응답이 완료되는 시점에 리턴된다.
  • 모든 응답이 완료되면 API서버는 모든 응답의 data 값을 더하여 결과로 리턴한다.

 

참고