[Spring Web MVC] Multipart
HTML Form을 통해 파일 업로드를 이해해보자.
HTML Form 전송 방식에는 크게 두 가지가 있다.
1. application/x-www-form-urlencoded
2. multipart/form-data
1번은 가장 기본적인 방식이다.
별도의 enctype 옵션이 없으면 요청 HTTP 헤더에 Content-Type: application/x-www-form-urlencoded 을 추가하고, 폼에 입력한 내용을 HTTP 바디 메세지에 & 로 구분해서 전송한다.
파일을 업로드 할 경우 문자가 아니라 바이너리 데이터를 전송해야 하는데...
1번 방식으로는 문자와 바이너리 데이터를 한 번에 전송하기 힘들다.
이런 문제를 해결하기 위해 multipart 가 도입됐다.
Form 태그에 enctype="multipart/form-data" 를 지정해 2번 방식을 사용한다.
2번 방식을 사용하면 다른 종류의 파일과 폼의 내용을 함께 전송할 수 있다.
입력 결과로 생성된 HTTP 메세지를 보면, 각각의 전송 항목들이 구분돼있다.
항목별로 Content-Disposition 헤더가 추가돼있고, 여기에 부가 정보가 포함된다.
@GetMapping("/upload")
public String newFile(){
return "upload-form";
}
@PostMapping("/upload")
public String saveFileV1(HttpServletRequest request) throws ServletException, IOException {
request.getParameter("itemName");
Collection<Part> parts = request.getParts();
return "upload-form";
}
getParts() 에서 parts는 폼에서 입력받은 데이터가 부분 부분 나누어져있는데, 각 부분을 의미한다.
spring.servlet.multipart.max-file-size=1MB
spring.servlet.multipart.max-request-size=10MB
spring.servlet.multipart.enabled=true
multipart를 사용할 때 몇 가지 조건을 설정할 수 있다.
max-file-size : 파일 하나의 최대 사이즈.
max-request-size : 업로드하는 파일 사이즈의 합.
업로드하는 파일의 사이즈를 설정할 수 있다.
enabled 값을 true로 설정 시 스프링은 서블릿 컨테이너에게 멀티파트 데이터를 처리하라고 설정한다. (기본 true)
디스패처 서블릿에서 MultipartResolver를 실행하고, 멀티파트 요청이 들어오면 서블릿 컨테이너가 일반적으로 사용하는 HttpServletRequest를 MultipartHttpServletRequest 인터페이스를 구현한 StandardMultipartHttpServletRequest 객체로 변환해서 반환한다. (HttpServletRequest의 자손으로, 멀티파트 관련 추가 기능을 제공함)
멀티파트 형식은 전송 데이터를 각 Part로 분리해서 전송하고, parts에는 나누어진 데이터들이 각각 들어간다.
서블릿이 제공하는 Part는 멀티파트 형식을 편하게 읽을 수 있도록 도와준다.
@RequestMapping("/servlet/v2")
public class ServletUploadControllerV2 {
@Value("${file.dir}")
private String fileDir;
@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);
for (Part part : parts) {
log.info("==== PART ====");
log.info("name={}", part.getName());
Collection<String> headerNames = part.getHeaderNames();
for (String headerName : headerNames) {
log.info("header {}: {}", headerName, part.getHeader(headerName));
}
//편의 메서드
//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";
}
getSubmittedFileName : 클라이언트가 전송한 파일 이름
getInputStream : Part의 전송 데이터
write : Part를 통해 전송된 데이터를 저장
서블릿이 제공하는 Part를 사용해 파일 업로드를 쉽게 구현할 수 있지만.. HttpServletRequest를 사용해야 하고 파일 부분만 구분하기 위해서 여러 코드를 추가해야 한다는 단점이 있다.
스프링은 MultipartFile 인터페이스를 지원해 멀티파트 파일 업로드를 도와준다.
@Slf4j
@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 {
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";
}
}
MultipartFile : 업로드하는 HTML 폼의 이름에 맞춰 @RequestParam을 적용한다.
getOriginalFilename 메서드로 사용자가 업로드한 파일 이름을 가져오고, transferTo 메서드로 파일을 저장한다.
서블릿으로 구현할 때 보다 훨씬 더 깔끔하게 구현할 수 있다.
파일 업로드를 구현할 때는 스프링의 MultipartFile 인터페이스를 사용하자.
예제를 살펴보자.
@Data
public class Item {
private Long id;
private String itemName;
private UploadFile attachFile;
private List<UploadFile> imageFiles;
}
상품 이름 / 첨부파일 / 이미지 파일 여러개를 업로드하는 예시이다.
@Data
public class UploadFile {
private String uploadFileName;
private String storeFileName;
public UploadFile(String uploadFileName, String storeFileName) {
this.uploadFileName = uploadFileName;
this.storeFileName = storeFileName;
}
}
uploadFile는 클라이언트가 업로드한 파일 이름이고, 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 : multipartFiles) {
if (!multipartFile.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();
// 서버에서 사용할 이름은 uuid로 생성함. 그런데 확장자는 원래 파일명꺼를 사용
return uuid + "." + ext;
}
private String extractExt(String originalFilename) {
int pos = originalFilename.lastIndexOf(".");
// 확장자 가저오기
return originalFilename.substring(pos + 1);
}
}
서버 내부에서 관리하는 파일명은 중복을 피하기 위해 UUID를 사용해서 생성한다.
이 때 확장자는 클라이언트가 업로드한 파일의 확장자를 그대로 사용하기 위해 별도로 처리해준다.
이미지는 여러 파일이 업로드 될 수 있으니 리스트를 사용해서 처리한다.
@Data
public class ItemForm {
private Long itemId;
private String itemName;
private List<MultipartFile> imageFiles;
private MultipartFile attachFile;
}
상품을 저장할 때 사용하는 아이템 폼을 만들자.
Item 클래스는 파일을 데이터베이스에 저장할 때 쓰인다.
즉, ItemForm 으로 데이터를 받아오고 저장할 때는 Item 객체를 사용한다.
@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());
// 파일에는 저장됐음
//데이터베이스에 저장. 여기서는 Map..
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";
}
@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);
}
}
저장한 파일을 클라이언트에게 보여줄 때는 서버 내부에서 사용하는 이름 대신 클라이언트가 저장한 이름을 사용한다.
items/{id} 경로로 접근했을 때 상품을 보여주는데, 이미지를 보여주는 HTML에서는 <img> 태그를 사용한다.
해당 태그의 src속성은 웹 페이지가 뜰 때 링크를 타서 소스를 가져오기 때문에 images/{filename} 으로 이미지를 넣어주기 전에는 이미지가 보이지 않는다.
HTTP 응답 헤더에 CONTENT_DISPOSITION 속성을 추가해 브라우저에서 파일을 다운로드하도록 한다.
'Spring > Spring Web MVC' 카테고리의 다른 글
[Spring Web MVC] 로그인 처리 - Filter / Interceptor (1) | 2023.07.11 |
---|---|
[Spring Web MVC] 로그인 처리 - Cookie / Session (0) | 2023.07.10 |
[Spring Web MVC] 스프링 타입 컨버터 (0) | 2022.08.28 |
[Spring Web MVC] API 예외 처리 (0) | 2022.08.26 |
[Spring Web MVC] 예외 처리와 오류 페이지 (0) | 2022.08.26 |
댓글
이 글 공유하기
다른 글
-
[Spring Web MVC] 로그인 처리 - Filter / Interceptor
[Spring Web MVC] 로그인 처리 - Filter / Interceptor
2023.07.11 -
[Spring Web MVC] 로그인 처리 - Cookie / Session
[Spring Web MVC] 로그인 처리 - Cookie / Session
2023.07.10 -
[Spring Web MVC] 스프링 타입 컨버터
[Spring Web MVC] 스프링 타입 컨버터
2022.08.28 -
[Spring Web MVC] API 예외 처리
[Spring Web MVC] API 예외 처리
2022.08.26