발단
사용자가 어떤 게시글을 작성하면 조건에 맞는 다른 사용자에게 쪽지같은 알림을 구현해야 하는 상황
- 게시글 작성
- 알림
처음에는 하나의 transaction
으로 처리로 구현을 진행했으나 알림 기능은 부가적인 기능이고 게시글 작성 기능에 영향을 주면 안된다고 생각이 들었다. 따라서 게시글 작성 후 알림 처리가 지연되는 경우 게시글 작성 자체를 지연하는 것이 아니라, 게시글 작성은 완료시키고 다른 Thread에서 알림을 처리하도록 비동기 처리를 진행할 수 있을 것이다.
스프링에서는 @Async Annotation을 이용하여 간단하게 비동기 처리를 할 수 있다.
Java 비동기방식 처리
그전에 먼저 자바의 비동기 작업 처리를 알아보자.
따라서 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 with SimpleAsyncTaskExecutor
@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를 작성하는 것이 좋다.
@Async with ThreadPoolTaskExecutor
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;
}
}
여기서 설정한 요소들은 아래와 같다.
- @Configuration : Spring 설정 관련 Class로 @Component 등록되어 Scanning 될 수 있다.
- @EnableAsync : Spring method에서 비동기 기능을 사용가능하게 활성화 한다.
- CorePoolSize : 기본 실행 대기하는 Thread의 수**
- MaxPoolSize : 동시 동작하는 최대 Thread의 수
- QueueCapacity : MaxPoolSize 초과 요청에서 Thread 생성 요청시,해당 요청을 Queue에 저장하는데 이때 최대 수용 가능한 Queue의 수,Queue에 저장되어있다가 Thread에 자리가 생기면 하나씩 빠져나가 동작
- ThreadNamePrefix : 생성되는 Thread 접두사 지정
위와 같이 작성한 후 비동기 방식 사용을 원하는 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을 사용할 때 아래와 같은 사항을 주의 해야한다.
- private method는 사용 불가, public method만 사용 가능
- self-invocation(자가 호출) 불가, 즉 inner method는 사용 불가
제약의 원인은 간단한데 @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 메서드에서 선택한 실행자와 함께 지정된 작업을 실제로 실행하도록 위임한다.
리턴타입은 크게 두가지로 나뉠 수 있다.
- 리턴값이 없는 경우
- 있는 경우(Futrue)
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를 이용하여 리팩토링도 진행할 예정이다.
참고 출처
'spring' 카테고리의 다른 글
WebClient의 DataBufferLimitException 해결방법 (0) | 2024.11.15 |
---|---|
TestExecutionListener를 이용한 테스트 격리 방법 (1) | 2024.11.05 |
Spring Webclient (0) | 2024.08.02 |
Spring AOP (0) | 2024.08.02 |
Spring Boot Slice Test (0) | 2024.08.02 |