[스프링] MultipartFile 인터페이스를 활용한 다중 파일 업로드
과거엔 HttpServletRequest 인터페이스를 상속받은 MultipartHttpServletRequest 인터페이스를 활용하여 파일을 업로드 할 수 있었습니다.
MultipartHttpServletRequest를 사용해서 파일 업로드를 구현할 수 있지만 스프링이 제공해주는 MultipartFile 인터페이스를 활용함으로써 더욱 쉽게 파일을 단일, 다중으로 업로드를 할 수 있습니다.
공통적으로 사용하는 HTML은 아래와 같습니다. 타임리프를 사용했습니다.
<div class="container">
<div class="py-5 text-center">
<h2>상품 등록 폼</h2>
</div>
<h4 class="mb-3">상품 입력</h4>
<form th:action method="post" enctype="multipart/form-data">
<ul>
<li>상품명 <input type="text" name="itemName"></li>
<li>파일<input type="file" name="file"></li>
<li>파일2<input type="file" name="file2"></li>
</ul>
<input type="submit"/>
</form>
</div> <!-- /container -->
파일을 2개 보낼 수 있습니다.
type은 file로 해야 MultipartFile이 인식을 할 수 있으며 각각의 파일을 구분지어야 하므로 name은 file, file2로 달라야 합니다.
input 옵션의 multiple을 사용하지 않았으므로 각각 파일 선택은 1개씩만 가능합니다.
단일 업로드를 해보겠습니다.
위의 HTML 파일에서 <li>파일2<input type="file" name="file2"></li> 을 빼고 진행했습니다.
@PostMapping("/upload")
public String saveFile(@RequestParam String itemName,
@RequestParam(name = "file") MultipartFile file) throws IOException {
if (!file.isEmpty()) {
String fullPath = fileDir + file.getOriginalFilename();
file.transferTo(new File(fullPath));
}
return "upload-form";
}
위의 코드에 대해 설명을 하자면 사용자가 폼으로 파일을 전송하면 컨트롤러는 POST 방식으로 이를 처리합니다.
HTTP Method가 POST인 경우에는 컨트롤러의 매개변수에 주로 @RequestBody를 넣어 처리를 하는데 파일을 처리하는 경우에는 조금 다릅니다.
기본적으로 @RequestBody는 body로 전달받은 JSON 형태의 데이터를 파싱을 합니다. 반면 Content-Type이 multipart/form-data로 전달되어 올 때는 Exception을 발생시켜 문제가 됩니다.
따라서 Content-Type이 multipart/form-data인 경우에는 @RequestBody가 아닌 다른 방법을 사용해야 합니다.
방법은 @RequestParam, @RequestPart 어노테이션을 사용하는 방법이 있는데 여기서는 @RequestParam을 사용했습니다.
이번엔 그리 좋지 않은 방법으로 다중 업로드를 해보겠습니다.
@PostMapping("/upload")
public String saveFile(@RequestParam String itemName,
@RequestParam(name = "file") MultipartFile file,
@RequestParam(name = "file2") MultipartFile file2) throws IOException {
if (!file.isEmpty()) {
String fullPath = fileDir + file.getOriginalFilename();
file.transferTo(new File(fullPath));
}
if (!file2.isEmpty()) {
String fullPath = fileDir + file2.getOriginalFilename();
file2.transferTo(new File(fullPath));
}
return "upload-form";
}
보통 폼으로 데이터를 전송할 때는 전송하는 데이터들이 서로 연관이 있는 관계입니다.
어떤 상품을 등록한다고 가정할 때 상품이름, 상품이미지1, 상품이미지2 데이터를 폼으로 넘겨준다면 서로 연관이 있는 관계란 뜻입니다.
그런데 위 컨트롤러의 saveFile의 파라미터를 @RequestParam으로 받아버린다면 저것을 구현한 개발자가 아닌 또 다른 개발자가 본다면 서로 연관이 있는 데이터가 아니라고 생각할 수도 있습니다.
따라서 @RequestParam을 사용해서 데이터를 각각 받는 것 보다는 Product 클래스를 만들고 상품이름, 상품이미지1, 상품이미지2를 필드로 지정한다음 @ModelAttribute를 사용해서 받으면 훨씬 깔끔해질 뿐만 아니라 다른 개발자가 이것을 봐도 데이터의 연관 관계를 쉽게 파악할 수 있습니다.
@RequestBody를 사용하지 않고 @ModelAttribute를 사용하는 이유는 클라이언트에서 전송하는 HTTP Request의 Content-Type이 multipart/form-data이기 때문입니다.
@RequestBody의 역할은 클라이언트가 보내는 HTTP 요청의 본문(JSON 또는 XML)을 HttpMessageConverter를 통해 Java 객체로 변환하는 것입니다. 더 정확히 말하면 HttpMessageConverter 인터페이스를 구현한 MappingJackson2HttpMessageConverter를 통해 Java 객체로 변환합니다.
그런데 이번에 다루는 내용은 Content-Type이 application/json이 아닌 multipart/form-data이기 때문에 @ModelAttribute를 사용해야 합니다. @ModelAttribute는 Content-Type이 x-www-form-urlencoded, multipart/form-data 일 때 사용합니다.
깔끔한 다중 파일 업로드 실습을 하기 전에 HTML input을 수정하겠습니다.
현재는 파일 선택에서 1개씩만 파일을 선택할 수 있지만, 실제 서비스를 이용하다보면 파일 선택에서 여러 개의 파일을 선택합니다. 1개씩만 선택할 수 있다면 사용자의 입장에서는 불만이 많겠죠.
여러 개의 파일을 선택하기 위해서는 type="file"을 유지한 채 multiple 옵션을 주고 name에는 배열 형식으로 이름을 지정해줘야 합니다. (itemImgList[])
<div class="container">
<div class="py-5 text-center">
<h2>상품 등록 폼</h2>
</div>
<h4 class="mb-3">상품 입력</h4>
<form th:action method="post" enctype="multipart/form-data">
<ul>
<li>상품명 <input type="text" name="itemName"></li>
<li>파일<input type="file" name="itemImgList[]" multiple></li>
</ul>
<input type="submit"/>
</form>
</div> <!-- /container -->
그럼 Product 클래스를 만들고 @ModelAttribute를 사용해보겠습니다.
롬복 @Data를 사용하여 Getter, Setter를 생성했습니다.
위의 input의 name="itemImgList[]" 였으므로 List<MultipartFile>로 이를 받았습니다.
@Data
public class ProductDomain {
private String itemName;
private List<MultipartFile> itemImgList;
}
@PostMapping("/upload")
public String saveFile(@ModelAttribute ProductDomain productDomain,
HttpServletRequest request) throws Exception {
log.info("request={}", request);
if (productDomain == null) {
throw new Exception("전달받은 폼 데이터가 없음");
}
log.info("mutipartList = {}", productDomain.getItemImgList());
for (MultipartFile file : productDomain.getItemImgList()) {
log.info("file name = {}", file.getOriginalFilename());
}
return "upload-form"
}
저는 파일 2개를 선택했고 아래 로그에서 잘 가져오는 것을 확인했습니다.