TIL

파일 업로드

소개

HTML 폼 전송 방식

application/x-www-form-urlencoded 방식

img.png

파일 업로드 시 위 방식의 문제점

multipart/form-data 방식

img_1.png

Part

multipart/form-data는 application/x-www-form-urlencoded와 비교해서 매우 복잡하고 각각의 부분(Part) 로 나누어져 있다. 그렇다면 이렇게 복잡한 HTTP 메시지를 서버에서 어떻게 사용할 수 있을까?

Collection<Part> parts = request.getParts();

멀티파트 사용 옵션

업로드 사이즈 제한

spring.servlet.multipart.enabled 끄기

스프링과 파일 업로드

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

SpringUploadController

@Controller
@RequestMapping("/spring")
public class SpringUploadController {

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

      if (!file.isEmpty()) {
         String fullPath = fileDir + file.getOriginalFilename();
         file.transferTo(new File(fullPath));
      }

      return "upload-form";
   }
}

JSON 타입과 MultipartFile 한 번에 받기

@PostMapping(value = "/api/v1/character", consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.MULTIPART_FORM_DATA_VALUE})
public void saveCharacter(@RequestPart CharacterCreateRequest request,
                          @RequestPart MultipartFile imgFile) {
    // ...
}

파일 저장 예시

@Data
public class UploadFile {

		private String uploadFileName; // 고객이 업로드한 파일명
		private String storeFileName; // 서버 내부에서 관리하는 파일 명

		public UploadFile(String uploadFileName, String storeFileName) {
				this.uploadFileName = uploadFileName;
				this.storeFileName = storeFileName;
		}
}
@Component
public class FileStore {

		@Value("${file.dir}")
		private String fileDir;

		public String getFullPath(String filename) {
				return fileDir + filename;
		}

		public List<UploadFile> storeFiles(List<MultipartFile> multipartFiles) throws IOException {
			List<UploadFile> storeFileResult = new ArrayList<>();
			for (MultipartFile multipartFile : imageFiles) {
			    if (!imageFile.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);
		}
}

파일 조회

@ResponseBody
@GetMapping("/images/{filename}")
public Resource downloadImage(@PathVariable String filename) throws MalformedURLException {
		return new UrlResource("file:" + fileStore.getFullPath(filename));
}

<img> 태그로 이미지를 조회할 때 사용한다. UrlResource로 이미지 파일을 읽어서 @ResponseBody로 이미지 바이너리를 반환한다.