• HttpServletRequest의 InputStream 재사용하기 - application/json
    개발공부/Spring 2024. 5. 21. 22:46
    반응형

     Spring Filter에서 InputStream을 읽어 내용을 확인하고, 올바른 정보만 서버로 전달해야하는 경우가 생겼다. http 헤더의 Content-Type에 따라, 바디를 읽는 방법이 다르므로, application/json을 기준으로 작성한다.

     

     [Spring Filter 도입하기] 글에서 이미 Request의 헤더나 바디 값을 수정해야하는 경우에는 Wrapper 클래스가 필요하다는 설명을 했다. 이 글에서 다룰 문제 또한 Wrapper 클래스로 해결한다. http 헤더의 Content-Type이 application/json인 경우, HttpServletRequest 클래스의 getInputStream() 메서드를 통해 InputStream을 반환한다. InputStream은 한 번만 사용이 가능하다.

     

     

    1. 오류가 발생하는 Filter 예제

    import org.springframework.http.MediaType
    import org.springframework.util.StreamUtils
    import org.springframework.web.filter.OncePerRequestFilter
    import javax.servlet.FilterChain
    import javax.servlet.http.HttpServletRequest
    import javax.servlet.http.HttpServletResponse
    
    // 첫 번째 필터
    class Test1Filter : OncePerRequestFilter() {
       override fun doFilterInternal(
          request: HttpServletRequest,
          response: HttpServletResponse,
          filterChain: FilterChain
       ) {
          // contentType이 application/json인 것들만 열어본다.
          if (request.contentType.contains(MediaType.APPLICATION_JSON_VALUE)) {
             val requestMessage = StreamUtils.copyToString(request.inputStream, Charsets.UTF_8)
             println("filter1 requestMessage:: $requestMessage")
          }
          
          filterChain.doFilter(request, response)
       }
    }

     

    import org.springframework.util.StreamUtils
    import org.springframework.web.filter.OncePerRequestFilter
    import javax.servlet.FilterChain
    import javax.servlet.http.HttpServletRequest
    import javax.servlet.http.HttpServletResponse
    
    // 두 번째 필터
    class Test2Filter : OncePerRequestFilter() {
       override fun doFilterInternal(
          request: HttpServletRequest,
          response: HttpServletResponse,
          filterChain: FilterChain
       ) {
             val requestMessage = StreamUtils.copyToString(request.inputStream, Charsets.UTF_8)
             println("filter2 requestMessage:: $requestMessage")
    
             filterChain.doFilter(request, response)
       }
    }
    // 첫 번째와 두 번째 필터를 Bean으로 등록한다.
    // 필터에 대한 내용이므로, Application.kt에 대한 자세한 내용을 생략한다.
    
    ...
       
       @Bean
       fun firstFilterRegister(): FilterRegistrationBean<Test1Filter> {
          val filterRegistrationBean = FilterRegistrationBean<Test1Filter>()
          filterRegistrationBean.filter = Test1Filter()
          filterRegistrationBean.order = 1 // 우선순위를 1로 설정
    
          return filterRegistrationBean
       }
    
       @Bean
       fun secondFilterRegister(): FilterRegistrationBean<Test2Filter> {
          val filterRegistrationBean = FilterRegistrationBean<Test2Filter>()
          filterRegistrationBean.filter = Test2Filter()
          filterRegistrationBean.order = 2 // 우선순위를 2로 설정
    
          return filterRegistrationBean
       }
       
       ...
    @RestController
    @RequestMapping("/v1")
    class TestController(
       @PostMapping("/test")
       fun getTest(@RequestBody testParams: Map<String, Any>): Any {
          return testParams
       }
    }

     

    위 코드를 간단하게 도식화하면, 다음과 같다.

     

    포스트맨을 통해, Post 메소드로, localhost:8080/v1/test를 실행해보자. 헤더의 contentType은 application/json이고, 바디의 내용은 { "test": "test" }이다.

    // 실행 결과는 다음과 같다.
    filter1 requestMessage:: { "test": "test" }
    filter2 requestMessage::
    
    ERROR org.springframework.http.converter.HttpMessageNotReadableException: Required request body is missing: public java.lang.Object kr.socar.admin.service.zone.api.controller.v1.BusinessOfficeController.getTest(java.util.Map<java.lang.String, ?>)

     

    filter1에서 inputStream을 사용했으므로, filter2에서는 아무 값도 찍히지 않는다. Controller에서는 바디가 없는데, @RequestBody를 사용하므로 ERROR가 발생한다.

     

     

     

    2. HttpServletRequest의 InputStream 재사용하기

     이미 servlet-api에 구현되어있는 HttpServletRequestWrapper를 상속받아, ReusableBodyRequestWrapper를 새로 만들어준다. ReusableBodyRequestWrapper 초기화 코드에서 한 번 읽을 수 있는 body를 저장한 뒤, getInputStream() 메서드를 호출할 때마다 body의 값을 InputStream으로 만들어 리턴하면 된다.  getInputStream 메서드는 항상 InputStream을 새로 리턴하므로, 재사용이 가능하다.

    import org.springframework.util.StreamUtils
    import java.io.InputStream
    import javax.servlet.ReadListener
    import javax.servlet.ServletInputStream
    import javax.servlet.http.HttpServletRequest
    import javax.servlet.http.HttpServletRequestWrapper
    
    class ReusableBodyRequestWrapper(request: HttpServletRequest) : HttpServletRequestWrapper(request) {
       val body: ByteArray
    
       init {
          val requestInputStream: ServletInputStream = request.inputStream
          this.body = StreamUtils.copyToByteArray(requestInputStream)
       }
    
       override fun getInputStream(): ServletInputStream {
          return object : ServletInputStream() {
             val inputStream: InputStream = body.inputStream()
    
             override fun isFinished(): Boolean {
                return inputStream.available() == 0
             }
    
             override fun isReady(): Boolean {
                return true
             }
    
             override fun setReadListener(readListener: ReadListener) {
                throw UnsupportedOperationException()
             }
    
             override fun read(): Int {
                return inputStream.read()
             }
          }
       }
    }
    import org.springframework.http.MediaType
    import org.springframework.util.StreamUtils
    import org.springframework.web.filter.OncePerRequestFilter
    import javax.servlet.FilterChain
    import javax.servlet.http.HttpServletRequest
    import javax.servlet.http.HttpServletResponse
    
    // 첫 번째 필터
    class Test1Filter : OncePerRequestFilter() {
       override fun doFilterInternal(
          request: HttpServletRequest,
          response: HttpServletResponse,
          filterChain: FilterChain
       ) {
          // contentType이 application/json인 것들만 열어본다.
          val nextRequest = if (request.contentType.contains(MediaType.APPLICATION_JSON_VALUE)) {
             val reusableRequest = ReusableBodyRequestWrapper(request)
             val requestMessage = StreamUtils.copyToString(reusableRequest.inputStream, Charsets.UTF_8)
             println("filter1 requestMessage:: $requestMessage")
             reusableRequest
          } else {
             request
          }
          
          filterChain.doFilter(nextRequest, response)
       }
    }

     

    첫 번째 필터만 수정하고 실행해보자.

    // 실행 결과는 다음과 같다.
    filter1 requestMessage:: { "test": "test" }
    filter2 requestMessage:: { "test": "test" }
    
    // http response
    { 
       "test": "test" 
    }

     

    filter2에서도 바디를 확인할 수 있고, 컨트롤러에서도 RequestBody를 읽어 결과를 나타냈다.

     

    참고자료:

     

     

     

     

    3. HttpServletRequest의 InputStream 한 번만 재사용하기

     해당 필터를 거치면 모든 Request에서 InputStream을 재사용이 가능하다. 하지만, 이것은 이 필터의 개발자가 만든 규칙이 전체 필터 및 프로세스에 적용된다는 문제가 있다. 특별한 이유가 없다면, 코드에 나만의 규칙을 만들지 않도록 한다.

    import org.springframework.util.StreamUtils
    import java.io.InputStream
    import javax.servlet.ReadListener
    import javax.servlet.ServletInputStream
    import javax.servlet.http.HttpServletRequest
    import javax.servlet.http.HttpServletRequestWrapper
    
    class NewRequestWrapper(body: ByteArray, request: HttpServletRequest) : HttpServletRequestWrapper(request) {
       private val inputStream: ServletInputStream = object : ServletInputStream() {
          val inputStream: InputStream = body.inputStream()
    
          override fun isFinished(): Boolean {
             return inputStream.available() == 0
          }
    
          override fun isReady(): Boolean {
             return true
          }
    
          override fun setReadListener(readListener: ReadListener) {
             throw UnsupportedOperationException()
          }
    
          override fun read(): Int {
                   return inputStream.read()
          }
       }
    
       override fun getInputStream(): ServletInputStream = inputStream
    }

     

     ReusableBodyRequestWrapper를 수정하여 InputStream을 재사용하지 못하도록 수정한다. 그리고, 클래스명 또한 NewRequestWrapper로 변경했다.

    import org.springframework.http.MediaType
    import org.springframework.util.StreamUtils
    import org.springframework.web.filter.OncePerRequestFilter
    import javax.servlet.FilterChain
    import javax.servlet.http.HttpServletRequest
    import javax.servlet.http.HttpServletResponse
    
    // 첫 번째 필터
    class Test1Filter : OncePerRequestFilter() {
       override fun doFilterInternal(
          request: HttpServletRequest,
          response: HttpServletResponse,
          filterChain: FilterChain
       ) {
          // contentType이 application/json인 것들만 열어본다.
          val nextRequest = if (request.contentType.contains(MediaType.APPLICATION_JSON_VALUE)) {
             val requestMessage = StreamUtils.copyToString(request.inputStream, Charsets.UTF_8)
             val newRequest = NewRequestWrapper(requestMessage, request)
             println("filter1 requestMessage:: $requestMessage")
             newRequest
          } else {
             request
          }
          
          filterChain.doFilter(nextRequest, response)
       }
    }

     

    첫 번째 필터를 다시 수정하고 실행해보자.

    // 실행 결과는 다음과 같다.
    filter1 requestMessage:: { "test": "test" }
    filter2 requestMessage::
    
    ERROR org.springframework.http.converter.HttpMessageNotReadableException: Required request body is missing: public java.lang.Object kr.socar.admin.service.zone.api.controller.v1.BusinessOfficeController.getTest(java.util.Map<java.lang.String, ?>)

     

    스프링의 기본 동작과 동일한 결과를 보여준다.

    반응형

    댓글

Designed by Tistory.