File Upload

  • HTML Form 전송 방식
    • application/x-www-form-urlencoded

      image

      • 이미지 출처: 스프링 MVC 2편 - 백엔드 웹 개바 활용 기술 [인프러 김영한님 강의]
      • HTML 폼데이터를 전송할 때 사용하는 기본적인 방법
      • HTTP Body에 문자로 username=kim&age=20와 같이 &로 구분하며 key, value를 나타낸다.
    • multipart/form-data image

      • 이미지 출처: 스프링 MVC 2편 - 백엔드 웹 개바 활용 기술 [인프러 김영한님 강의]
      • 문자와 바이너리를 전송해야 하는 상황에 사용
      • 전송하는 데이터를 Part로 나누어 전송 (구분자: boundary)

Servlet File Upload

  • MultipartHttpServletRequest와 MultipartResolver
    @PostMapping("/servlet/upload")
      public String fildUpload(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 "ok";
      }
    
    • request.getParts(): multipart/form-data 전송 방식에 각각 나누어진 부분을 받아서 확인할 수 있다.
      Content-Type: multipart/form-data; boundary=----xxxx
      ------xxxx
      Content-Disposition: form-data; name="itemName"
      
      Spring
      ------xxxx
      Content-Disposition: form-data; name="file"; filename="test.data"
      Content-Type: application/octet-stream
      sdklajkljdf...
      
  • Multipart 사용 옵션
    • 업로드 사이즈 제한
      spring.servlet.multipart.max-file-size= 1MB
      spring.servlet.multipart.max-request-size= 10MB
      
      • Size를 넘어서면 예외(SizeLimitExceeededException) 발생
    • multipart enable 끄기
      spring.servlet.multipart.enabled=false
      
      • request.getParts() 의 결과가 비어있다
      • request 객체
        • multipart 사용 X: org.apache.catalina.connector.RequstFacade@xxx
        • multipart 사용 O: org.springframework.web.multipart.support.StandartMultipartHttpServletRequest
      • Multipart를 사용하면 DispatcherServlet엣 멀티파트 리졸버(MultipartResolver)를 실행
      • 일반적인 HttpServletRequest에서 MultipartHttpServletRequest로 변환하여 반환
      • 스프링이 제공하는 기본 멀티파트 리졸버는 MultipartHttpServletRequest 인터페이스를 구현한StandardMultipartHttpServletRequest
  • 파일 업로드 예시
    • application.properties
      file.dir=/Users/htak/study/file/
      
    • Controller
      @Value("${file.dir}")
      private String fileDir;
          
      @PostMapping("/servlet/upload")
      public String fildUpload(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: {}", part);
          for (String headerName : part.getHeaderNames()) {
            log.info("headerName: {}, headerValue: {}", headerName, part.getHeader(headerName));
          }
      
          log.info("submittedFileName={}", part.getSubmittedFileName());
          log.info("size={}", part.getSize());
      
          InputStream inputStream = part.getInputStream();
          String body = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
          log.info("body: {}", body);
      
          if(!StringUtils.hasText(body)) {
            String fullPath = filePath + part.getSubmittedFileName();
            log.info("FilePath: {}", filePath);
            part.write(fullPath);
          }
        }
        return "ok";
      }
      
      • part.getSubmittedFileName() : 클라이언트가 전달한 파일명
      • part.getInputStream(): Part의 전송 데이터를 읽을 수 있다.
      • part.write(…): Part를 통해 전송된 데이터를 저장할 수 있다.

