AWS SQS 사용하다 만난 녀석(No thread-bound request found)

Bobby·2023년 2월 9일
0

오답노트

목록 보기
2/6
post-thumbnail

🤬 사건

SQS를 사용하는 작업이 있다.

SQS 리스너에서 메시지를 폴링하는 순간 다음과 같은 에러를 만났다.

No 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.

문제 발생 구간은 AOP를 이용해 로깅을 하는 구간이다.
HttpServletRequest을 가져와 요청에 대한 url, 시간, 파라미터 등등을 찍는 곳이다.

public Object doLogging(ProceedingJoinPoint pjp) throws Throwable {
    HttpServletRequest request =
                ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();

❗️ 잠깐 상식
AOP에서 HttpServletRequest 가져오는 방법은 두가지가 있다.
1. RequestContextHolder.currentRequestAttributes()
2. RequestContextHolder.getRequestAttributes()
둘의 차이는 값이 없을 경우 1번은 Exception 발생 2번의 경우는 null을 리턴한다.

저기서 값이 없어서 에러가 발생했다.


🤯 전개

현재 sqs 리스너 설정은 다음과 같다.

  • sqs 리스너는 비동기로 동작한다.
  • 한번 폴링에 최대 10개 가져온다.
  • 쓰레드풀은 10개 이다.
@Bean
public SimpleMessageListenerContainerFactory simpleMessageListenerContainerFactory(AmazonSQSAsync amazonSQSAsync) {
    SimpleMessageListenerContainerFactory factory = new SimpleMessageListenerContainerFactory();
    factory.setAmazonSqs(amazonSQSAsync);  
    factory.setMaxNumberOfMessages(10);    
    factory.setWaitTimeOut(10);            
    factory.setTaskExecutor(messageThreadPoolTaskExecutor());  
    return factory;
}

@Bean
public ThreadPoolTaskExecutor messageThreadPoolTaskExecutor() {
    ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
    taskExecutor.setThreadNamePrefix("sqs-task-");
    taskExecutor.setCorePoolSize(10);
    taskExecutor.setMaxPoolSize(10);
    taskExecutor.afterPropertiesSet();
    return taskExecutor;
}

구글과 chatGPT 에게 물어봤다.

❗️ 요청이 들어오면 RequestContextHolder는 ThreadLocal에 HttpServletRequest를 저장한다.
❗️ 비동기 처리를 할 경우 다른 쓰레드에서 동작하기 때문에 HttpServletRequest를 가지고 있지 않다.

그래서 없는 HttpServletRequest를 가져오려고 하니 에러가 발생한 것이다.


😅 뻘짓

1. RequestContextHolder를 생성해주기 위해 RequestContextListener 빈으로 등록

@Bean 
public RequestContextListener requestContextListener(){
    return new RequestContextListener();
} 

❌ 실패..

2. 쓰레드를 분리할 때 dispatcherServlet에서 하위 쓰레드에게 컨텍스트 상속

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

    @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("/");
    }
}

❌ 실패..

3. sqs 리스너가 사용하는 쓰레드 풀에 들어온 RequestContextHolder를 복사

커스텀 데코레이터 생성.

public class CustomDecorator implements TaskDecorator {

    @Override
    public Runnable decorate(Runnable runnable) {
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();

        return() -> {
            RequestContextHolder.setRequestAttributes( requestAttributes );
            runnable.run();
        };
    }
}

TaskExecutor에 데코레이터 적용

@Bean
public ThreadPoolTaskExecutor messageThreadPoolTaskExecutor() {
    ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
    taskExecutor.setThreadNamePrefix("sqs-task-");
    taskExecutor.setCorePoolSize(10);
    taskExecutor.setMaxPoolSize(10);
    taskExecutor.afterPropertiesSet();
    taskExecutor.setTaskDecorator(new CustomDecorator());
    return taskExecutor;
}

❌ 실패..


😇 해결

처음부터 비동기 요청일 때 HttpServletRequest를 어떻게 가져올까? 라는 생각만 하고 있었다.
그런데 이 문제는 전혀 다른 문제다.

애초에 HttpServletRequest는 나의 서버로 '요청'이 들어왔을 때 생기는 객체이다.

그런데 SQS 리스너는?
서버로 요청이 들어오는 것이 아니고 일정 시간마다 폴링 해서 데이터를 가져오는 형태다.

그렇다면..? HttpServletRequest는 원래 없다.. 그냥 없다!
전달해 주고 말고 할 것이 아니었다..

그리고 SQS리스너가 가져오는 데이터는 AOP Logger의 목적과 전혀 관계가 없다.

  • 현재 프로젝트의 AOP Logger는 controller 패키지 하위에 있는 객체들의 실행에 동작하게 되어있다.
@Pointcut("within(com.example.controller..*)")

👏 결론적으로 controller 패키지 아래에 SQS 리스너가 있으면 안되는 것이었다.

👏 SQS리스너를 listner 패키지 아래에 두고 로거가 필요하다면 ListnerAOPLogger 클래스를 만들어서 분리 할 예정이다.


아...나의 3시간..

profile
물흐르듯 개발하다 대박나기

0개의 댓글