프로젝트 진행중 RecommendInterceptor를 사용할 일이 있었다.
인터셉터를 추가하여 사용 도중 아래와 같은 오류가 발생하게 되었다.
org.springframework.http.converter.HttpMessageNotReadableException: Required request body is missing
문제의 원인은 HttpServletRequest의 ServletInputStream을 통해 RequestBody를 String 형태로 읽어와야 하는데 한번이라도 HttpServletRequest에서 Stream형태로 RequestBody에 접근해 데이터를 가져갈경우 외부에서는 HttpServletRequest 저장되어 있었던 RequestBody에 접근할 수 없게되어 위 오류가 발생한다.
나는 RecommendInterceptor에서 HttpServletRequest를 이용해 body값을 읽어 사용하는 부분이 있는데 이 때문에 필터를 거처 컨트롤러단으로 넘어가게되면 body값을 읽지 못해 아래와 같은 오류가 발생하게 되었던 것이다.
해결 방법으로 나는 HTTP Request Body 여러 번 사용하기 글을 참고하여 HttpServletRequestWrapper 클래스를 상속 받아 캐싱하는 방식으로 해결하였다.
public class CachedBodyServletInputStream extends ServletInputStream {
private InputStream cachedBodyInputStream;
public CachedBodyServletInputStream(byte[] cachedBody) {
this.cachedBodyInputStream = new ByteArrayInputStream(cachedBody);
}
@Override
public boolean isFinished() {
try {
return cachedBodyInputStream.available() == 0;
} catch (IOException e) {
e.printStackTrace();
}
return false;
}
@Override
public boolean isReady() {
return true;
}
@Override
public int read() throws IOException {
return cachedBodyInputStream.read();
}
@Override
public void setReadListener(ReadListener listener) {
}
}
public class CachedBodyHttpServletRequest extends HttpServletRequestWrapper {
private byte[] cachedBody;
public CachedBodyHttpServletRequest(HttpServletRequest request) throws IOException {
super(request);
InputStream is = request.getInputStream();
this.cachedBody = StreamUtils.copyToByteArray(is);
}
@Override
public ServletInputStream getInputStream() throws IOException {
return new CachedBodyServletInputStream(this.cachedBody);
}
@Override
public BufferedReader getReader() throws IOException {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.cachedBody);
return new BufferedReader(new InputStreamReader(byteArrayInputStream));
}
}
@Slf4j
@Component
public class ContentCachingFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
log.info("ContentCachingFilter doFilterInternal Method");
CachedBodyHttpServletRequest cachedBodyHttpServletRequest = new CachedBodyHttpServletRequest(request);
filterChain.doFilter(cachedBodyHttpServletRequest, response);
}
}
ContentCachingFilter 클래스를 만들고 필터에 @Component 붙이면 전역적으로 모든 url에 필터가 적용되고 특정 url에 대해 따로 설정이 안된다.
ContentCachingFilter의 @Component를 제거하고 설정 파일을 만들어 관리하도록 했다.
@Bean
public FilterRegistrationBean<ContentCachingFilter> contentCachingFilter() {
FilterRegistrationBean<ContentCachingFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new ContentCachingFilter());
registrationBean.addUrlPatterns("/api/recommends/thanked", "/api/recommends/likes");
registrationBean.setOrder(Ordered.LOWEST_PRECEDENCE);
return registrationBean;
}
추가로 setOrder 부분은 따로 설정안하면 알아서 Integer.MAX_VALUE 상태 즉 우선순위에서 가장 뒤로 밀린다.