스프링에서 비동기 프로그래밍을 구현하는 방식 중 하나다.
@Async 어노테이션이 붙은 메서드가 별도의 스레드에서 실행되므로 메인 스레드에서 캐치를 할 수 없다.
예외를 처리하기 위해서는 AsyncUncaughtExceptionHandler를 사용하여 예외를 적절히 처리해야 한다.
@Configuration
@EnableAsync
public class AsyncConfig extends AsyncConfigurerSupport {
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new MyAsyncUncaughtExceptionHandler();
}
}
public class MyAsyncUncaughtExceptionHandler implements AsyncUncaughtExceptionHandler {
@Override
public void handleUncaughtException(Throwable throwable, Method method, Object... obj) {
System.out.println("Exception message - " + throwable.getMessage());
System.out.println("Method name - " + method.getName());
for (Object param : obj) {
System.out.println("Parameter value - " + param);
}
}
}
@Async 어노테이션은 프록시 기반으로 동작하기 때문에 같은 클래스 내부에서 직접 호출하면 별도에 스레드에서 실행되지 않고 동기적으로 실행된다.
@Transactional이랑 같은 현상.
이를 해결하려면 self reference를 사용해야 하거나 비동기 처리하는 로직을 별도의 클래스로 빼서 호출해야 한다.
@Service
public class SelfInvocationService {
@Autowired
private SelfInvocationService self;
public void method() {
self.asyncMethod();
}
@Async
public void asyncMethod() {
}
}
@Async가 붙은 메서드는 void, Future, CompletableFuture중 하나를 반환해야 한다.
Future를 반환하면 비동기 작업 결과를 반환하고 작업 완료를 대기할 수 있다.
하지만 Future나 CompletableFuture를 반환 받아도 완료 값이 없을 수 있다.
@Service
public class xxxService {
@Autowired
private AsyncService asyncService;
public void method() {
Future<String> future = asyncService.asyncMethodWithReturnType();
String result = future.get();
System.out.println(result);
}
}
이럴 땐 future.get()을 호출해서 비동기처리한 결과값이 담길 때 까지 기다릴 수 있다.
@Async 메서드는 호출한 메서드와 독립적인 스레드에서 동작한다.
따라서 비동기 메서드 내에서 생성된 트랜잭션은 호출한 메서드의 트랜잭션과 별도의 생명주기를 가진다.
따라서 비동기 메서드 내에 예외가 발생해서 트랜잭션이 롤백 되어야 할 때에도 독립적인 스레드 트랜잭션 에서만 처리되고 원래 호출 한 스레드의 트랜잭션에는 영향이 미치지 않는다.
그냥 따로 트랜잭션이 생성된다고 보는게 쉬울 것 같다.
스프링에서 비동기 작업을 하고 싶다면 @EnableAsync, @Async를 사용해야한다.
하지만 기본으로 설정하면 SimpleAsyncTaskExecutor가 사용된다.
SimpleAsyncTaskExecutor는 비동기 작업마다 새로운 스레드를 생성하는 스레드풀이다. 이로인해 리소스 낭비, 성능 저하, 스케일링 문제 등이 발생할 수 있다.
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean(name = "threadPoolTaskExecutor")
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(500);
executor.setThreadNamePrefix("Executor-");
executor.initialize();
return executor;
}
}
이런식으로 빈을 등록하면 미리 만들어진 스레드 풀에서 스레드를 사용한다.
스레드를 안만들고 재사용하니 성능에도 좋고 정해진 트래픽을 받기 때문에 리소스 낭비도 없다.