ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring Interceptor에서 Request 데이터 처리
    🌱 spring 2022. 12. 6. 17:09

    📍 HttpServletRequest - InputStream


    ‘application/json’ 타입의 데이터를 Servlet의 Filter 나 Spring의 Interceptor에서 따로 처리를 해주기 위해서는 HttpServletRequestInputStream 을 읽어야 한다.

    • 인증 작업 등과 같은 상황에서 사용할 수 있다.

     

    ⚠️ 하지만 HttpServletRequest의 InputStream은 한 번 읽으면 다시 읽을 수 없다.

    • 톰캣 개발자들이 막아 둠.

    Interceptor나 Filter에서 InputStream을 읽게 되면 이후 Spring이 Converter를 이용해 JSON 데이터를 바인딩 처리할 때 다음과 같은 에러가 발생한다.

    java.lang.IllegalStateException: getReader() has already been called for this request
    org.springframework.http.converter.HttpMessageNotReadableException: Could not read JSON: Stream closed; nested exception is java.io.IOException: Stream closed
    
    • Spring이 이미 읽어버린 InputStream을 다시 읽으려고 시도하다가 에러 발생

     

    HttpServletRequestWrapper

    💡 javax.servlet.http 패키지에 있는 HttpServletRequest를 래핑할 때 쓰라고 미리 준비한 HttpServletRequestWrapper 클래스
    - 이를 확장해서 wrapper 클래스를 만든다. 

    wrapper 객체를 만들어 InputStream을 읽어 그것을 통해 해야할 작업을 한 뒤, 다른 곳에서 InputStream을 다시 읽으려고 시도하는 경우 이미 읽었던 데이터로 InputStream을 생성해 돌려준다.

     

    wrapper 클래스 만들기

    public class RereadableRequestWrapper extends HttpServletRequestWrapper {
    
        private final Charset encoding;
        private byte[] rawData;
    
        public RereadableRequestWrapper(HttpServletRequest request) throws IOException {
            super(request);
    
            String characterEncoding = request.getCharacterEncoding();
            if (StringUtils.isBlank(characterEncoding)) {
                characterEncoding = StandardCharsets.UTF_8.name();
            }
            this.encoding = Charset.forName(characterEncoding);
    
            // Convert InputStream data to byte array and store it to this wrapper instance.
            try {
                InputStream inputStream = request.getInputStream();
                this.rawData = IOUtils.toByteArray(inputStream);
            } catch (IOException e) {
                throw e;
            }
        }
    
        @Override
        public ServletInputStream getInputStream() throws IOException {
            final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.rawData);
            ServletInputStream servletInputStream = new ServletInputStream() {
                public int read() throws IOException {
                    return byteArrayInputStream.read();
                }
            };
            return servletInputStream;
        }
    
        @Override
        public BufferedReader getReader() throws IOException {
            return new BufferedReader(new InputStreamReader(this.getInputStream(), this.encoding));
        }
    
        @Override
        public ServletRequest getRequest() {
            return super.getRequest();
        }
    }
    

     

    SpringInterceptor에서 Wrapper 클래스를 사용 ❗

    Servlet Filter가 아닌 Spring Interceptor에서 wrapper 클래스를 사용할 예정이라면 고려 사항이 존재

    • Spring의 DispatcherServlet에서 Interceptor를 핸들링한다!!

     

    ⇒ Interceptor 내에서 wrapper를 만들어 preHandle()에서 넘겨주게되면 Spring이 데이터를 바인딩할 때 Stream이 닫혔다는 메시지를 만난다.

     

    ❓ why

    • Interceptor가 DispatcherServlet의 doDispatcher 메서드 내에서 열심히 반복문을 돌면서 실행된 뒤 다음 구문에서 데이터 바인딩을 하러 가기 때문이다.
    • 즉, Interceptor 내에서 preHandle()으로 넘겨준 request 객체가 데이터바인딩 작업을 하러 갈 때는 call by value에 따라 이미 사라지고 없다.

     

    따라서, DispatcherServlet으로 가기 전인 Filter에서부터 wrapper 클래스로 전환해주어야 정상동작 한다.

    • 적절한 filter 속에서 wrapper로 전환해주는 작업을 해주고 doFilter() 메서드에는 래핑한 request 를 넘겨주면 Interceptor에서도 래핑된 request 객체를 받아와 사용할 수 있다.

     

    getParameterXX()

    서버에서 ‘application/json’ 타입만 받고 있다면 문제가 없다.

    하지만, ‘application/x-www-form-urlencoded’ 타입도 함께 받고 있다면 Spring은 데이터를 Controller에서 @ModelAttribute 어노테이션을 달아주었던 model 객체에 바인딩해주지 못한다.

     

    왜 ❓

    ❗Tomcat이 전달해준 HttpServletRequest의 getParameterXX() 메서드는 최초 호출될 때 들고 있는 raw data를 파싱해서 돌려준다.

    • Tomcat의 Request 클래스는 내부적으로 Lazy Loading Pattern을 사용하여 getParameterXX()가 최초 호출되기 전까지 Body로 온 InputStream을 그저 들고만 있다.

     

    하지만 wrapper 클래스를 만들당시 getParameterXX()가 한 번도 호출되지 않은 request 객체를 가져와 생성했다.

    wrapper 클래스에서 getParameterXX() 메서드들을 override 해준적이 없어 Spring이 getParameterXX()메서드를 호출시 기존 Request 객체가 raw data 파싱작업을 시도한 뒤 만들어진 parameter를 돌려준다.

    • 그러나 이미 InputStream이 없기 때문에 Request 객체는 파싱작업을 할 것이 없다.
    • 비어있는 parameter를 돌려준다.

     

    → 객체에 값이 바인딩 되지 않는다.

     

    wrapper 클래스에서 getParameterXX() 메서들을 요청했을 때 적절히 반환해줄 수 있도록 InputStream으로 들어왔던 raw data를 적절하게 처리해주는 부분을 추가해야 한다.

    • POST 방식으로 전달되는 ‘application/x-www-form-urlencoded’ 타입 데이터도 잘 처리할 수 있다.

     

    📌 ContentCachingRequestWrapper


    HttpServletRequestWrapper를 상속한 클래스를 직접 작성하지 않고 스프링에서 이미 만들어 놓은 ContentCachingRequestWrapper 클래스 사용

    이 클래스는 body 데이터를 캐싱해놓을 수 있다.

    @Component
    public class CustomServletWrappingFilter implements Filter {
    
        @Override
        public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) {
            ContentCachingRequestWrapper wrappingRequest = new ContentCachingRequestWrapper((HttpServletRequest) req);
            ContentCachingResponseWrapper wrappingResponse = new ContentCachingResponseWrapper((HttpServletResponse) res);
            chain.doFilter(wrappingRequest, wrappingResponse);
            wrappingResponse.copyBodyToResponse();
        }
    }

    request와 마찬가지로 response도 body를 한 번만 읽을 수 있다.

    ContentCachingRequestWrapper 는 객체 생성과 동시에 바디값을 저장한다.

    하지만, ContentCachingResponseWrapper 는 객체를 생성하면, 내부적으로 원래 response를 super(response)로 세팅할 뿐이다.

     

    따라서 wrappingResponse.copyBodyToResponse() 를 통해 이 ContentCachingResponseWrapper의 content 필드에 복사를 해놓는 과정이 따로 필요하다.

     

    참고 자료

    https://meetup.toast.com/posts/44

    https://www.baeldung.com/spring-reading-httpservletrequest-multiple-times

    https://www.coderanch.com/t/364591/java/read-request-body-filter

    https://docs.oracle.com/javaee/7/api/javax/servlet/http/HttpServletRequestWrapper.html

    https://modimodi.tistory.com/74

    https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/util/ContentCachingRequestWrapper.html

    '🌱 spring' 카테고리의 다른 글

    🌊 Connection Pool  (0) 2023.01.13
    JDBC ❓  (0) 2023.01.13
    애플리케이션 컨텍스트와 빈팩토리  (0) 2022.11.16
    JPA - @ElementCollection  (0) 2022.11.02
    JPA - Embedded Type  (0) 2022.11.02

    댓글

Designed by Tistory.