스프링 MVC-스프링 파일 업로드
HTML 파일 업로드
HTML 폼을 통해 파일을 업로드하는 것은 생각보다 간단하지 않다. 이걸 이해하기 위해서는 먼저 HTML 폼을 통한 전송방식에 두 가지가 있다는 것을 알아야 한다.
먼저 우리가 HTML 폼을 사용할 때 자주 사용했던 Content-type : application/x-www-form-urlencoded , 그리고 파일을 담을 때 사용되는 Content-type : multipart/form-data가 있다.
application/x-www-form-urlencoded의 작동 방식은 아래와 같다.
헤더의 Content-type에 application/x-www-form-urlencoded를 넣고, 폼을 통해 입력받은 내용들을 쿼리 파라미터로 받아 메세지 바디에 넣어 전송한다.
multipart/form-data는 파일의 전송에 쓰이는데, 파일은 바이너리로 전환되어 전송되기 때문에 문자와 바이너리를 동시에 전송해야 하는 필요로 인해 만들어졌다.
작동 방식은 아래와 같다.
이 방식을 사용하기 위해선 Form 태그에 별도로 enctype=”multipart/form-data” 를 입력해주어야 한다.
boundary를 통해 구분된 항목에는 각 항목의 헤더가 따로 붙어있고, 일반 데이터는 항목 별로 문자가 전송되고 파일의 경우 파일 이름, Content-type이 추가되고 바이너리 데이터가 전송된다.
서블릿 파일 업로드
이와 같이 복잡한 구성으로 전달된 multipart 양식을 편리하게 다룰 수 있도록 서블릿에서는 여러 메서드들을 제공한다.
먼저 예제로 구현한 아래 코드를 확인하자.
@Controller
@Slf4j
@RequestMapping("/servlet/v2")
public class ServletUploadControllerV2 {
@Value("${file.dir}")
private String fileDir;
@GetMapping("/upload")
public String newFile() {
return "upload-form";
}
@PostMapping("/upload")
public String saveFileV2(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("==== parts ====");
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());
//데이터 읽기
InputStream inputStream = part.getInputStream();
String s = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
log.info("body={}",s);
//파일 저장
if(StringUtils.hasText(part.getSubmittedFileName())) {
String fullPath = fileDir + part.getSubmittedFileName();
log.info("파일 저장 fullPath={}", fullPath);
part.write(fullPath);
}
}
return "upload-form";
}
}
서블릿을 사용하여 파일을 폼을 통해 받아 서버에 업로드하는 코드이다. HttpServlerRequest의 getParts() 메서드를 통해 각 파트들에 손쉽게 접근할 수 있다. getParts()를 통해 받은 Part 객체에는 해당 파트를 읽을 수 있는 여러 편리한 메서드를 제공한다.
- part.getSubmittedFileName() : 클라이언트가 전달한 파일명
- part.getInputStream(): Part의 전송 데이터를 읽을 수 있다.
- part.write(…): Part를 통해 전송된 데이터를 저장할 수 있다.
위 코드에서는 getSubmittedFileName()를 통해 파일인지 확인하고, 파일이라면 파일 저장소 경로에 파일 이름을 더한 경로에 write()를 통해 파일을 저장해주었다.
스프링 파일 업로드
스프링에서는 서블릿보다 더 편리하게 파트들을 다룰 수 있다. 스프링에서 제공하는 MultipartFile 이라는 객체 덕분인데, 아래 코드를 확인해보자.
@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";
}
스프링에서는 @RequestParam, @ModelAttribute 등의 어노테이션을 통해 MultipartFile 객체를 주입받음으로써 파일을 손쉽게 받아올 수 있고, MultipartFile의 여러 메서드로 파일의 정보를 읽어올 수 있다.
주요 메서드들은 다음과 같다.
- file.getOriginalFilename() : 업로드 파일 명
- file.transferTo(…) : 파일 저장
스프링 파일 다운로드
파일 업로드 관련 예제를 하나 진행했지만 다 올리기엔 분량이 너무 커 생략하였다. 지금까지 배운 것의 응용이었고 핵심은 파일 자체가 아닌 경로를 통해 파일을 관리하는 것, 그리고 파일을 업로드 받은 파일 이름, UUID를 통해 생성한 겹치지 않는 파일 이름 두 가지를 갖도록 하여 같은 이름의 파일을 저장해도 서버에서 충돌이 나지 않도록 하는 것이다.
대신 스프링 파일의 다운로드에 대해 알아보자. 아래 코드를 확인하면 되는데 파일 이름들을 받아오는 부분을 제외하면 일종의 문법에 가깝기 때문에 외외두고 쓰거나 나중에 필요할 때 확인하고 사용해야 할 것 같다.
@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);
}
정리
HTML 폼을 통해 파일을 어떻게 전송하는지, 그리고 복잡한 multipart 형식으로 받은 요청을 서블릿과 스프링에서 어떻게 효과적으로 처리하는지를 배울 수 있었다.
파일의 업로드, 다운로드 파트를 끝으로 스프링 mvc 2편 강의가 끝이 났다. 여러 필요한 기능들을 깊이있게 배울 수 있어 역시 좋은 강의였다고 생각하지만, 아직도 스프링 부트, jpa 등 배울 것이 너무 많은 느낌이 든다.
일단 결제한 스프링 db 연결 강의까지는 들은 후 포트폴리오를 만들어봐야 할 것 같다. 프론트는 어떻게 처리할지 아직은 막막하다.
다음 포스팅은 db 강의를 들은 후 배운 내용을 정리해보도록 하겠다.