스프링 MVC에서는 MultipartResolver를 설정하면 어플리케이션들어오는 Multipart 요청을 처리하여 파일 업로드를 쉽게 할 수 있다.
스프링 부트에서는 기본 빈으로 등록되어있기 때문에 추가적으로 등록해 줄 필요가 없다!
spring.servlet.multipart.max-file-size=1MB
spring.servlet.multipart.max-request-size=10MB
file.dir=D:/test/
max-file-size
: 파일 하나의 최대사이즈 설정, 아무런 설정을 안하면 기본값이 1MB이다.
max-request-size
: Multipart 요청 하나에 여러 파일을 업로드 할 수 있는데, 그 전체 합의 크기를 설정해줄 수 있다. 기본값 10MB
file.dir
: file.dir 이 부분을 원하는 이름으로 적고 원하는 파일저장경로를 설정해주면 된다. 후에 @Value("${file.dir}")로 불러다가 사용할 예정이다. 만약 파일 저장 위치를 각기 다르게 해야할 경우, 원하는 경로로 여러개 만들어서 불러다가 사용하면 편리하다. 관리하기 편하라고 .properties에다가 설정해둔 것.
<!DOCTYPE HTML>
<head>
<meta charset="utf-8">
</head>
<body>
<h2>파일 업로드</h2>
<form action="/upload" method="post" enctype="multipart/form-data">
<h4>단일 파일 업로드</h4>
<input type="file" name="file">
<h4>다중 파일 업로드</h4>
<input type="file" multiple="multiple" name="files">
<input type="submit"/>
</form>
</body>
</html>
enctype="multipart/form-data"
로 설정을 해주어야 한다.multiple="multiple"
은 원래 type="file"의 input은 기본적으로 하나의 파일만 선택 가능한데 위의 속성을 적용해주면 여러 개의 파일을 선택할 수 있도록 해준다.@NoArgsConstructor
@Table(name = "file")
@Entity
public class FileEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name="file_id")
private Long id;
private String orgNm;
private String savedNm;
private String savedPath;
@Builder
public FileEntity(Long id, String orgNm, String savedNm, String savedPath) {
this.id = id;
this.orgNm = orgNm;
this.savedNm = savedNm;
this.savedPath = savedPath;
}
}
Spring Data JPA 이용
public interface FileRepository extends JpaRepository<FileEntity,Long> {}
@RequiredArgsConstructor
@Service
public class FileService {
@Value("${file.dir}")
private String fileDir;
private final FileRepository fileRepository;
public Long saveFile(MultipartFile files) throws IOException {
if (files.isEmpty()) {
return null;
}
// 원래 파일 이름 추출
String origName = files.getOriginalFilename();
// 파일 이름으로 쓸 uuid 생성
String uuid = UUID.randomUUID().toString();
// 확장자 추출(ex : .png)
String extension = origName.substring(origName.lastIndexOf("."));
// uuid와 확장자 결합
String savedName = uuid + extension;
// 파일을 불러올 때 사용할 파일 경로
String savedPath = fileDir + savedName;
// 파일 엔티티 생성
FileEntity file = FileEntity.builder()
.orgNm(origName)
.savedNm(savedName)
.savedPath(savedPath)
.build();
// 실제로 로컬에 uuid를 파일명으로 저장
files.transferTo(new File(savedPath));
// 데이터베이스에 파일 정보 저장
FileEntity savedFile = fileRepository.save(file);
return savedFile.getId();
}
}
@RequiredArgsConstructor
@Controller
public class TestController {
private final FileService fileService;
@GetMapping("/upload")
public String testUploadForm() {
return "test/uploadTest";
}
@PostMapping("/upload")
public String uploadFile(@RequestParam("file") MultipartFile file,@RequestParam("files") List<MultipartFile> files) throws IOException {
fileService.saveFile(file);
for (MultipartFile multipartFile : files) {
fileService.saveFile(multipartFile);
}
return "redirect:/";
}
}
작동 성공!
업로드할 때 작성해둔 컨트롤러에 추가로 작성해줄 것 이다.
@RequiredArgsConstructor
@Controller
public class TestController {
private final FileService fileService;
private final FileRepository fileRepository;
~ 기존 코드 ~
@GetMapping("/view")
public String view(Model model) {
List<FileEntity> files = fileRepository.findAll();
model.addAttribute("all",files);
return "view";
}
// 이미지 출력
@GetMapping("/images/{fileId}")
@ResponseBody
public Resource downloadImage(@PathVariable("fileId") Long id, Model model) throws IOException{
FileEntity file = fileRepository.findById(id).orElse(null);
return new UrlResource("file:" + file.getSavedPath());
}
// 첨부 파일 다운로드
@GetMapping("/attach/{id}")
public ResponseEntity<Resource> downloadAttach(@PathVariable Long id) throws MalformedURLException {
FileEntity file = fileRepository.findById(id).orElse(null);
UrlResource resource = new UrlResource("file:" + file.getSavedPath());
String encodedFileName = UriUtils.encode(file.getOrgNm(), StandardCharsets.UTF_8);
// 파일 다운로드 대화상자가 뜨도록 하는 헤더를 설정해주는 것
// Content-Disposition 헤더에 attachment; filename="업로드 파일명" 값을 준다.
String contentDisposition = "attachment; filename=\"" + encodedFileName + "\"";
return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,contentDisposition).body(resource);
}
}
UrlResource
로 파일을 읽어서 @ResponseBody
로 이미지 바이너리를 반환new UrlResource("file:" + "파일이 저장된 경로")
로 사용하면된다UriUtils.encode(file.getSavedNm(),~)
로 주었다면 실제 파일을 다운로드 받을 때 파일명이 uuid로 해놓은 파일명으로 나온다.<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h3>이미지 출력</h3>
<div th:each="imageFile : ${all} ">
<img th:src="|/images/${imageFile.id}|" width="150" height="150">
<p th:text="${imageFile.orgNm}"></p>
</div>
<h3>파일 다운로드</h3>
<div th:each="file : ${all}">
<a th:href="|/attach/${file.id}|" th:text="${file.orgNm}"></a>
</div>
</body>
</html>
파일 업로드는 대충 이런 식으로 흘러간다는 것을 기억하자.
밑의 다운로드는 content disposition의 attachment; filename="파일명"
에 변화를 주었을 때 어떻게 저장되는지 헷갈려서 테스트 해본 흔적이다.
References
인프런 김영한님 강의
혹시 DB file 테이블 구성좀 알려주실수 있나요?