Spring MVC : 파일 업로드 및 다운로드 구현해보기
- Spring/Spring MVC
- 2022. 1. 8.
이 포스팅은 인프런 김영한님의 강의를 복습하며 정리한 내용입니다.
HTML FORM 전송 방식
- application/x-www-urlencoded
- multipart/form-data
HTML FORM 전송 방식은 위와 같이 두 가지(https://ojt90902.tistory.com/651)가 있다. 이름에서부터 유추가 가능하겠지만 multipart/form-data는 뭔가 여러 형태의 데이터를 전송해 줄 것이라는 것이 유추된다. 그와 대조적으로 application/x-www-urlencoded는 한 형태의 데이터를 전송해줄 것이라는 것이 유추된다.
application/x-www-urlencoded 전송 방식
- application/x-www-urlencoded는 HTML 폼 데이터를 전송하는 가장 기본적인 방법이다.
- 전송하고자 하는 데이터를 String 형태로 전송해준다.
- FORM 태그에 enctype 옵션이 없으면, 웹 브라우저는 자동으로 application/x-www-urlencoded를 헤더에 추가한다.
- 전송할 데이터를 쿼리 파라미터 형식으로 HTML Body에 넣어 전송해준다.
- username=kim&age=20
위의 이미지에서 볼 수 있듯이 application/x-www-urlencoded는 input-type = "text"에서 볼 수 있듯이 문자를 전송하는 방식이다. 이 방식으로는 파일을 전송할 수는 없다.
multipart/form-data 전송 방식
- multipart/form-data는 여러 형태의 자료(문자 / 바이너리)를 전송하기 위한 전송형태다. 여러 형태를 전송하기 때문에 multipart라는 이름을 가진다.
- multipart/form-data를 사용하기 위해서는 Form 태그에 enctype="multipart/form-data"를 입력해줘야한다.
multipart/form-data가 생성한 HTTP 메세지를 보면 application/x-www-urlencoded와 다르다는 것을 알 수 있다. 먼저 Content-type에 boundary라는 값이 보인다. 데이터는 이렇게 boundary로 나누어져서 전송되는 것을 알 수 있다.
전송되는 데이터들은 Boundary로 나누어지고, 이것을 'Part'라고 한다. 각 Part는 각 Part에 맞는 Header 정보와 Body값을 가진 채로 클라이언트에서 서버로 전송된다. 파일을 업로드하기 위해서는 'Part' 단위로 전송되는 이 값을 처리해주어야 한다.
MultiPart/form-data 사용 시, 서블릿은 바뀐다.
multipart/form-data를 사용하지 않으면 이 때 HttpServletRequest는 RequestFacade 형태로 사용이 된다. 그렇지만 multipart/form-data를 사용하게 되면 RequestFacade는 standardMultipartHttpServletRequest가 전달된다. 이것은 스프링의 Dispatcher Servlet이 중간에서 처리를 해주고 있는 것이다.
multipart/form-data = true인 경우, 스프링은 Dispatcher Servlet에서 MultiPartResolver를 실행한다. MultiPartResolver는 멀티파트 요청인 경우, 서블릿 컨테이너가 전달하는 일반전인 HttpServletRequest인 RequestFacade를 MultiPartHttpservletRequest 형태로 변환해서 넘겨준다.
서버에서 Part 값 받기(파일 업로드)
- 서블릿 기술 : HttpServletRequest에서 getPart로 받기
- 스프링 기술 : MultiPartFile로 받기
Part값을 받는 방법은 위에서 볼 수 있듯이 크게 두 가지 방법이 있다. 가장 원초적인 방법으로는 HttpServletRequest에서 getPart로 전송된 Part를 Collection 형태로 받을 수 있다. 또 한 가지 방법은 Parameter를 Binding하는 시점에 스프링이 지원하는 MultiPartFile로 받는 방법이다.
MultiPartFile로 받기 위해서는 @ModelAttibute를 이용하면 된다. HTML FORM 데이터는 모든 데이터를 요청 파라미터 형식으로 전달하기 때문이다. 이런 이유 때문에 MultiPartFile을 매개변수에서 받을 때는 @ModelAttribute도 생략할 수 있다. Spring은 @RequestBody가 아닌 경우, 자동적으로 기본형 자료에는 @RequestParam가, 나머지 자료에는 @ModelAttribute가 적용되기 때문이다.
파일 업로드와 관련있는 스프링의 설정 정보
spring.servlet.multipart.max-file-size=1MB
spring.servlet.multipart.max-request-size=10MB
spring.servlet.multipart.enabled=true
- max-file-size : 파일 하나 당 올릴 수 있는 최대 파일 용량
- max-request-size : 한번에 올릴 수 있는 전체 최대 파일 용량
- multipart.enabled : multipart/form-data를 사용할 수 있는지를 정한다.
application.properties를 통해서 파일 업로드와 관련된 스프링 설정을 처리할 수 있다. 위에 간략히 내용을 정리했다.
업로드 파일 저장 시 주의사항
업로드 된 파일을 저장할 때 주의할 사항이 있다. 서버는 하나지만, 클라이언트는 여러 군데에서 값을 올려준다. 서로 다른 클라이언트가 2.jpg라는 값을 각각 올려주면 별다른 조치를 하지 않으면 값은 소실될 수 밖에 없다. 이것을 해결하기 위해서 UUID를 이용해 파일을 저장하는 것을 추천한다.
서버에 저장될 파일 이름을 UUID로 만들어준다. 그리고 그 UUID로 만든 파일명이 업로드 파일명을 맵핑해준다. 위처럼 객체 형태로 파일명을 바인딩해서, 바인딩 된 파일명만 DB에 넣어주는 방식도 있다. 그리고 파일이 필요할 때는, 해당 객체를 DB에서 찾아와서 여기서 이름을 참조해서 값을 내려주는 것이다.
public UploadFile saveFile(MultipartFile file) throws IOException {
// 예외체크
if (!StringUtils.hasText(file.getOriginalFilename())) {
return null;
}
// 성공 로직
String originalFilename = file.getOriginalFilename();
String storeFileName = createStoreFileName(originalFilename);
String fullPath = findFinalPath(storeFileName);
file.transferTo(new File(fullPath));
return new UploadFile(storeFileName, originalFilename);
}
public String findFinalPath(String storeFileName) {
return fileDir + storeFileName;
}
public String createStoreFileName(String originalFilename) {
String uuid = UUID.randomUUID().toString();
String ext = extractExt(originalFilename);
return uuid + "." + ext;
}
public String extractExt(String originalFilename) {
int posi = originalFilename.lastIndexOf(".");
return originalFilename.substring(posi + 1);
}
@Data
@AllArgsConstructor
public class UploadFile {
private String storeFileName;
private String uploadFileName;
}
위는 업로드된 파일을 UUID 형태를 가진 파일명으로 치환하고, 그 이름으로 서버에 저장해준다. 그리고 서버에 저장한 후의 동작 결과물로 UploadFile이라는 기존 파일명, 저장된 파일명이 바인딩 된 객체를 전달해주는 형태를 가지는 식으로 동작한다.
파일 저장하기
- 서블릿 기술 : part.getWrite()로 저장
- 스프링 기술 : MultiPartfile.transferTo()로 저장
다른 형태의 기술이기 때문에 당연히 업로드 된 파일을 저장하는 것도 다르다. 서블릿 기술은 경로를 잡고 part를 getWrite()를 해주면 된다. MultiPartfile은 transferTo()로 저장을 할 수 있게 된다.
파일 다운로드(웹 랜더링 / PC 다운로드)
파일 다운로드는 크게 두 가지로 나누어진다. 하나는 실제로 클라이언트가 요청해서 파일을 PC로 받을 수 있는 형태다. 또 다른 하나는 우리가 웹에 랜더링 된 이미지를 볼 수 있는 것처럼, 단순히 웹에서만 볼 수 있는 방법이 있다.
기본적으로 자바 + 스프링에서 파일을 내려주려면 Resource 형태의 값을 내려주면 된다. 이 때, 주로 사용하는 방법은 UrlResource, ClassPathResource이다. UrlResource는 Prefix로 프로토콜을 명시해주고 해당 리소스의 위치를 알려주는 URL 방식을 통해 리소스를 내려준다.
Content-Disposition : attachment; filename="1.png"
파일을 실제로 PC에 다운받으려면 응답에 특정 Header를 포함시켜야한다. 만약, PC에서도 파일을 직접 다운 받을 수 있게 하고 싶다면 Content-Disposition : attachment; filename="파일명" 형식의 헤더를 반드시 응답에 포함시켜서 내려줘야한다. 여기서 지정된 파일명은 실제 PC에 다운로드 받아질 때의 파일명이 된다. 웹이 이미지를 렌더링하고 싶다면, 위 헤더를 포함하지 않고 응답을 내려주기만 하면 된다.
코드 실습
HttpServletRequest를 이용해 업로드 해보기
@PostMapping("/upload")
public String saveFileV2(HttpServletRequest request) throws ServletException, IOException {
String itemName = request.getParameter("itemName");
Collection<Part> parts = request.getParts();
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));
}
log.info("submittedFilename ={}", part.getSubmittedFileName());
log.info("size = {}", part.getSize()); // body 사이즈 읽기
// 파일을 String으로 인코딩해서 로그 출력
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);
}
}
- HttpServletRequest 형식으로 Part 데이터를 전달받는다.
- 이 때, Servlet은 MultipartHttpServletRequest다.
- MultiPartServletRequest는 getPart()를 통해 part를 제공한다. 그리고 part와 관련된 다양한 편의 메서드를 제공한다.
- 파일을 저장하는 것은 part.write()를 이용해서 저장한다. 이 때, 저장될 위치를 표기해주면 된다.
Spring의 MultiPartFile로 저장해보기
@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)); // 이렇게 하면 파일을 fullpath 경로에다가 저장을 해준다.
}
return "upload-form";
}
- 스프링은 파라미터 바인딩 시점에 MultiPartFile로 File을 바인딩해준다.
- @RequestParam, @ModelAttribute를 직접 작성해도 되고 생략해도 된다.
- MultiPartFile은 transferTo(...) 메서드를 이용해 저장한다.
MultiPartFile의 다운로드 (웹 이미지 렌더링)
@ResponseBody
@GetMapping("/images/{storeFileName}")
public Resource imageDownLoad(@PathVariable String storeFileName) throws MalformedURLException {
return new UrlResource("file:" + fileStore.findFinalPath(storeFileName));
}
- 스프링은 Resource 형식의 데이터를 내려주는 형식으로 파일의 다운로드를 실행해준다.
- HTML Body에 Resource 데이터를 넣어서 내려주면 된다.
- UrlResource는 "file : 절대경로" 형식으로 파일의 위치를 지정해서 파일이 내려올 수 있게 해준다.
MultiPartFile의 다운로드 (PC에 다운로드)
@GetMapping("/attach/{itemId}")
public ResponseEntity<Resource> attachFileDownload(@PathVariable Long itemId, HttpServletResponse response) throws MalformedURLException {
log.info("headfdfdf");
Item item = itemRepository.findById(itemId);
UploadFile attachFile = item.getAttachFile();
String storeFileName = attachFile.getStoreFileName();
log.info("storeFileName : {}", storeFileName);
UrlResource urlResource = new UrlResource("file:" + fileStore.findFinalPath(storeFileName));
String uploadFileName = attachFile.getUploadFileName();
String encode = UriUtils.encode(uploadFileName, StandardCharsets.UTF_8);
String contentDispostion = "attachment; filename=\"" + encode + "\"";
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, contentDispostion)
.body(urlResource);
}
- HTML Body에 동일하게 Resource를 내려준다.
- Resource는 UrlResource 형식으로 내려줬고, file: 절대경로 형식으로 설정해서 내려주었다.
- 이 때, CONTENT-DISPOSTION : attachment; filename = "파일명" 헤더를 넣어주어야 한다. 이 때, 파일명은 다운로드 될 때의 파일이름이 된다.
'Spring > Spring MVC' 카테고리의 다른 글
Spring MVC : Pageable 파라미터 받기 (0) | 2022.02.06 |
---|---|
타임리프 : 상대경로 / 절대경로 표현 (0) | 2022.02.03 |
Spring MVC : @Value 어노테이션 (0) | 2022.01.08 |
Spring MVC : Converter, Formatter 알아보기 (0) | 2022.01.07 |
Spring MVC : HTTP API 예외처리 (0) | 2022.01.06 |