Technology/Spring

No thread-bound request found 에러의 원인은?

ikjo 2024. 1. 17. 04:14

발생 이슈

gRPC(google Remote Procedure Call) 를 통해 요청을 받은 후 spring-data-jpa 에서 제공하는 SimpleJpaRepository 의 save 메서드를 호출하는 시점에 아래와 같은 에러가 발생했다.

 

java.lang.IllegalStateException: 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

 

이미 여러 기술 블로그에서 각가지의 이유로 해당 에러가 발생한 이슈에 대해 다룬 글들이 많이 있었으나, 나의 상황에서는 직접적인 도움이 되지 못했다. 😂 다행히, 로컬 환경에서 기능 구현 시 발생했던 건이었다. 🙃

 

 

원인 분석

표면적인 원인은 위와 같이 장문의 메시지를 담은 IllegalStateException 예외를 보면 대략 알 수 있다. 그렇다면 해당 예외는 어디서 발생했던 것일까? 바로 스프링에서 제공하는 RequestContextHolder 클래스였다. 이때, RequestContextHolder 는 스레드별로 할당된 RequestAttributes 타입의 객체 형태로 web request 를 노출시키는 Holder 클래스이다.

 

spring 에서 제공하는 RequestContextHolder 클래스

 

좀 더 구체적으로는 RequestContextHolder 클래스 내 정적 메서드인 currentRequestAttributes 에서 해당 예외가 발생했다. 해당 메서드의 구현 코드를 살펴보면 다음과 같다.

 

  public static RequestAttributes currentRequestAttributes() throws IllegalStateException {
   RequestAttributes attributes = getRequestAttributes();
   if (attributes == null) {
    if (jsfPresent) {
     attributes = FacesRequestAttributesFactory.getFacesRequestAttributes();
    }
    if (attributes == null) {
     throw new IllegalStateException("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.");
    }
   }
   return attributes;
  }

 

앞서 잠시 언급했던 RequestAttributes 타입의 객체를 조회하고 있으며, 로직을 따라가다 해당 객체가 최종적으로 null 일 경우, 본 이슈에서의 예외가 발생하게 된다. 참고로 해당 RequestAttributes 타입의 객체는 ThreadLocal 에서 관리된다.

 

그렇다면, 내 상황에서는 왜  RequestAttributes 타입의 객체가 null 이었던 걸까? 그리고 어디서 RequestContextHolder 클래스의 currentRequestAttributes 메서드를 호출하는 것일까?

 

디버깅을 통해 해당 예외가 발생하기 이전의 call stack 을 살펴보다가 currentRequestAttributes 메서드를 호출하는 지점을 발견할 수 있었다. 해당 지점은 spring-data-jpa 에 포함되어 제공되는 AuditorAware 인터페이스를 구현한 클래스로서,

다른 팀원께서 구현하셨던 코드였다. 먼저, AuditorAware 인터페이스의 스펙은 아래와 같다.

 

package org.springframework.data.domain;

import java.util.Optional;

public interface AuditorAware<T> {

  Optional<T> getCurrentAuditor();
}

 

참고로, AuditorAware 는 스프링(spring-data-common)에서 제공하는 Auditing 기능 중 하나인데, AuditorAware 인터페이스를 구현한 후 이를 스프링 빈으로 등록하여 사용할 수 있다. Auditing 기능을 사용 시 AuditingEntityListener 는 새로운 엔티티가 생성(영속화)되거나 기존 엔티티가 수정되는 경우를 감지한다. 이후, AuditingHandler 가 동작하여 앞서 스프링 빈으로 등록된 AuditorAware 의 getCurrentAuditor 메서드가 호출되고 반환된 값은 앞서 @CreatedBy 또는 @LastModifiedBy 애노테이션이 선언된 필드에 리플렉션을 통해 주입되게 된다.

 

아래 코드는 SpringSecurity 에서 AuditorAware 인터페이스를 구현한 예시이다.

 

class SpringSecurityAuditorAware implements AuditorAware<User> {

  public User getCurrentAuditor() {

    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

    if (authentication == null || !authentication.isAuthenticated()) {
      return null;
    }

    return ((MyUserDetails) authentication.getPrincipal()).getUser();
  }
}

 

다시 본론으로 돌아와서, 다른 팀원께서 AuditorAware 인터페이스를 구현했었기에, SimpleJpaRepository 의 save 메서드를 호출하는 시점에 AuditorAware 인터페이스의 getCurrentAuditor 메서드가 호출되었던 것이다. 그리고 이 메서드의 로직 중 RequestContextHolder 클래스의 currentRequestAttributes 메서드를 호출하면서 본 예외(No thread-bound request found)가 발생했던 것이다.

 

그렇다면 해당 예외의 원인이 되었던 RequestAttributes 타입의 객체가 null 이었던 이유는 무엇이었을까?

 

그 이유는 해당 요청은 web 에 의한 것이 아니라, gRPC 에 의한 것이기 때문이었다. 앞서 RequestContextHolder 클래스는 web request 정보를 노출시키는 Holder 클래스라고 했다. 즉, 여기서의 RequestAttributes 타입의 객체는 web request 정보를 지니는 객체인 것이다. 그런데, gRPC 로 요청을 받아 처리했으니 해당 스레드에서는 웹 기술(서블릿 컨테이너, 디스패처 서블릿 등)의 도움을 받지 못해 web request 정보를 지니는 RequestAttributes 객체를 지니지 못했던 것이다.

 

 

결론

해당 이슈의 원인을 찾았으니, 이에 대한 해결 방법은 간단하다. 해당 작업을 gRPC 로 요청을 받아서 처리하지 않고 web 으로 요청을 받아서 처리하면 된다. 다만, 이는 표면적인 해결 방법일 수도 있다. 왜냐하면 현재 우리 팀 서비스의 경우, MSA 로 운영되고 있기에 gRPC 를 통한 통신이 잦은데, AuditorAware 인터페이스를 구현함으로써, gRPC 를 통한 엔티티 생성 또는 수정 작업에 제약이 생겼다고 볼 수 있기 때문이다. 근본적으로 AuditorAware 구현 시 web 에 의존적인  RequestContextHolder 를 통한 RequestAttributes 에 접근하는게 좋은 패턴인지 또는 gRPC 를 통해 데이터 조회 외 엔티티 생성 또는 수정 작업을 처리하는게 좋은 패턴인지에 대한 추가적인 리서치 및 팀원간의 고민이 필요해보인다. 👀

 

 

참고자료

  • https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/context/request/RequestContextHolder.html
  • https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/context/request/RequestAttributes.html
  • https://docs.spring.io/spring-data/jpa/docs/1.7.0.DATAJPA-580-SNAPSHOT/reference/html/auditing.html