본문 바로가기
Spring

@Async 비동기 메서드와 No thread-bound request found 오류

by PROMISE_YOO 2023. 5. 30.

문제 양상

java.lang.IllegalStateException 이 발생했다.
한 줄기 희망인 Exception log 를 살펴보았다.

Exception log 내용
 Unexpected exception occurred invoking async method: publicNo thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request.

해석
thread 내에서 requst 를 찾을 수 없습니다. 실제 웹 요청 외부의 요청 속성을 참조하거나 원래 수신 thread 외부에서 요청을 처리하고 있습니까? 실제로 웹 요청 내에서 작업 중인데도 여전히 이 메시지를 수신하는 경우 코드가 DispatcherServlet 외부에서 실행 중일 수 있습니다. 이 경우 현재 request 를 노출하려면 RequestContextListener 또는 RequestContextFilter를 사용하세요.

 

정리해보면 "해당 thread 에서 request 를 찾을 수 없다!"

그리고 친절하게 어디서 Exception 이 발생했는지 알려줬고 Exception 이 발생한 메서드를 살펴보았다.


문제 원인

- Exception 이 발생한 메서드는 Push 알림을 전송하는 메서드로 @Async(비동기처리) 어노테이션을 사용한다.

 

- 해당 메서드의 내부 로직에서 호출하는 메서드 중 하나가 HttpServletRequest 에서 locale 정보를 가져오는 request.getLocale() 메서드를 호출하고 있었다. 

 

- 처음 API 요청이 발생한 thread (설명을 위한 임의 명칭 : thread-1)에 Requset 정보를 저장하고 있는데... 해당 thead (thread-1)가 아닌 새로운 thread (thread-2)에서 request 정보를 가져오라고 하니 오류가 발생한 것이다. 

 

문제 해결

방법1

@Async(비동기처리) 어노테이션을 사용하는 Push 알림 전송 메서드 내에 있던 request.getLocale() 을 사용하는 메서드를 빼내서 결과값을 파라미터로 넘겨준다. 다행히 Push 알림 전송 메서드를 호출하는 부분이 다섯곳 미만이였다. 그리고 로직 변경이 용이해서 해당 방법을 적용하였다.

 

방법2

혹시나 같은 Exception 이지만 위의 방법으로는 해결이 불가능 할때를 대비해 RequestContextHolder 도 사용해보기로 했다.

 

RequestContextHolder는 기존 Servlet, Interceptor, Controller 정도에서만 접근 가능한 HttpServletRequest에 대해 Service, Component, DAO 등 전 구간에서 접근하도록 도와주는 유틸성 클래스이다. 다만 그 범위를 ThreadLocal로 관리하기 때문에 추가 설정이 없이는 다른 쓰레드에서 해당 정보를 이용할 수 없다.

 

참고) ThreadLocal : 하나의 쓰레드에서 읽고 쓸 수 있는 지역변수 thread-local을 관리하는 클래스.

 

내 경우에는 새로운 Thread 에서 Request 를 사용해야했으므로 추가 옵션을 셋팅해야했다.

바로 setThreadContextInheritable 옵션이다. 해당 옵션 값을 true 로 설정하면 LocaleContext 및 RequestAttributes 를 새로 생성한 Thread 에서도 사용할 수 있다.

public void setThreadContextInheritable(boolean threadContextInheritable)

LocaleContext 및 RequestAttributes를 자식 스레드에 대해 상속 가능하게 하여 노출할지 여부를 설정할 수 있다.( InheritableThreadLocal 사용).
생성된 백그라운드 스레드에 대한 부작용을 방지하기 위한 기본값은 "false"입니다. 요청 처리 중에 생성되고 이 요청에만 사용되는(즉, 스레드를 재사용하지 않고 초기 작업 후에 종료되는) 사용자 지정 자식 스레드에 대한 상속을 활성화하려면 "true"로 전환하십시오.

경고: 필요할때 새 스레드(예: JDK)를 잠재적으로 추가하도록 구성된 스레드 풀에 액세스하는 경우 하위 스레드에 상속을 사용하지 마십시오. ThreadPoolExecutor 상속된 컨텍스트가 풀링된 스레드에 노출되기 때문입니다.


1. RequestContextHolder 을 사용하기 위해 Application Class 에 interface ServletContextInitializer 구현했고 onStartup 메서드를 override 했다. 중요 포인트인 dispatcherServlet.setThreadContextInheritable(true) 로 셋팅했다.

package com.project.sample;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRegistration;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletContextInitializer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;

@EnableAsync
@SpringBootApplication
public class SampleApplication implements ServletContextInitializer {
	@Override
	public void onStartup(ServletContext servletContext) throws ServletException {
		AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext();
		DispatcherServlet dispatcherServlet = new DispatcherServlet(applicationContext);
		dispatcherServlet.setThreadContextInheritable(true);

		ServletRegistration.Dynamic dispatcher = servletContext.addServlet("dispatcherServlet", dispatcherServlet);
		dispatcher.setLoadOnStartup(1);
		dispatcher.addMapping("/");
	}

	public static void main(String[] args) {
		SpringApplication.run(SampleApplication.class, args);
	}


}
 

 

 

2. RequestContextHolder 로부터 Request 를 가져와서 필요한 정보를 사용했다.

 내 경우에는 Request 의 Locale 정보가 필요했다. @Async(비동기처리) 어노테이션을 사용하는 메서드 내부에서 Request 를 사용하는 호출부에 아래와 같이 코드를 추가하여 새로운 Thead 에서도 Request를 가져올 수 있도록 했다.

ServletRequestAttributes attributes = ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes());
HttpServletRequest request = attributes.getRequest();
log.info("Request Locale : " + RequestContextUtils.getLocale(request));

 

수정을 완료하고 console log 를 캡쳐했다.

처음 요청을 받고 처리하는 Thread 는 http-nio-80-exec-2 라는 걸 확인 할 수 있다. 해당 Thread 에 request 정보가 저장되어있다.

만일 HttpServletRequest 으로  request.getLocale() 호출하면 위의 문제가 발생하게 된다.

하지만 DispatcherServlet셋팅을 완료하였으니 RequestContextHolder 에서 Request 사용하니 다른 Thread(task-7) 임에도 문제없이 Locale 정보를 가져온 것을 확인하였다. 

 

 

참고 

https://gompangs.tistory.com/entry/Spring-RequestContextHolder

 

Spring RequestContextHolder

RequestContextHolder 개요 RequestContextHolder 는 Spring에서 전역으로 Request에 대한 정보를 가져오고자 할 때 사용하는 유틸성 클래스이다. 주로, Controller가 아닌 Business Layer 등에서 Request 객체를 참고하려

gompangs.tistory.com

https://brunch.co.kr/@springboot/401

 

Spring Boot @Async 어떻게 동작하는가?

스프링부트 환경에서, @Async 어노테이션 사용해서 비동기 메서드 구현 | 이 글에서는, 스프링 프레임워크에서 제공하는 @Aysnc 비동기 메서드 사용 방법에 대해서 설명한다. 이 글을 읽기 위해서

brunch.co.kr