Controller에서 HttpRequest Body 값은 왜 비워져 있을까?

채상엽·2022년 8월 4일
1

Spring

목록 보기
11/21

문제 및 고민한 내용


  • form data 는 쿼리파라미터로 들어가는데 post 하면 url에 노출되어 안좋지 않을까?

    • form data를 get 요청으로 보낼 경우
      • url에 쿼리파라미터로 들어간다
    • form data를 post 요청으로 보낸 경우
      • 바디에 쿼리파라미터가 들어간다
  • multipart/form-data도 일종의 form-data 이기 때문에 쿼리파라미터 일 것이다. 그러므로 HttpRequestServlet의 요청 파라미터 값들을 조회하면 나와야한다. 그런데 왜 문자열 key value들은 잘 나오는데, 이미지 파일에 대한 파라미터 정보는 나오지 않을까?

    • getParts()를 이용하여 각 파트를 조회하면 알 수 있다.
  • 그런데 왜 컨트롤러에서 Http body로 들어온 내용을 HttpRequestServlet을 이용하여 정보를 출력하려고 하면 비어있는 상태일까?

    • 순수 Servlet에서 HttpRequestServlet의 body의 정보를 출력했을 경우에는 body 값이 그대로 잘 찍히는 모습을 확인할 수 있다.
    • 그런데, 컨트롤러에서 같은 방식으로 body를 출력하고자하면, form data, json에 관계없이 모두 바디가 비워져 들어오는 모습을 확인할 수 있었다.
    • 요청 -> 디스패처 서블릿 -> 컨트롤러의 객체로 값을 바인딩 하는 과정에서 바디 데이터 소비 -> 컨트롤러의 request body 비워져 있음
  • HttpServletRequestgetInputStream() 을 Servlet의 Filter에서 사용하면 다른 필터나 Controller에서 해당 요청에 대한 getInputStream을 가져갈 수 없다.

문제 확인


@RestController
public class TestController {

    @PostMapping("/")
    public void bodyEmpty(@RequestBody User user, HttpServletRequest request) throws IOException {
        ServletInputStream inputStream = request.getInputStream();
        String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);

        System.out.println("Http Body = " + messageBody);
        System.out.println("Http Body Length = " + request.getContentLength());
    }
}

위와 같이 컨트롤러에서 HttpServletRequest를 이용하여 Http Body 값을 출력하려고 한다. Postman을 이용하여 {"name" : "name", "password":"password"} 를 body에 담아 post 요청을 보내게 되면 다음과 같은 결과가 콘솔에 출력 된다.

Http Body = 
Http Body Length = 38

Body의 길이는 잘 출력되지만 Body에 원래 들어있는 내용은 출력되지 않는 모습을 확인할 수 있다.

해결


아래와 같이 Http Body가 소비되는 시점을 대략적으로 파악했었다.

요청 -> 디스패처 서블릿 -> 컨트롤러의 객체로 값을 바인딩 하는 과정에서 바디 데이터 소비 -> 컨트롤러의 request body 비워져 있음

이를 기반으로 찾아본 결과 오라클 문서에서 다음과 같은 내용을 찾아볼 수 있었다.

https://docs.oracle.com/javaee/6/api/javax/servlet/ServletRequest.html#getParameter%28java.lang.String%29

If the parameter data was sent in the request body, such as occurs with an HTTP POST request, then reading the body directly via getInputStream() or getReader() can interfere with the execution of this method.

이를 토대로 Interceptor에서 getInputStream() 을 호출하여 Http Body를 읽으면서 소비하였기 때문에, Controller의 Http Body가 비어있음을 알 수 있었다.

그렇다면 Http Body를 어떻게하면 Controller에서 확인할 수 있을까? 먼저 Http 요청이 들어오는 차례를 알아보자. Client에서 요청이 들어오면 해당 요청은 Controller에 들어오기 전 Filter, Dispatcher Servlet, Interceptor를 거쳐 Controller에 들어오게 된다.

위에서 발견한 내용이라면 Http Body는 컨트롤러에 들어오기 전 Interceptor에서 소비되고 있었다.

그렇다면 Interceptor보다 먼저 호출되는 Filter 단계에서 Http Body를 저장해놓는다면 Controller에서 출력할 수 있을 것 같아 해당 방법을 찾아보게 되었다.

방법은 다음과 같다.

public class MultiReadHttpServletRequest extends HttpServletRequestWrapper {
    private ByteArrayOutputStream cachedBytes;

    public MultiReadHttpServletRequest(HttpServletRequest request) {
        super(request);
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        if (cachedBytes == null)
            cacheInputStream();

        return new CachedServletInputStream(cachedBytes.toByteArray());
    }

    @Override
    public BufferedReader getReader() throws IOException{
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }

    private void cacheInputStream() throws IOException {
        /* Cache the inputstream in order to read it multiple times. For
         * convenience, I use apache.commons IOUtils
         */
        cachedBytes = new ByteArrayOutputStream();
        IOUtils.copy(super.getInputStream(), cachedBytes);
    }


    /* An input stream which reads the cached request body */
    private static class CachedServletInputStream extends ServletInputStream {

        private final ByteArrayInputStream buffer;

        public CachedServletInputStream(byte[] contents) {
            this.buffer = new ByteArrayInputStream(contents);
        }

        @Override
        public int read() {
            return buffer.read();
        }

        @Override
        public boolean isFinished() {
            return buffer.available() == 0;
        }

        @Override
        public boolean isReady() {
            return true;
        }

        @Override
        public void setReadListener(ReadListener listener) {
            throw new RuntimeException("Not implemented");
        }
    }
}

한번 읽고 사라지는 HttpBody가 아닌 여러번 계속해서 읽을 수 있도록 구현하기 위해서, 먼저 HttpServletRequestWrapper 를 상속받는 MultiReadHttpServletRequest 클래스를 하나 만든다. 그리고 여기서 getInputStream()에서 요청 Http Body를 복사하여 값을 저장하고 반환하는 기능을 재정의 한다.

그리고 이를 다음과 같이 MyFilter 라는 새로운 필터 클래스를 생성한 뒤 다음과 같이 MultiReadHttpServletRequest 클래스 생성을 필터에서 실행해준다.

@Component
public class MyFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {

        MultiReadHttpServletRequest multiReadRequest = new MultiReadHttpServletRequest((HttpServletRequest) request);

        chain.doFilter(multiReadRequest, response);
    }
}

이렇게 되면 인터셉터에서 Http Body가 소모 되기 전에 필터 단계에서 Http Body를 옮겨 담는 기능이 수행되기 때문에, Controller 에서 Http Body 값을 확인할 수 있게 된다.

아래는 필터를 적용한 뒤 콘솔창의 출력 결과이다.

Http Body = {"name":"name", "password":"password"}
Http Body Length = 38
profile
프로게이머 연습생 출신 주니어 서버 개발자 채상엽입니다.

0개의 댓글