파일 업로드

상훈·2024년 3월 24일
post-thumbnail

우리가 평소 HTML Form을 통해 데이터를 주고 받을 때는 폼 전송 방식을 application/x-www-form-urlencoded 을 사용한다.

이때는 입력 받은 문자열에 대한 처리인데, 만약 파일을 전송할 때는 파일의 바이너리 코드를 전송하고 받을 필요가 있다. 이때는 폼 전송 방식을 multipart/form-data를 사용하면 된다.

  • default
    • Content-type: application/x-www-form-urlencoded
  • 문자 + binary
    • enctype = "multipart/form-data"
    • 업로드중..

이떄 Multipart/form-data는 전송 항목을 각각 구분하여 항목별로 헤더에 추가해준다. 이때 일반 데이터는 각 항목별로 문자가 전송되고, 파일의 경우 파일 이름과 Content-Type이 추가되어 바이너리 데이터가 전송된다.

또한 바이너리 데이터를 전송할 때는 업로드 할 파일의 사이즈 제한이 존재하는데 다음과 같은 옵션을 주어 사이즈를 설정할 수 있다.

spring.servlet.multipart.max-file-size=1MB			// 하나의 파일 최대 사이즈 : 1MB (Default)
spring.servlet.multipart.max-request-size=10MB	// 전체 파일 최대 사이즈 : 10MB (Default)

서블릿에서는 기본적인 폼 요청보다 복잡한 멀티파트 관련 처리를 온 오프하는 옵션이 제공되는데 다음과 같이 설정할 수 있다

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

이렇게 설정하여 멀티파트 형식을 통해 데이터를 전송 받게 되면 데이터를 하나하나 각각 부분(Part)으로 나누어 전송하게 되는데 parts에는 나누어진 데이터가 각각 담긴다.

Collection<Part> parts = request.getParts();
for (Part part : parts) {
  log.info("name={}", part.getName());
}

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

Part 주요 메서드

  • part.getSubmittedFileName() : 클라이언트가 전달한 파일명

    Collection<Part> parts = request.getParts();
    for (Part part : parts) {
      log.info("submittedFileName={}", part.getSummitedFileName())
    }
  • part.getInputStream() : Part의 전송 데이터 읽기

    InputStream inputStream = part.getInputStream();
    String body = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
  • part.write() : Part를 통해 전송된 데이터 저장

    if (StringUtils.hasText(part.getSubmittedFileName())) {
      String fullPath = fileDir + part.getSubmittedFileName();
      part.write(fullPath)
    }

    파일 경로의 경우 application.properties에 넣고 쓰면 편리하게 사용할 수 있다.

    //application.properties
    
    file.dir=/Users/sanghoon/Desktop/file/
      
    // controller
      
    @Value("${file.dir}")
    private String fileDir;
    

Servlet의 Part를 사용하면 multipart/form-data 방식을 조금 더 편리하게 다룰 수는 있지만 HttpServletRequest를 사용해야하고, 추가로 파일 부분만 구분하려면 여러가지 코드를 더 넣어줘야 한다. 하지만 스프링은 MultipartFile이라는 인터페이스를 사용하면 멀티파트 파일을 매우 편리하게 지원한다.

MultiparFile 주요 메서드

  • file.getOriginalFilename()

     public String saveFile(@RequestParam String itemName,
                                @RequestParam MultipartFile file, HttpServletRequest request) throws IOException {
    	if(!file.isEmpty()) {
        String full Path = fileDir + file.getOriginalFilename();
      }
     }
    • 기존 request.getParts를 통해 part를 가져오고 순회를 돌며 getSubmittedFileName()을 찾았던 것과 비교하면 매우 간단!
  • file.transferTo()

    file.transferTo(new File(fullPath));
    • 기존에 fileName을 가져오고 값이 존재하는지 확인 후 저장했던 것에 비해 훨씬 간편해진 것을 알 수 있다.

파일 업로드와 다운로드 고려 사항

고객이 업로드한 파일명과 서버 내부에서 관리하는 파일명을 구분해야 한다.

고객이 업로든한 파일명을 그대로 서버에 저장할 경우 다른 고객이 동일한 이름으로 저장한 경우 덮어씌워질 가능성이 있다. 따라서 서버 내부에 저장하는 이름은 UUID같은 겹치지 않는 값을 통해 저장해야 한다.

  • uploadFileName : 고객이 업로드한 파일명

    public UploadFile storeFile(MultipartFile multipartFile) throws IOException {
      if (multipartFile.isEmpty()) {
                return null;
      }
      String originalFilename = multipartFile.getOriginalFilename();
    }
  • storeFileName : 서버 내부에서 관리하는 파일명

     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 extractExt(String originalFilename) {
            int pos = originalFilename.lastIndexOf(".");
            return originalFilename.substring(pos + 1);
        }
    
        private String createStoreFileName(String originalFilename) {
            String ext = extractExt(originalFilename);
            String uuid = UUID.randomUUID().toString();
            return uuid + "." + ext;
        }

이미지를 다중 업로드 하기 위해서는 리스트에 MultipartFile을 사용한다.

  • List\ imageFiles

    @Data
    public class ItemForm {
       private Long itemId;
       private String itemName;
       private List<MultipartFile> imageFiles;
       private MultipartFile attachFile;
    }
  • 타임리프에서 다중 이미지를 등록하는 방법은 multiple="multiple" 옵션을 주면 된다.

    <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>
    

이미지를 올리기 위해서는 UrlResource로 이미지 파일을 읽어서 @ResponseBody로 이미지 바이너리를 반환한다.

@GetMapping("/images/{filename}")
public Resource downloadImage(@PathVariable String filename) throwsMalformedURLException {
    return new UrlResource("file:" + fileStore.getFullPath(filename));
}
// html
<img th:each="imageFile : ${item.imageFiles}" th:src="|/images/$
 {imageFile.getStoreFileName()}|" width="300" height="300"/>

파일을 다운로드 할 때는 Content-Disposition헤더에 attachment; filename="업로드 파일명" 값을 준다.

UrlResource resource = new UrlResource("file:" + fileStore.getFullPath(storeFileName));

String encodedUploadFileName = UriUtils.encode(uploadFileName, StandardCharsets.UTF_8);
String contentDisposition = "attachment; filename=\"" + encodedUploadFileName + "\"";

return ResponseEntity.ok()
        .header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition)
        .body(resource);
profile
문송 개발자

0개의 댓글