문제 상황

최근에 db connection timeout 상황이 많이 나왔고, 오래걸리는 로직을 서버에서 자체적으로 종료시키자는 논의가 나왔습니다. 추가로 10초 이상 걸리면 프론트로 시스템 오류 메시지를 던지도록 했습니다.

해결 방법

우선적으로 ELB 의 세션타임아웃을 고민해봤으나, 타임아웃이 발생한다고 하더라도 서버 내부에서는 로직이 계속 실행된다는 문제가 있습니다. 원하는 흐름은 단순히 “연결종료” 뿐만 아니라 “실행종료” 이기 때문입니다.

따라서 쓰레드가 로직을 실행할 때, 그 쓰레드의 실행시간을 측정해야 합니다. 그러기 위해서는 실행 쓰레드를 분리할 수밖에 없다고 생각했습니다. 그래서 Future 를 통해 내부에 사용되는 스레드를 구분해주었습니다.

스레드 구분은 AOP 레벨에서 *Controller 클래스를 호출할 때 발생되는데, 그러면 Filter, Interceptor 에서 넣었던 ThreadLocal 의 정보가 전파되지 않습니다. 또한 OSIV 트랜잭션도 전파되지 않기 때문에 두 부분도 함께 전달해야 합니다. 이러한 부분은 직접 넣어주는 방식으로 구현했는데, 이러한 구현으로 성능 상으로 떨어지거나 레이턴시가 증가하는 경향이 있는지는 나중에 로드 테스트로 확인해봐야 합니다.

코드 레벨

@Aspect
@Component
class TimeoutAop(
  	//1
    @Qualifier("futureTaskExecutor")
    private val futureTaskExecutor: ThreadPoolTaskExecutor,
  	//2
    private val transactionTemplate: TransactionTemplate

) {

    @Pointcut("execution(* kr.monthler.api.controller.*.*(..))")
    fun controllerPointcut() {}

    @Around("controllerPointcut()")
    fun timeoutAdvice(pjp: ProceedingJoinPoint): Any? {
      
      	//3
        val callable = Callable {
            transactionTemplate.execute {
                pjp.proceed()
            }
        }

      	//4
        val contextAwareCallable = ContextAwareCallable(callable)
        val future: Future<Any> = futureTaskExecutor.submit(contextAwareCallable)

        return try {
          	//5
            future.get(10000, TimeUnit.MILLISECONDS)
        } catch (e: TimeoutException) {
          	//6
            future.cancel(true)
             //7
            throw BaseException(MESSAGES.RESPONSE_TIMEOUT)
        }
    }
}

class ContextAwareCallable<V>(private val delegate: Callable<V>) : Callable<V> {

    private val requestAttributes: RequestAttributes? = RequestContextHolder.getRequestAttributes()
    private val securityContext = SecurityContextHolder.getContext()

    override fun call(): V {
        RequestContextHolder.setRequestAttributes(requestAttributes)
        SecurityContextHolder.setContext(securityContext)
        return delegate.call()
    }
}
  1. futureTaskExecutor 는 내부에서 실행될 스레드풀로, 최대 200개입니다.
  2. transactionTemplate 은 트랙잭션을 전파하기 위해 사용합니다.
  3. pjp.proceed() 를 실행할 Callable 입니다.
  4. ContextAwareCallable 클래스로 감싸서 필요한 정보를 주입합니다. 이 프로젝트에서는 RequestAttributes 와 SecurityContextHolder 가 스레드로컬로 필요합니다.
  5. future.get 으로 스레드를 실행합니다. 10초를 타임아웃으로 둡니다.
  6. TimeoutException 이 발생하면 future 을 취소합니다.
  7. 미리 정의된 BaseException(MESSAGES.RESPONSE_TIMEOUT) 을 던집니다.

이렇게 해서 내부에서 10초 이상 걸리면 자동으로 실행을 종료하고 에러를 반환합니다. 다만 스레드가 2배로 사용된다는 점이 마음에 걸리는데, 로드 테스트와 함께 나중에 개선포인트로 생각해야 겠습니다.

태그:

카테고리:

업데이트:

댓글남기기