
그리고 폼에 입력한 전송할 항목을 HTTP Body에 문자로
username=kim&age=20와 같이 & 로 구분해서 전송한다.

multipart/form-data는 application/x-www-form-urlencoded와 비교해서 매우 복잡하고 각각의 부분(Part) 로 나누어져 있다. 그렇다면 이렇게 복잡한 HTTP 메시지를 서버에서 어떻게 사용할 수 있을까?
Collection<Part> parts = request.getParts();
HttpServletRequest에서 part를 꺼낼 수 있다.part.getSubmittedFileName() : 클라이언트가 전달한 파일명
    part.getInputStream(): Part의 전송 데이터를 읽을 수 있다.part.write(...): Part를 통해 전송된 데이터를 저장할 수 있다.
    업로드 사이즈 제한
servlet.multipart.max-file-size=1MB
    servlet.multipart.max-request-size=10MB
    spring.servlet.multipart.enabled 끄기
servlet.multipart.enabled=false (기본 true)servlet.multipart.enabled 옵션을 끄면 서블릿 컨테이너는 멀티파트와 관련된 처리를 하지 않는다.request.getParameter("itemName"), request.getParts() 의 결과가 비어있다.스프링은 MultipartFile이라는 인터페이스로 멀티 파트 파일을 매우 편리하게 지원한다.
@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";
   }
}
@RequestParam MultipartFile file@RequestParam을 적용@ModelAttribute에서도 MultipartFile을 동일하게 사용할 수 있다.getOriginalFilename() : 업로드 파일 명transferTo(...) : 파일 저장@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);
		}
}
createStoreFileName() : 서버 내부에서 관리하는 파일명은 유일한 이름을 생성하는 UUID 를 사용해서 충돌하지 않도록 한다.extractExt() : 확장자를 별도로 추출해서 서버 내부에서 관리하는 파일명에도 붙여준다. 예를 들어서 고객이 a.png 라는 이름으로 업로드 하면 51041c62-86e4-4274-801d614a7d994edb.png 와 같이 저장한다@ResponseBody
@GetMapping("/images/{filename}")
public Resource downloadImage(@PathVariable String filename) throws MalformedURLException {
		return new UrlResource("file:" + fileStore.getFullPath(filename));
}
<img> 태그로 이미지를 조회할 때 사용한다. UrlResource로 이미지 파일을 읽어서 @ResponseBody로 이미지 바이너리를 반환한다.