Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

multipart는 HTTP POST로만 전송해야 한다

작업을 하다가 multipart/form-data로 PUT 전송을 할 일이 생겼다. 예를 들어 일반적인 회원가입 폼을 생각해 보면 회원가입시에는 /signup에 POST로 폼 전송을 하지만 회원 가입 후 회원정보를 갱신한다고 생각하면 /user/{id}에 PUT 전송을 하는게 요즘은 일반적이다. 완전히 REST를 다 따르진 않다고 하더라도 /user/edit?userId=1과 같이 사용할 수도 있지만 요즘은 RESTful하게 작성하는 것이 어느 정도 보편화되었고 리소스중심으로 URI를 가져가면 여러 모로 더 깔끔해 지는 장점도 있다.

어쨋든 이러한 회원 가입 시나리오에서 회원정보에 이미지같은 파일이 있기 때문에 폼 전송을 multipart/form-data로 하면서 PUT으로 하게 된 것이다. 뭘 개발하냐에 따라 다르겠지만 요즘은 input[type='file']보다는 파일을 업로드하는 컴포넌트를 따로 써서 폼전송과 별도로 업로드 하는 게 더 일반적이기 때문에 multipart/form-data자체를 사용한지가 좀 오래되었다. 스프링 웹MVC를 사용하는 환경이었는데 브라우저에서 폼 전송을 하다보니 바로 PUT 전송을 하는 것이 아니라 메서드 오버라이드를 이용해서 POST전송을 하면서 <input type="hidden" name="_method" value="PUT">을 이용해서 PUT으로 처리되도록 사용하고 있었는데 스프링 웹MVC가 이를 PUT으로 인식하지 않고 계속 POST로만 인식하고 있었다. 스프링은 이를 HiddenHttpMethodFilter가 처리하는데 이 처리가 제대로 되지 않았다.(물론 multipart/form-data가 아니면 정상적으로 동작한다.)

추적해 본 결과 CommonsMultipartResolverApache Commons FileUpload사용하는데 ServletFileUpload의 isMultipartContent 소스를 보면 다음과 같이 POST로 하드코딩이 되어 있다.

public static final boolean isMultipartContent(HttpServletRequest request) {
  if (!POST_METHOD.equalsIgnoreCase(request.getMethod())) {
    return false;
  }
  return FileUploadBase.isMultipartContent(new ServletRequestContext(request));
}

일단 multipart/form-data로 넘어왔으므로 HiddenHttpMethodFilter가 쿼라파라미터를 제대로 판단하지 못했고 멀티파트는 아예 POST만 받도록 처리하게 되어 있었다. 도움을 받아서 이를 우회할 수 있도록 일단 처리는 했지만 POST로 하드 코딩되어 잇는 이유가 궁금했다. 듣보잡 라이브러리도 아니고 아파치 커먼즈 라이브러리에서 POST만 받도록 처리한데는 이유가 있을것 같아서 원인을 찾기 시작했다.

검색으로는 잘 나오지 않았기 때문에(대부분은 "_method 인풋을 히든으로 만드세요"라는 답변이거나 좀 더 찾으면 PUT을 인식할 수 있게 리졸버를 작성하라는 내용이 전부였다.) FileUpload 라이브러리의 이슈를 찾아가니 isMultipartContent가 PUT을 지원하지 않는다라는 이슈가 있었다.

Roy T. Fielding added a comment - 07/Mar/13 06:25

PUT means the sent representation is the replacement value for the target resource. A server could certainly support that functionality using any container format, it wouldn't be "normal" to use a MIME multipart, nor is it expected to be supported by the file upload functionality defined for browsers in RFC1867.

If you want to PUT a package, I suggest defining a resource that can be represented by an efficient packaging format (like ZIP) and then using PUT on that resource to have the side-effect of updating the values of its subsidiary resources.

대충 번역하자면

PUT은 대상 리소스의 값을 교체한다는 의미이다. 서버는 어떤 컨테이너 포맷이라도 사용해서 이 기능을 반드시 구현해야 하는데 여기서 포맷은 보통 multipart MIME을 사용하지 않고 RFC 1867에 따라 브라우저도 파일 업로드 기능으로 지원하지 않는다.

PUT으로 패키지를 올리기 원한다면 패키지 포맷(zip같은)을 나타내는 리소스를 정의하고 해당 리소스의 값을 갱신하는 리소스에 PUT을 사용해야 한다.

이 글이 달리고 내용이 정리되어 해당 이슈는 닫혔는데 여기서 댓글을 단 Roy T. Fielding은 HTTP 스펙을 작성한 사람중의 하나이면서 아파치 HTTP 서버를 만든 사람이기도 하고 REST를 최초로 만든 그 로이 필딩이다! 이견없이 한번에 이해가 됐다. "아~ PUT으로 multipart를 보내면 안되는 거구나." 그러니까 PUT은 특정 리소스를 갱신하는 역할을 하는데 multipart로 보내면 한번에 여러 리소스를 처리하므로 이미지 같은 경우를 PUT으로 처리하려면 이미지등에 대한 리소스 URI에 별도의 PUT 요청을 보내서 갱신하고 일반적인 폼은 따로 처리하라는 의미이다. 스펙에 빠삭하지 못해서 정확치는 않지만 이 경우에는 한 URI로 PUT을 보내서 여러 리소스(회원 정보 + 이미지)를 한꺼번에 처리하려고 했으므로 PUT이 적합치 않다는 의미로 보인다.

UI를 당장 파일업로드별로 따로 처리할 수 없다보니(요즘은 다 이미지 같은건 따로 처리하는데 이게 꼭 Rich한 UI를 가지기 위한 것만은 아님을 알 수 있었다.) POST로 처리해버렸다.

2013/11/25 23:44 2013/11/25 23:44