-
Spring Interceptor에서 Request 데이터 처리🌱 spring 2022. 12. 6. 17:09
📍 HttpServletRequest - InputStream
‘application/json’ 타입의 데이터를 Servlet의 Filter 나 Spring의 Interceptor에서 따로 처리를 해주기 위해서는 HttpServletRequest의 InputStream 을 읽어야 한다.
- 인증 작업 등과 같은 상황에서 사용할 수 있다.
⚠️ 하지만 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
'🌱 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