본문 바로가기

Spring Boot

controller 호출 전 Request body 값 읽기

상황

모든 api 요청에 대한 로그를 남겨야 한다.

이때 파라미터 값, 즉 body 에 들어있는 값도 저장해야한다.

 


 

문제

 

filter 나 interceptor 로 request 객체의 body 에 접근하여 데이터를 읽어 오려고 하였다.

 

이때 getReader 나 getInputStream 을 사용하여 body 의 데이터를 읽어왔는데

 

java.lang.IllegalStateException: getReader() has already been called for this request

 

와 같이 이미 읽은 데이터라는 에러가 등장하였다.

 

에러의 원인은 getReader를 사용하면 request body 를 읽기 위한 스트림을 반환하고, 읽는 동안 내부적으로 포인트를 사용하여 읽은 위치를 기억하고, 두번째 부터 읽을 때는 body의 마지막 부분을 기억하고 있는 상태이기 때문에 body 에 데이터가 없다고 판단하게 되는 것이라고 합니다.

https://aljjabaegi.tistory.com/683

 


 

문제해결

 

해결방법은 간단합니다.  request 객체를 복사하고 다음 요청 부터는 복사된 객체를 넘기면 됩니다.

 

이미 이러한 문제를 해결하기 위해  HttpServletRequestWrapper 이라는 클래스를 구현해 두었습니다.

 

먼저 HttpServletRequestWrapper  클래스를 상속받을 클래스를 만들어 상속해줍니다.

 

public class ReadableRequestBodyWrapper extends HttpServletRequestWrapper {
    class ServletInputStreamImpl extends ServletInputStream {
        private InputStream inputStream;

        public ServletInputStreamImpl(final InputStream inputStream) {
            this.inputStream = inputStream;
        }

        @Override
        public boolean isFinished() {
            // TODO Auto-generated method stub
            return false;
        }

        @Override
        public boolean isReady() {
            // TODO Auto-generated method stub
            return false;
        }

        @Override
        public int read() throws IOException {
            return this.inputStream.read();
        }

        @Override
        public int read(final byte[] b) throws IOException {
            return this.inputStream.read(b);
        }

        @Override
        public void setReadListener(final ReadListener listener) {
            // TODO Auto-generated method stub
        }
    }

    private byte[] bytes;
    private String requestBody;

    public ReadableRequestBodyWrapper(final HttpServletRequest request) throws IOException {
        super(request);

        InputStream in = super.getInputStream();
        // request의 InputStream의 content를 byte array로 가져오고
        this.bytes = IOUtils.toByteArray(in);
        // 그 데이터는 따로 저장한다
        this.requestBody = new String(this.bytes);
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        // InputStream을 반환해야하면 미리 구해둔 byte array 로
        // 새 InputStream을 만들고 이걸로 ServletInputStream을 새로 만들어 반환
        final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.bytes);
        return new ServletInputStreamImpl(byteArrayInputStream);
    }

    public String getRequestBody() {
        return this.requestBody;
    }
}

 

 

이후 filter 를 등록해 줍니다.

 

public class CopyBodyFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, 
                         FilterChain chain) throws IOException, ServletException {
        try {
            ReadableRequestBodyWrapper wrapper = 
                           new ReadableRequestBodyWrapper((HttpServletRequest) request);
                           
            wrapper.setAttribute("requestBody", wrapper.getRequestBody());
            // setAttribute 로 body 데이터를 저장해둡니다.
            
            chain.doFilter(wrapper, response);
        } catch (Exception e) {
            chain.doFilter(request, response);
        }
    }
}

 

@Configuration
public class FilterConfig {
    @Bean
    public FilterRegistrationBean<CopyBodyFilter> filterRegistrationBean() {
        FilterRegistrationBean<CopyBodyFilter> registrationBean = new FilterRegistrationBean<>();
        registrationBean.setFilter(new CopyBodyFilter()); // 위에서 만든 Filter 를 등록
        registrationBean.addUrlPatterns("/api/*");

        return registrationBean;
    }
}

 

interceptor 에서 body 데이터를 불러와 읽습니다.

 

public class APILogInterceptor implements HandlerInterceptor {

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
       
        String body = (String) request.getAttribute("requestBody"); // body 값을 읽음

        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        return true;
    }


    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
    }
}

 


마무리

 사실 interceptor 나 filter 만으로 데이터를 가져오고 싶었는데 급하게 하다보니 방법을 찾지 못했습니다.

 

좀더 나은 방법이 생기면 수정하도록 하겠습니다!.