@Async 를 사용해서 여러 스레드로 작업을 돌리다가, 한 스레드에서 에러가 나게 되면 그 스레드만 아무 notice 없이 죽게 된다.
하지만 우리는 이런 예외 상황도 알고 싶은 경우가 많다.
결론 부터 말하면, 비동기로 처리할 메서드의 return 이 있냐 없냐에 따라 다르다.
1. return 이 없는 경우
테스트 해 볼 서비스는 아래와 같다.
@Service
class AsyncServiceImpl {
@Async
fun asyncTask(x: Int) {
for (y in listOf(10, 20, 30)) {
Thread.sleep(1000)
res += x * y
if (x * y == 90) throw IllegalArgumentException("에러 !! 에러 !!")
println("$x 작업중.... ${x * y}")
}
}
}
들어온 인수를 10, 20 ,30 각각 곱하는걸 1초 딜레이로 실행한다.
90 일 때 에러를 뱉는다.
@RestController
class TestController(
private val ayncServiceImpl: AsyncServiceImpl
) {
@GetMapping("test")
fun test() {
val sumList = mutableListOf<Future<Int>>()
for (x in 1..3) {
println("$x 시작 !!")
ayncServiceImpl.asyncTask(x)
}
}
}
이렇게 실행해 볼 것이다.
세번째 스레드의 3 * 30 일 때 한번 에러가 발생할 것이다.
에러에 대한 로그를 찍거나 하는 처리를 하고 싶다면
AsyncConfigurer 를 구현한 Configuration 클래스에서
getAsyncUncaughtExceptionHandler() 메서드에서 처리해야한다.
@Configuration
@EnableAsync
class CustAsyncConfig : AsyncConfigurer {
override fun getAsyncExecutor(): Executor =
ThreadPoolTaskExecutor().apply {
this.corePoolSize = 2
this.initialize()
}
// async 메서드의 리턴값이 없을 때 이게 동작함.
// 리턴 Future 가 있으면 동작안함.
override fun getAsyncUncaughtExceptionHandler(): AsyncUncaughtExceptionHandler {
return AsyncUncaughtExceptionHandler { ex, me, param -> println(ex.message + me + param) }
}
}
2. Future 리턴을 가지는 경우
@Service
class AsyncServiceImpl {
@Async
fun asyncTask(x: Int): Future<Int> {
var res = 0
for (y in listOf(10, 20, 30)) {
Thread.sleep(1000)
res += x * y
if (x * y == 90) throw IllegalArgumentException("에러 !! 에러 !!")
println("$x 작업중.... ${x * y}")
}
return AsyncResult(res)
}
}
이번 @Async 서비스는 리턴값을 반환한다.
Async 서비스인 만큼 Future 인터페이스의 구현체인 AsyncResult 를 반환했다.
이 때는, 리턴값이 없었던 경우와 달리 이 Future 객체에 에러 정보가 담겨서 리턴된다.
따라서 리턴값을 받는 곳에서 적절히 처리해주면 된다.
@RestController
class TestController(
private val ayncServiceImpl: AsyncServiceImpl
) {
@GetMapping("test")
fun test() {
val sumList = mutableListOf<Future<Int>>()
for (x in 1..3) {
println("$x 시작 !!")
val res = ayncServiceImpl.asyncTask(x)
sumList.add(res)
}
sumList.forEach {
try {
println(it.get())
} catch (e: Exception) {
println(e)
}
}
}
}
Future 클래스에는 get() 메서드가 있다.
비동기적 메서드가 결과를 만들어 낼 때 까지 블록시키다가, 결과가 나오면 결과값을 리턴한다.
get() 를 호출했을때 해당 메서드에서 에러가 발생했다면, catch 해서 처리해주면 된다.
@Async 메서드에서 발생할 수 있는 예외는 다음과 같다.
- CancellationException – if the computation was cancelled
- ExecutionException – if the computation threw an exception
- InterruptedException – if the current thread was interrupted while waiting
Async 메서드에서 에러가 발생하면 ExecutionException 이 발생한다.
리턴값이 Async 메서드에서는 ExecutionException 를 잡아서 처리하면 된다.
CancellationException 이 뭘까.
Future 에는 cancel() 이라는 메서드가 있다. 비동기 메서드를 처리하다가 어떤 조건에는 취소시킬 수 있다.
취소된 메서드를 get() 하게 되면 이 에러가 발생한다.
그래서 쌍으로 제공되는 isCancelled() 라는 메서드를 사용하면 예외를 피해서 처리할 수 있다.
그런데, 내가 사용한 AsyncResult 구현체는 이 isCancelled() 오버라이딩이 무조건 false 였다.
사용하지 말라는 것 같았다.
ExecutionException 으로 다 잡아서 처리하는게 일관성 있다 생각한 듯 하다.
'spring' 카테고리의 다른 글
kotlin 마스킹 (0) | 2021.11.09 |
---|---|
spring interceptor (0) | 2021.10.30 |
빈 스코프 (0) | 2021.03.21 |
스프링 빈의 생명주기와 초기화 분리 (0) | 2021.03.20 |
자동 주입시 빈이 2개 이상일 때 문제 해결 (0) | 2021.03.19 |