본문 바로가기

spring

@Async 예외처리

 

@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