빙응의 공부 블로그

[Spring]스프링 MVC 2편 - 파일 업로드 본문

Spring/인프런_개념

[Spring]스프링 MVC 2편 - 파일 업로드

빙응이 2024. 2. 11. 15:35

📝파일업로드

일반적으로 사용하는 HTML Form을 통한 파입 업로드를 이해하려면 먼저 폼을 전송하는 다음 두 가지 방식의 차이를 이해해야 한다. 

 

application/x-www-form-urlencoded 방식은 HTML 폼 데이터를 서버로 전송하는 가장 기본적인 방법이다.

Form 태그에 별도의 enctype 옵션이 없으면 브라우저는 요청 http 메시지 헤더에 다음 내용을 추가한다.

Cotent-Type : application/x-www-form-urlencoded 

 

파일을 업로하드 하려면 파일은 문자가 아니라 바이너리 데이터를 전송해야 한다. 문자를 전송하는 이 방식으로 파일을

전송하기는 어렵다. 그리고 또 한가지 문제가 있는데, 보통 폼을 전송할 때 파일만 전송하는 것이 아니다.

 

게시판으로 예를 들어보자면

제목, 이름, 내용, 첨부파일

 

이렇게 파일과 함께 전송해야한다. 이것은 문자와 바이너리를 동시에 전송하는 상황이다.

 

이 문제를 해결하기 위해 HTTP는 multipart/form-data라는 전송 방식을 제공한다.

 

해당 방식을 사용하려면 Form 태그에 별도의 enctype="multipart/form-data"를 지정해야 한다.

multipart/form-data 방식은 다른 종류의 여러 파일을 폼의 내용과 함께 전송할 수 있다.

 

📝스프링 파일 

스프링에서 파일 업로드를 하는법을 보자

간단한 예제
  @GetMapping("/upload")
  public String newFile(){
    return "upload-form";
  }
  @PostMapping("/upload")
  public String saveFileV1(HttpServletRequest request) throws ServletException, IOException {
    log.info("request={}", request);
    String itemName = request.getParameter("itemName");
    log.info("itemName={}", itemName);

    Collection<Part> parts = request.getParts();
    log.info("parts={}", parts);
    return "upload-form";
  }
  <form th:action method="post" enctype="multipart/form-data">
    <ul>
      <li>상품명 <input type="text" name="itemName"></li>
      <li>파일<input type="file" name="file" ></li>
    </ul>
    <input type="submit"/>
  </form>

 

하나하나씩 살펴보자

 Collection<Part> parts = request.getParts();
 log.info("parts={}", parts);

request.getParts() : multipart/form-data 전송 방식에서 각각 나누어진 부분을 받아서 확인할 수 있다.

 

<form th:action method="post" enctype="multipart/form-data">

multipart/form-data 방식을 사용하려면 해당처럼 enctype을 지정해줘야 한다. 

 

 

🚩멀티파트 사용 옵션

업로드 사이즈 제한

spring.servlet.multipart.max-file-size=1MB
spring.servlet.multipart.max-request-size=10MB

큰 파일을 무제한 업로드하게 둘 수는 없다 그러므로 제한을 해야한다.

 

spring.servlet.multipart.enabled 

멀티파트는 일반적인 폼 요청인 application/x-www-form-urlencoded 보다 훨씬 복잡하

spring.servlet.multipart.enabled 옵션을 끄면 서블릿 컨테이너는 멀티파트와 관련된 처리를 하지 않는다.

그래서 결과 로그를 보면 request.getParameter("itemName") , request.getParts() 의 결과가 비어있다.

spring.servlet.multipart.enabled=false

결과 로그 

request=org.apache.catalina.connector.RequestFacade@xxx
itemName=null
parts=[]

spring.servlet.multipart.enabled=true (기본 true)

이 옵션을 켜면 스프링 부트는 서블릿 컨테이너에게 멀티파트 데이터를 처리하라고 설정한다.

결과 로그

request=org.springframework.web.multipart.support.StandardMultipartHttpServletRe
quest
itemName=Spring
parts=[ApplicationPart1, ApplicationPart2]

 

