Spring MVC/02.Spring @MVC

[SpringBoot]file upload /download 처리

  • -

웹 애플리케이션을 작성하다 보면 파일을 업로드 하거나 다운로드 하는 일은 매우 빈번한 일이다. 이번 포스트에서는 SpringBoot를 이용해서 file을 upload 및 다운로드하는 방법에 대해 알아보자.

 

 

이번 프로젝트는 다음의 환경에서 테스트 되었다.

 

file upload와 관련된 설정은 하단의 spring.servlet.multipart 관련 부분이다. 내용은 주석을 참고한다.

server: port: 9090 servlet: encoding: force-response: true spring: datasource: driver-class-name: org.h2.Driver password: '' url: jdbc:h2:~/spring-test username: sa h2: console: enabled: true path: /h2-console jpa: database-platform: org.hibernate.dialect.H2Dialect hibernate: ddl-auto: validate properties: hibernate: format_sql: true show-sql: true mustache: suffix: .html servlet: multipart: location: c:/Temp/ # file upload 되는 경로 max-file-size: 10MB # 단일 파일의 최대 크기 max-request-size: 50MB # 하나의 요청에 포함된 전체 크기

 

한번의 요청에 여러 개의 파일을 등록할 수 있는 구조로 작성할 계획이므로 upload에는 main 정보, uploadfile에는 detail 정보를 저장하자.

 

form의 submit 과 ajax를 이용해서 처리해보자. form을 만들 때 중요한 점은 enctype="multipart/form-data" 속성을 추가해줘야 한다. 또한 파일 전송은 method가 post만 가능하다. 추가로 업로드된 파일이 image 계열이라면 "업로드 결과"의 <img>태그에서 보여주는데 경로명이 uploads로 되어있음에 기억해두자. 

코드는  mustache 기반으로 작성되었으므로 사용하는 template engine에 따라 작성해주자.

