사용자가 어떤 게시글을 작성하면 조건에 맞는 다른 사용자에게 쪽지같은 알림을 구현해야 하는 상황
처음에는 하나의 transaction
으로 처리로 구현을 진행했으나 알림 기능은 부가적인 기능이고 게시글 작성 기능에 영향을 주면 안된다고 생각이 들었다. 따라서 게시글 작성 후 알림 처리가 지연되는 경우 게시글 작성 자체를 지연하는 것이 아니라, 게시글 작성은 완료시키고 다른 Thread에서 알림을 처리하도록 비동기 처리를 진행할 수 있을 것이다.
스프링에서는 @Async Annotation을 이용하여 간단하게 비동기 처리를 할 수 있다.
그전에 먼저 자바의 비동기 작업 처리를 알아보자.
따라서 method가 실행되면 새로운 thread를 만들고 그 thread에서 메시지를 저장하도록 처리하면 될 것 같다.
public class AsyncService {
public void asyncMethod(String message) throws Exception {
// do something
}
}
하지만 해당 방법은 thread를 관리할 수 없기 때문에 위험한 방법이다.
Thread를 관리하기 위해서 ExecutorService 사용해보자.
ExecutorService는 쉽게 비동기로 작업을 실행할 수 있도록 도와주는 JDK(1.5부터)에서 제공하는 interface 이다. 일반적으로 ExecutorService는 작업 할당을 위한 스레드 풀과 API를 제공한다.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class AsyncService {
private static ExecutorService executorService = Executors.newFixedThreadPool(10);
public void asyncMethod(final String message) throws Exception {
executorService.submit(new Runnable() {
@Override
public void run() {
// do something
}
});
}
}
하지만 비동기방식으로 처리하고 싶은 method마다 반복적으로 동일한 수정 작업들을 진행 해야할 것이다.
@Async
Annotation은 Spring에서 제공하는 Thread Pool을 활용하는 비동기 메소드 지원 Annotation이다.
만약 Spring Boot
에서 간단히 사용하고 싶다면, 단순히 Application
Class에 @EnableAsync
Annotation을 추가하고, 원하는 method 위에 @Async
Annotation을 붙여주면 사용할 수 있다.
@EnableAsync
@SpringBootApplication
public class SpringBootApplication {
...
}
public class AsyncService {
@Async
public void asyncMethod(String message) throws Exception {
....
}
}
하지만 @Async의 기본설정은 SimpleAsyncTaskExecutor를 사용하도록 되어있기 때문입니다.
본인의 개발 환경에 맞게 Customize하기에는 직접 AsyncConfigurerSupport
를 상속받는 Class를 작성하는 것이 좋다.
Thread pool을 이용해서 thread를 관리가능한 방식다. 아래와 같은 AsyncConfigurerSupport
를 상속받는 Customize Class를 구현하자.
그리고 Application 클래스에 @EnableAutoConfiguration(혹은 @SpringBootApplication) 설정이 되어있다면 런타임시 @Configuration가 설정된 SpringAsyncConfig 클래스의 threadPoolTaskExecutor bean 정보를 읽어들이기 때문에 앞서 설정한 Application 클래스의 @EnableAsync을 제거한다.
import java.util.concurrent.Executor;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurerSupport;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
@EnableAsync
@Configuration
public class AsyncConfig extends AsyncConfigurerSupport {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecuto();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("my thread-");
executor.initialize();
return executor;
}
}
여기서 설정한 요소들은 아래와 같다.
위와 같이 작성한 후 비동기 방식 사용을 원하는 method에 @Async
Annotation을 지정해주면 된다.
@Async annotation에 bean의 이름을 제공하면 SimpleAsyncTaskExecutor가 아닌 설정한 TaskExecutor로 thread를 관리하게 된다.
@EnableAsync
@Configuration
public class AsyncConfig extends AsyncConfigurerSupport {
@Bean(name = "threadPoolTaskExecutor")
public Executor threadPoolTaskExecutor()
{
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.setCorePoolSize(3);
taskExecutor.setMaxPoolSize(30);
taskExecutor.setQueueCapacity(10);
taskExecutor.setThreadNamePrefix("Executor-");
taskExecutor.initialize();
return taskExecutor;
}
}
---
public class AsyncService {
@Async("threadPoolTaskExecutor")
public void asyncMethod(String message) throws Exception {
// do something
}
}
Thread Pool의 종류를 여러개 설정하고자한다면 SpringAsyncConfig 안에 bean을 여러개 만들고 @Async를 설정시 원하는 bean의 이름을 설정하면 된다.
@Async
Annotation을 사용할 때 아래와 같은 사항을 주의 해야한다.
제약의 원인은 간단한데 @Async
은 AOP에 의해 동작하고 있기 때문에 해당 메서드는 프록시될 수 있어야 하기 때문이다.
출처 : https://dzone.com/articles/effective-advice-on-spring-async-part-1
해당 @Async
method를 가로챈 후, 다른 Class에서 호출이 가능해야 하므로,private
method는 사용할 수 없는 것이다. 또한 inner method의 호출은 해당 메서드를 직접호출 하기 때문에 self-invocation
이 불가능하다. @Transactional 사용시 주의점과 비슷하다고 할 수 있다.
@Slf4j
@Service
public class TestService {
@Async
public void innerAsyncMethod(int i) {
log.info("async i = " + i);
}
public void asyncMethod(int i) {
innerAsyncMethod(i);
}
}
@Async 메서드는 AsyncExecutionAspectSupport
클래스의 doSubmit 메서드에서 선택한 실행자와 함께 지정된 작업을 실제로 실행하도록 위임한다.
리턴타입은 크게 두가지로 나뉠 수 있다.
Future에 경우에도 여러 타입이 존재하는데 해당 리턴 값에 대한 것은 여기 에서 자세히 보면 좋을 것 같다
@Nullable
protected Object doSubmit(Callable<Object> task, AsyncTaskExecutor executor, Class<?> returnType) {
if (CompletableFuture.class.isAssignableFrom(returnType)) {
return CompletableFuture.supplyAsync(() -> {
try {
return task.call();
}
catch (Throwable ex) {
throw new CompletionException(ex);
}
}, executor);
}
else if (ListenableFuture.class.isAssignableFrom(returnType)) {
return ((AsyncListenableTaskExecutor) executor).submitListenable(task);
}
else if (Future.class.isAssignableFrom(returnType)) {
return executor.submit(task);
}
else {
executor.submit(task);
return null;
}
}
메서드 반환 형식이 Futre인 경우 예외 처리가 쉽다. Future.get() 메서드에서 예외가 발생하기 때문이다.
하지만 반환값이 없는 void이면 예외가 호출 스레드에 전파되지 않는다. 즉 해당 thread가 소리없이 죽기 때문에 예외 처리가 관리되지 않는다.
이러한 예외 처리를 위해서는 AsyncUncaughtExceptionHandler
인터페이스를 구현하여 사용자 지정 비동기 예외 처리기를 생성한다. handleUncaughtException()
메서드는 캐치되지 않은 비동기 예외가 있을 때 호출된다.
public class CustomAsyncExceptionHandler implements AsyncUncaughtExceptionHandler {
@Override
public void handleUncaughtException(Throwable ex, Method method, Object... params) {
log.warn("Exception message - {}", ex.getMessage());
log.warn("Method name - {}", method.getName());
}
}
추후 해당 처리를 Spring event를 이용하거나 AOP를 이용하여 리팩토링도 진행할 예정이다.