참고!
spring.servlet.multipart.enabled 옵션을 켜면 스프링의 DispatcherServlet에서 멀티 파트 리졸버를 실행한다.
멀티파트 리졸버는 멀티파트 요청인 경우 서블릿 컨테이너가 전달하는 일반적인 HttpServletRequest를 
MultipartHttpServletRequest로 변환해서 반환한다.
MultipartHttpServletRequest 는 HttpServletRequest 의 자식 인터페이스이고, 멀티파트와 관련된 추가 기능을 제공한다
스프링이 제공하는 기본 멀티파트 리졸버는 MultipartHttpServletRequest 인터페이스를 구현한 StandardMultipartHttpServletRequest 를 반환한다
이제 컨트롤러에서 HttpServletRequest 대신에 MultipartHttpServletRequest 를 주입받을 수 있는데, 이것을 사용하면 멀티파트와 관련된 여러가지 처리를 편리하게 할 수 있다. 그런데 이후 강의에서 설명 할 MultipartFile 이라는 것을 사용하는 것이 더 편하기 때문에 MultipartHttpServletRequest 를 잘 사용하지는 않는다. 더 자세한 내용은 MultipartResolver 를 검색해보자

 

📝서블릿과 파일 업로드

서블릿이 제공하는 Part에 대해 알고보고 실제 파일을 서버에 업로드 해보자

 

먼저 파일 업로드를 하려면 실제 파일을 저장되는 경로가 필요하다.

application.properties
file.dir=파일 업로드 경로 설정(예): /Users/kimyounghan/study/file/

 

 

PART 사용 예제
  @Value("${file.dir}")
  private String fileDir;

  @GetMapping("/upload")
  public String newFile() {
    return "upload-form";
  }
  @PostMapping("/upload")
  public String saveFileV2(HttpServletRequest request) throws
      ServletException, IOException {
    log.info("request={}", request);

    String itemName = request.getParameter("itemName");
    log.info("itemName={}", itemName);

    Collection<Part> parts = request.getParts();
    log.info("parts={}", parts);

    for(Part part : parts){
      log.info("==== PART ====");
      log.info("name = {}", part.getName());
      //PART 각각의 헤더들의 값을 출력
      Collection<String> headerNames = part.getHeaderNames();
      for (String headerName : headerNames) {
        log.info("header {}: {}", headerName, part.getHeader(headerName));
      }
      //편의 메서드
      //content-disposition은 헤더에 여러개가 들어가나 편의 메소드로 편하게 출력가능
      //content-disposition; filename
      log.info("submittedFileName={}", part.getSubmittedFileName());
      log.info("size={}", part.getSize()); //part body size

      //데이터 읽기(바디데이터)
      InputStream inputStream = part.getInputStream();
      String body = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
      log.info("body ={}", body);
      
      //파일에 저장 
      if(StringUtils.hasText(part.getSubmittedFileName())){
        String fullPath = fileDir + part.getSubmittedFileName();
        log.info("파일 저장 fullPath={}", fullPath);
        part.write(fullPath);
      }
    }
    return "upload-form";
  }

멀티파트 형식은 전송 데이터를 각각 부분(Part)으로 나누어 전송한다. parts에는 이렇게 나누어진 각각의 데이터가 담긴다

서블릿이 제공하는 PART는 멀티파트 형식을 편리하게 읽을 수 있는 다양한 메서드를 제공한다.

 

PART 주요 메서드

  • part.getSubmittedFileName() : 클라이언트가 전달한 파일명
  • part.getInputStream(): Part의 전송 데이터를 읽을 수 있다.
  • part.write(...): Part를 통해 전송된 데이터를 저장할 수 있다.

 

📝스프링과 파일 업로드

스프링은 MultipartFile 이라는 인터페이스로 멀티파트 파일을 매우 편리하게 지원한다.

  @Value("${file.dir}")
  private String fileDir;
  @GetMapping("/upload")
  public String newFile() {
    return "upload-form";
  }
  @PostMapping("/upload")
  public String saveFile(@RequestParam String itemName, @RequestParam MultipartFile file, HttpServletRequest request) throws IOException {
    log.info("request={}", request);
    log.info("itemName={}", itemName);
    log.info("multipartFile={}", file);
    if (!file.isEmpty()) {
      String fullPath = fileDir + file.getOriginalFilename();
      log.info("파일 저장 fullPath={}", fullPath);
      file.transferTo(new File(fullPath));
    }
    return "upload-form";
  }

코드를 보면 스프링 답게 딱 필요한 부분만 코드로 작성하면 된다.

@RequestParam MultipartFile file

업로드하는 HTML Form의 name에 맞추어 @RequestParam을 적용하면 된다. 추가로 @ModelAttribute에서도

MultipartFile을 동일하게 사용할 수 있다.

 

MultipartFile 주요 메서드

file.getOriginalFilename() : 업로드 파일 명

file.transferTo(...) : 파일 저장