Spring file upload

http 통신으로 이미지등의 파일을 전송할때는 일반적으로 multipart-form/data 를 통해 전달합니다.

multipart-form/data 처리하기

multipart/form-data 란 http 스펙중 하나이며, 데이터를 part 단위로 구분하여 메시지 또는 파일을 전송합니다.

html form 태그에서 사용시엔 속성을enctype='multipart/form-data' 로 설정해야 합니다. 각 part는 key/value 형태로 구성되었으며 part별 header가 존재하여 데이터 형식 및 값을 정의합니다. part는 Boundary를 통해 구분하며, -- 구분자로 시작하는 규칙외에는 특별한 규칙이 존재하지 않습니다.

POST /upload-file HTTP/1.1
Host: localhost:8080
Content-Length: 265
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW

----WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="image"; filename="image.png"
Content-Type: image/png

(data)
----WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="name"

hahava
----WebKitFormBoundary7MA4YWxkTrZu0gW

servlet 에서 파일 처리하기

servlet 3.0 이전에는 파일 관련 기능이 없어 개발자가 직접 request를 stream으로 파싱하여 사용해야 했습니다. 그러나 3.0 이후 Part 인터페이스를 구현하여 손 쉽게 처리할 수 있게 되었습니다. Part는 클라이언트에서 multipart/form-data 으로 전송시 해당 영역을 파싱하여 Collection 객체로 가지고 있게 됩니다.

/**
 * This class represents a part as uploaded to the server as part of a
 * <code>multipart/form-data</code> request body. The part may represent either
 * an uploaded file or form data.
 *
 * @since Servlet 3.0
 */
public interface Part {

    /**
     * Obtain an <code>InputStream</code> that can be used to retrieve the
     * contents of the file.
     *
     * @return An InputStream for the contents of the file
     *
     * @throws IOException if an I/O occurs while obtaining the stream
     */
    public InputStream getInputStream() throws IOException;

    // ...중략...
}

servlet 에서 @MultipartConfig 또는 web.xml 에 <multipart-config> 관련 설정 추가시 아래 코드와 같이 간단하게 getParts() 를 호출하여 해당 객체를 관리할 수 있습니다.

@MultipartConfig
@WebServlet(name = "fileServlet", urlPatterns = "/upload-file")
public class FileServlet extends HttpServlet {
	@Override
	protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		Collection<Part> parts = req.getParts();

		for (Part part : parts) {
			for (String headerName : part.getHeaderNames()) {
				System.out.println(headerName + " : " + part.getHeader(headerName));
			}

			if (part.getHeader("Content-Disposition").contains("filename=")) {
				String fileName = UUID.randomUUID().toString();
				Files.copy(part.getInputStream(), new File("/tmp/" + fileName).toPath());
			} else {
				System.out.println(part.getName() + "=" + req.getParameter(part.getName()));
			}

			System.out.println();
		}
	}
}

spring 에서 파일 처리하기

spring 역시 servlet과 마찬가지로 multipart를 간단하게 처리하기 위한 인터페이스를 제공합니다. DispatcherSerlvet에서 request를 감지한뒤 multipart타입일 경우 MultipartHttpServletRequest 를 생성하여 비즈니스 로직을 수행하게 됩니다.

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
  HttpServletRequest processedRequest = request;
  HandlerExecutionChain mappedHandler = null;
  boolean multipartRequestParsed = false;

  WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

  try {
    ModelAndView mv = null;
    Exception dispatchException = null;

    try {
      processedRequest = checkMultipart(request);
      multipartRequestParsed = (processedRequest != request);

//...중략...

protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException {
  if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) {
    if (WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class) != null) {
      if (DispatcherType.REQUEST.equals(request.getDispatcherType())) {
        logger.trace("Request already resolved to MultipartHttpServletRequest, e.g. by MultipartFilter");
      }
    }
    else if (hasMultipartException(request)) {
      logger.debug("Multipart resolution previously failed for current request - " +
          "skipping re-resolution for undisturbed error rendering");
    }
    else {
      try {
        return this.multipartResolver.resolveMultipart(request);
      }

//...중략...

// StandardMultipartHttpServletRequest.java 
private void parseRequest(HttpServletRequest request) {
  try {
    Collection<Part> parts = request.getParts();
    this.multipartParameterNames = new LinkedHashSet<>(parts.size());
    MultiValueMap<String, MultipartFile> files = new LinkedMultiValueMap<>(parts.size());
    for (Part part : parts) {
      String headerValue = part.getHeader(HttpHeaders.CONTENT_DISPOSITION);
      ContentDisposition disposition = ContentDisposition.parse(headerValue);
      String filename = disposition.getFilename();
      if (filename != null) {
        if (filename.startsWith("=?") && filename.endsWith("?=")) {
          filename = MimeDelegate.decode(filename);
        }
        files.add(part.getName(), new StandardMultipartFile(part, filename));
      }
      else {
        this.multipartParameterNames.add(part.getName());
      }
    }
    setMultipartFiles(files);
  }
  catch (Throwable ex) {
    handleParseFailure(ex);
  }
}

최종적으로 parseRequest 를 호출하게 됩니다. 이때 servlet의 Part 객체를 파싱하는 것을 확인할 수 있습니다. 파싱후에는 controller에서 MultipartHttpServletRequest 또는 @RequestParam을 이용하여 손쉽게 조작할 수 있습니다.

@PostMapping("/upload-file-v2")
public ResponseEntity<Void> uploadImage(MultipartHttpServletRequest multipartHttpServletRequest) {
  System.out.println(multipartHttpServletRequest.getParameter("name"));
  System.out.println(multipartHttpServletRequest.getFile("image").getOriginalFilename());
  return ResponseEntity.noContent().build();
}

@PostMapping("/upload-file-v3")
public ResponseEntity<Void> uploadImageV2(@RequestParam MultipartFile image, @RequestParam String name) {
  System.out.println(name);
  System.out.println(image.getOriginalFilename());
  return ResponseEntity.noContent().build();
}

카테고리:

업데이트:

댓글남기기