<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Insert title here</title> <style> .upload-images { display: flex; } .upload-images > div{ border-right: 1px solid blueviolet; padding: 0 10px; } .upload-images img{ width: 100px; } </style> </head> <body> <h1>file upload home 한글</h1> <hr> <form method="post" action="/upload" enctype="multipart/form-data"> <input type="file" name="files" id="files" multiple> <input type="text" name="info" id="info" placeholder="파일 정보"><br> <input type="submit" value="form 업로드"> <input type="button" value="ajax 업로드" id="ajax"> </form> <hr> <h1>업로드 결과</h1> <div id="ajaxresult" class="upload-images"> {{#uploads}} <div> <a href="/download?filename={{genFileName}}">{{orgFileName}}</a><br> {{#img}} <img src="/uploads/{{genFileName}}"> {{/img}} </div> {{/uploads}} </div> </body> </html>

 

만약 ajax를 이용한다면 fetch 처리해볼 수도 있다. ajax로 upload를 처리할 때는 FormData 객체를 이용해야 한다. 서버에서  FormData를 이용한 전송을 처리하기 위해서는 @RequestParam (또는 @ModelAttribute)를 사용해야 한다.

<script> document.querySelector("#ajax").addEventListener("click", async function () { let formData = new FormData(); let files = document.querySelector("#files").files; for (let i = 0; i < files.length; i++) { formData.append("files", files[i]); } formData.append("info", document.querySelector("#info").value); let response = await fetch("/uploadajax", { method: "post", body: formData }); let json = await response.json(); ajaxresult.innerHTML = ""; json.forEach(info => { let html = `<div><a href="/download?filename=${info.genFileName}">${info.orgFileName}</a><br>`; if (info.img) { html+= `<img src="/uploads/${info.genFileName}">`; } html+="</div>" ajaxresult.innerHTML += html; }) }) </script>

 

 

업로드된 파일의 확인을 위해 uploads 경로로 요청했던 내용을 기억할 것이다. 이를 위해 WebMvcConfigurer에 addResourceHandlers를 재정의해서 요청 경로와 물리적인 파일 경로를 매핑해주자.

@SpringBootApplication public class FileuploadDownloadApplication implements WebMvcConfigurer{ @Value("${spring.servlet.multipart.location}") String filePath; public static void main(String[] args) { SpringApplication.run(FileuploadDownloadApplication.class, args); } @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // addResourceLocations 등록 시 어떤 방식으로 접근하는지(file or classpath) 설정 registry.addResourceHandler("/uploads/**").addResourceLocations("file:"+filePath); } }

 

main 정보를 관리하는 UploadDto이다. UploadDto는 여러 개의 MultiPartFile 타입 정보를 가지므로 List형태로 files를 갖는다.

package com.quietjun.example.dto; import java.util.List; import org.springframework.web.multipart.MultipartFile; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; @Data @AllArgsConstructor @NoArgsConstructor @Builder public class UploadDto { private String info; List<MultipartFile> files; }
package com.quietjun.example.entity; @Entity @Table(name = "upload") @Data @AllArgsConstructor @NoArgsConstructor @Builder public class UploadEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private long no; private String info; @OneToMany(mappedBy = "upload", cascade = CascadeType.ALL, fetch = FetchType.LAZY) @Builder.Default private List<FileEntity> files = new ArrayList<>(); public UploadEntity dtoToEntity(UploadDto dto, List<FileDto> fDtos) { this.info = dto.getInfo(); for(FileDto fileDto : fDtos) { FileEntity fileEntity = new FileEntity().dtoToEntity(fileDto); fileEntity.setUploadEntity(this); } return this; } }

 

다음은 개별 파일의 정보를 저장할 FileDto이다. 파일이 서버에 등록될 때 클라이언트가 전송한 파일의 이름을 그대로 사용하면 이름 충돌이 발생할 수 있다. 따라서 전송된 이름(orgFileName)과 별도로 유일한 파일 이름(genFileName)을 만들어서 사용해야 한다.

package com.quietjun.example.dto; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; @Data @AllArgsConstructor @NoArgsConstructor @Builder public class FileDto { private String orgFileName; // 원래 파일 이름 private String genFileName; // 생성된 unique 한 파일 이름 private String contentType; // 파일의 content-type public boolean isImg() { // 파일이 image 계열인지 확인 return contentType.startsWith("image/"); } }
package com.quietjun.example.entity; @Entity @Table(name = "uploadfile") @Data @AllArgsConstructor @NoArgsConstructor @Builder public class FileEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long fileNo; private String genFileName; private String orgFileName; private String contentType; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn private UploadEntity upload; public void setUploadEntity(UploadEntity upload) { if (this.upload != null) { this.upload.getFiles().remove(this); } this.upload = upload; if(this.upload!=null) { this.upload.getFiles().add(this); } } public FileEntity dtoToEntity(FileDto dto) { this.genFileName = dto.getGenFileName(); this.orgFileName = dto.getOrgFileName(); this.contentType = dto.getContentType(); return this; } }

 

다음은 upload를 처리할 Controller를 작성해보자.

@Controller @RequiredArgsConstructor @Slf4j public class FileController { @Value("${spring.servlet.multipart.location}") String filePath; private final UploadService uploadService; @GetMapping("/") public String index() { return "index"; } @PostMapping("/upload") public String upload(@ModelAttribute UploadDto dto, Model model) { log.debug("업로드 정보: {}, {}", dto, dto.getFiles()); List<FileDto> list = uploadService.save(dto); // 파일 저장 및 DB에 반영하기 model.addAttribute("uploads", list); return "index"; } @PostMapping("/uploadajax") @ResponseBody public List<FileDto> uploadajax(@ModelAttribute UploadDto dto, Model model) { log.debug("업로드 정보: {}", dto); List<FileDto> uploads = uploadService.save(dto); // 파일 저장 및 DB에 반영하기 model.addAttribute("uploads", uploads); return uploads; } @GetMapping("/download") @ResponseBody public ResponseEntity<Object> download(@RequestParam String filename) throws IOException { log.debug("다운로드 정보: {}", filename); Map<String, Object> config = uploadService.download(filename); HttpHeaders headers = new HttpHeaders(); headers.add(HttpHeaders.CONTENT_TYPE, config.get("contentType").toString()); headers.setContentDisposition(ContentDisposition.builder("attachment") .filename(config.get("realName").toString(), StandardCharsets.UTF_8) .build()); //headers.setCacheControl("no-cache, no-store, must-revalidate"); return new ResponseEntity<Object>(config.get("resource"), headers, HttpStatus.OK); } }

 

다음은 요청을 처리할 service이다. 

saveFile 메서드는 파일을 파일 서버에 저장하는 역할을 수행하는데 이름 충돌을 대비하여 unique 한 이름을 생성하기 위해 UUID_fileName의 형태를 취하고 있다.

package com.quietjun.example.service; @Service @RequiredArgsConstructor @Slf4j public class UploadService { @Value("${spring.servlet.multipart.location}") String filePath; private final UploadRepo repo; @Transactional public List<FileDto> save(UploadDto dto) { List<FileDto> fileDtos = saveFile(dto); // file server에 파일 저장 repo.save(new UploadEntity().dtoToEntity(dto, fileDtos)); // database에 파일 정보 저장 return fileDtos; } private List<FileDto> saveFile(UploadDto dto) { List<MultipartFile> files = dto.getFiles(); List<FileDto> uploads = new ArrayList<>(); // 화면에 업로드된 파일의 정보를 담기 위한 List if (files != null) { files.forEach(file -> { FileDto fDto = FileDto.builder().orgFileName(file.getOriginalFilename()) .contentType(file.getContentType()).build(); // unique한 이름 만들어주기 fDto.setGenFileName(UUID.randomUUID() + "_" + fDto.getOrgFileName()); try { File localFile = new File(filePath, fDto.getGenFileName()); // 원격지의 file을 서버의 localFile에 출력 file.transferTo(localFile); uploads.add(fDto); log.debug("파일 저장 완료: {}", localFile.getCanonicalPath()); } catch (IllegalStateException | IOException e) { throw new RuntimeException(e); } }); } return uploads; } public Map<String, Object> download(String filename) throws IOException { // 실제 물리 파일 정보 확인 Path unique = Paths.get(filePath + filename); String contentType = Files.probeContentType(unique); // 화면에 보여줄 파일 이름 생성 String realName = filename.substring(filename.indexOf("_") + 1); // 파일에 연결할 stream 구성 Resource resource = new InputStreamResource(Files.newInputStream(unique)); return Map.of("contentType", contentType, "realName", realName, "resource", resource); } }

 

repository에는 별로 볼게 없다. 그냥 기본의 JpaRepository를 사용하자.

package com.quietjun.example.repo; import org.springframework.data.jpa.repository.JpaRepository; import com.quietjun.example.entity.UploadEntity; public interface UploadRepo extends JpaRepository<UploadEntity, Long> { }

 

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.