Spring File Upload

  • Spring은 MultipartFile이라는 인터페이스로 Multipart File을 매우 편리학 지원한다.
  • 예제
    @PostMapping("/spring/upload")
    public String springUpload(@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. = filePath + file.getOriginalFilename();
    		log.info("File Path: {}", fullPath);
    		file.transferTo(new File(fullPath));
    	}
    	return "upload-form";
    }
    
    • @RequestParam MultipartFile file
      • 업로드하는 HTML Form의 name에 맞추어 @RequestParam 을 적용하면 된다. 추가로 @ModelAttribute 에서도 MultipartFile 을 동일하게 사용할 수 있다.
    • MultipartFile 주요 메서드
      • file.getOriginalFilename() : 업로드 파일 명
      • file.transferTo(…) : 파일 저장
  • 예제로 구현하는 파일 업로드, 다운로드
    • Item.java, UploadFile.java
      @Data
      public class Item {
      	private Long id;
      	private String itemName;
      	private UploadFile attachFile;
      	private List<UploadFile> imageFiles;
      }
      
      @Data
      @AllArgsConstructor
      public class UploadFile {
      	private String uploadFileName;
      	private String storeFileName;
      }
      
      @Data
      public class ItemForm {
      	private Long itemId;
      	private String itemName;
      	private List<MultipartFile> imageFiles;
      	private MultipartFile attachFile;
      }
      
      • 충돌이 일어나지 않도록 업로드 파일명과 저장 파일명이 달라야 한다.
    • FileStore.java
      @Component
      public class FileStore {
      	@Value("${file.dir}")
      	private String fileDir;
      
      	public String getFullPath(String filename) {
      		return this.fileDir + filename;
      	}
      	
      	public List<UploadFile> storeFiles(List<MultipartFile> multipartFiles) throws IOException{
      		List<UploadFile> storeFileResult = new ArrayList<>();
      		for(MultipartFile multipartFile: multipartFiles) {
      			if(mulipartFile.isEmpty()) {
      				storeFileResult.add(storeFile(multipartFile));
      			}
      		}
      		return storeFileResult;
      	}
      	public UploadFile storeFile(MultipartFile multipartFile) throws IOException {
      		if(multipartFile.isEmpty()){
      			return null;
      		}
      		String originalFilename = multipartFile.getOriginalFilename();
      		String storeFileName = createStoreFileName(originalFilename);
      		multipartFile.transferTo(new File(getFullpath(storeFilename));
      		return new UploadFile(originalFilename, storeFilename);
      	}
      	private String createStoreFilename(String originalFilename) {
      		String ext = extractExt(originalFilename);
      		String uuid = UUID.randomUUID().toString();
      		return uuid + "." + ext;
      	}
      	private String extractExt(String originalFilename) {
      		int pos = originalFilename.lastIndexOf(".");
      		return originalFilename.substring(pos + 1);
      	}
      }
      
    • ItemController.java
      @Slf4j
      @Controller
      @RequiredArgsConstructor
      public class ItemController {
      	private final ItemRepository itemRepository;
      	private final FileStore fileStore;
      	
      @GetMapping("/items/new")
      public String newItem(@ModelAttribute ItemForm form) {
          return "item-form";
      }
      	
      @PostMapping("/items/new")
      public String saveItem(@ModelAttribute ItemForm form, RedirectAttributes redirectAttributes) throws IOException {
          UploadFile attachFile = fileStore.storeFile(form.getAttachFile());
          List<UploadFile> storeImageFiles = fileStore.storeFiles(form.getImageFiles());
      		
          Item item = new Item();
          item.setItemName(form.getItemName());
          item.setAttachFile(attachFile);
          item.setImageFiles(storeImageFiles);
          itemRepository.save(item);
      		
          redirectAttributes.addAttribute("itemId", item.getId());
      		
          return "redirect:/items/{itemId}";
      }
      	
      @GetMapping("/items/{id}")
        public String items(@PathVariable Long id, Model model) {
            Item item = itemRepository.findById(id);
            model.addAttribute("item", item);
            return "item-view";
        }
      //<img> 태그로 이미지를 조회할 때 사용한다. UrlResource 로 이미지 파일을 읽어서 @ResponseBody 로 이미지 바이너리를 반환한다.
        @ResponseBody
        @GetMapping("/images/{filename}")
        public Resource downloadImage(@PathVariable String filename) throws MalformedURLException {
            return new UrlResource("file:" + fileStore.getFullPath(filename));
        }
        @GetMapping("/attach/{itemId}")
        public ResponseEntity<Resource> downloadAttach(@PathVariable Long itemId) throws MalformedURLException {
            Item item = itemRepository.findById(itemId);
            String storeFileName = item.getAttachFile().getStoreFileName();
            String uploadFileName = item.getAttachFile().getUploadFileName();
            UrlResource resource = new UrlResource("file:" + fileStore.getFullPath(storeFileName));
            log.info("uploadFileName={}", uploadFileName);
            String encodedUploadFileName = UriUtils.encode(uploadFileName, StandardCharsets.UTF_8);
            String contentDisposition = "attachment; filename=\"" + encodedUploadFileName + "\"";
            return ResponseEntity.ok()
                .header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition)
                .body(resource);
        }
      }
      
    • 조회 form
      상품명: <span th:text="${item.itemName}">상품명</span><br/>
      첨부파일: <a th:if="${item.attachFile}" th:href="|/attach/${item.id}|" th:text="${item.getAttachFile().getUploadFileName()}" /><br/>
      <img th:each="imageFile : ${item.imageFiles}" th:src="|/images/$ {imageFile.getStoreFileName()}|" width="300" height="300"/>
      
    • 등록 form
      <form th:action method="post" enctype="multipart/form-data">
          <ul>
          <li>상품명 <input type="text" name="itemName"></li>
          <li>첨부파일<input type="file" name="attachFile" ></li>
          <li>이미지 파일들<input type="file" multiple="multiple" name="imageFiles" ></li>
          </ul>
      	    <input type="submit"/>
      </form>
      

출처

  • 스프링 MVC 2편 - 백엔드 웹 개바 활용 기술 [인프러 김영한님 강의]