Daily Develop

[Post] application/x-www-form-urlencoded 데이터 받는 방법, inputStream 주의

에디개발자 2021. 6. 18. 07:00
반응형

나를 닮았다고 한다...

프로젝트를 진행하던 중 개인인증 관련하여 외부연동하던 중 발생했던 이슈를 정리해보겠습니다. 

 

Callback 을 전송하는 타입은 아래와 같습니다.

Method : Post
Content-Type: application/x-www-form-urlencoded

 

대부분은 Post 일 경우는 RequestBody로 데이터를 전달받습니다. (application/json) 하지만 위와 같이 content-type이 application/x-www-form-urlencoded 일 경우 Body로 데이터가 전달되지 않습니다. 그리하여 @RequestBody가 아닌 @RequestParam 이나 @ModelAttribute로 받아야합니다. 

 

@RequestParam으로 받을 경우 Controller에 파라미터별로 설정을 해주거나 따로 데이터 객체에 매핑하는 filter를 만들어야합니다. 하지만 @ModelAttribute를 사용하면 객체로 바로 받을 수 있어 편리합니다. 그리하여 저는 @ModelAttribute로 받는 것을 선택하였습니다. 코드로 살펴보겠습니다.

 

Controller

@RestController
@RequestMapping("/api/v1/test")
class TestController(
    val service: TestService
) {
    
    @PostMapping(consumes = [MediaType.APPLICATION_FORM_URLENCODED_VALUE])
    fun test(
        request: ReqTestDTO
    ) = service.test(request)
    
}

 

데이터 객체

data class ReqTestDTO(
    var test1: String,
    var test2: Int
}

PostMapping 어노테이션을 사용하고 consumes로 받는 content-type을 설정합니다. 그리고 @ModelAttribute로 데이터를 받습니다. 위처럼 객체만 선언한 경우 암묵적으로 @ModelAttrubite가 선언됩니다. 

 

Trouble shooting

여기서부터는 제가 겪은 문제에 대해서 작성해보겠습니다. 저는 위처럼 작성하였음에도 파라미터를 받지 못하는 문제가 있었습니다. 진짜 한참 삽질.....아.. 찾아보니 LogFilter에서 request의 inputStream을 사용하는 것이었습니다. 코드로 살펴보겠습니다.

 

open class HttpLoggingFilter: AbstractRequestLoggingFilter() {

override fun doFilterInternal(
        request: HttpServletRequest,
        response: HttpServletResponse,
        filterChain: FilterChain
    ) {
        if (includeFilter(request) && excludeFilter(request)) {
            // 문제가 되는 코드...
            val requestLoggingWrapper = RequestLoggingWrapper(reqId, request)
            
            // 나머지코드는 생략...
    }
}    
class RequestLoggingWrapper(
    private val request: HttpServletRequest,
) : HttpServletRequestWrapper(request) {

    // 코드 생략..
    constructor(requestId: Long, request: HttpServletRequest) : this(request) {
        // 문제가 되는 getInputStream....
        servletInputStream = super@RequestLoggingWrapper.getInputStream()
        IOUtils.copy(servletInputStream, bos)
    }

    override fun getInputStream(): ServletInputStream {
        return object : ServletInputStream() {
            var input = ByteArrayInputStream(bos.toByteArray())
            override fun read(): Int {
                return input.read()
            }

            override fun read(b: ByteArray, off: Int, len: Int): Int {
                return input.read(b, off, len)
            }

            @Throws(IOException::class)
            override fun read(b: ByteArray): Int {
                return input.read(b)
            }

            override fun isFinished(): Boolean {
                return servletInputStream.isFinished
            }

            override fun isReady(): Boolean {
                return servletInputStream.isReady
            }

            override fun setReadListener(readListener: ReadListener) {
                servletInputStream.setReadListener(readListener)
            }
        }
    }
}    

content-type: application/x-www-form-urlencoded 일 경우 request.getParameterMap을 통하여 데이터를 조회하여 객체에 매핑시켜줍니다. 하지만 위에서 보시는바와 같이 filter에서 이미 request에서 inputStream을 사용하였습니다. 물론 getInputStream을 오버라이딩하여 처리하였지만 getParameterMap에 대한 처리가 되어있지 않았습니다. 그래서 결국 데이터 객체가 매핑되지 못하고 null이 들어오는 문제였습니다. 

 

inputStream이 문제가 되는이유!!!

inputStream은 들어오는 통로라는 뜻입니다. 통로에서 데이터를 조회하는 순간 데이터는 사라집니다. 

 

그리하여 getParameterMap 메소드를 오버라이드하여 처리하였습니다. 

constructor(requestId: Long, request: HttpServletRequest) : this(request) {
    // 추가
    parameterMap.putAll(super@RequestLoggingWrapper.getParameterMap())
    servletInputStream = super@RequestLoggingWrapper.getInputStream()
    IOUtils.copy(servletInputStream, bos)
}


// 추가
override fun getParameterMap(): MutableMap<String, Array<String>> {
    return parameterMap
}

여기서 주의할 점은 두가지입니다.

  • parameterMap = super@RequestLoggingWrapper.getParameterMap()으로 하면 안됩니다. Map안의 주소값이 복사되므로 이 경우도 getInputStream을 하는 순간 parameterMap은 null이 됩니다.
  • servletInputStream = super@RequestLoggingWrapper.getInputStream() 보다 먼저 선언해야합니다. getInputStream을 선언하고 getParameterMap을 하면 null이 나옵니다.

저와 같은 실수를 하지 않기를 바랍니다.

반응형