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 값을 더하여 결과로 리턴한다.
참고
- https://spring.io/guides/gs/async-method
- https://www.baeldung.com/spring-async
- https://docs.spring.io/spring-framework/reference/integration/scheduling.html
'dev > spring' 카테고리의 다른 글
Spring AI를 사용한 AI 어시스턴트 구현 - Part 1 (with OpenAI) (1) | 2024.10.01 |
---|---|
[Spring] Rest service with Hexagonal architecture (1) | 2024.09.22 |
[Spring Batch] On K8S with Jenkins (0) | 2024.09.12 |
[Spring Boot] Virtual Threads vs Reactive vs Kotlin Coroutines 성능 비교 (0) | 2024.09.10 |
Spring Boot + vue, react 환경 구성 (1) | 2024.09.05 |