기존에서는 파일 업로드는 파일 원본 이름, 파일 rename된 이름, 저장된 경로, 카테고리, 올린 사람(로그인한 사람 ID), 용량 등등 을 묶어서 서버로 보내진다.
Spring으로 단일 파일 업로드, 다중 파일 업로드에 대해서 알아보자.
일단 단일 파일 업로드의 html 파일부터 보자.
main.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1 align="center">파일 업로드 하기</h1>
<h3>single file 업로드</h3>
<form action="single-file" method="post" enctype="multipart/form-data">
파일 : <input type="file" name="singleFile"><br>
파일 설명 : <input type="text" name="singleFileDescription"><br>
<input type="submit" value="업로드">
</form>
</body>
</html>
화면 캡처

파일의 업로드는 3가지 방식으로 된다.
multipart/form-data 이라는 특정한 인코딩 형태일 때 -> MultipartFile 로 보낸다.2번째 방식으로 해볼 것이다. (html 파일에도 적어뒀다.)
이제 파일 업로드 컨트롤러를 보자.
FileUploadController
@Controller
public class FileUploadController {
@PostMapping("single-file")
public String singleFileUpload() {
return "redirect:/result";
}
@GetMapping("result")
public void result() {}
}
위에서 '업로드' 버튼을 누르면 "single-file" 에 해당하는 메소드를 매핑해서 찾아서 실행하고, singleFileUpload() 메소드는 redirect 방식으로 result() 메소드를 또 찾아서 호출한다.
왜 redirect 방식으로 할까?
사진을 올리면 resources/static에 일단 저장이 되고 몇 초 뒤에 build 안에 저장된다.
그리고 JVM이 이해하기 위해서는 build에 올라와야 한다.
하지만 static에 저장이 되고 바로 redirect를 하면 rebuild 하는 동안 알 수가 없기 때문에 JVM이 이미지를 찾을 수 없는 문제가 생긴다.
해결하기 위해서 새로운 폴더를 만들어서 규격을 정해서 build 안에 저장하는 방식을 알아보자.
FileUploadController에 추가
@Controller
public class FileUploadController {
private ResourceLoader resourceLoader;
@Autowired
public FileUploadController(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
중략...
}
build 된 곳에 파일 업로드 경로를 지정하기 위해서 ResourceLoader 의존성 주입을 받게 했다.
그리고 새로운 폴더에 만들 것이기 때문에 위에 컨트롤러 속 메소드인 singleFileUpload()에 추가하자.
@PostMapping("single-file")
public String singleFileUpload() throws IOException {
Resource resource = resourceLoader.getResource("classpath:static/uploadFiles/img/single");
System.out.println("파일 업로드 할 폴더의 절대 경로 : " + resource.getFile().getAbsolutePath());
return "redirect:/result";
}
resource를 만들고 폴더의 절대 경로를 출력해보았다.
일단 서버를 켜보면 build 속의 resources 폴더 안에도 변화가 생긴다.

출력된 문장

이제 추가해줘야 하는 것은 singleFileUpload() 메소드에 매개변수로 MultipartFile과 파일 설명에 대한 문자열이 들어가야 한다.
@PostMapping("single-file")
public String singleFileUpload(@RequestParam MultipartFile singleFile, @RequestParam String singleFileDescription) throws IOException {
System.out.println("singleFile = " + singleFile);
System.out.println("singleFileDescription = " + singleFileDescription);
Resource resource = resourceLoader.getResource("classpath:static/uploadFiles/img/single");
String filePath = resource.getFile().getAbsolutePath();
System.out.println("파일 업로드 할 폴더의 절대 경로 : " + filePath);
return "redirect:/result";
}
매개변수에 @RequestParam 으로 받게끔 했다.
아직은 시작하고 사진과 설명을 적어서 업로드해도 500 에러는 뜨겠지만, 출력은 된다.

근데 또 다른 문제가 있다.
사진을 업로드했을 때 같은 이름의 파일을 저장할 수 없기 때문에 (1), (2)와 같이 이름을 바꾸게 된다.
ex) apple.png를 올리면 apple(1).png, apple(2).png 형식으로 저장된다.
해결하는 방법
1. 날짜 형식으로 이름을 지어서 저장시켜준다.
2. Random한 이름을 지어서 넣어준다. (UUID) 활용.
여기서 나온 UUID에 대해서 간단 정리
UUID : 16진수로 표현되는 32자리의 숫자인데 랜덤으로 만들어준다. 주로 PK로 사용함.-> 이걸 마구잡이로 쓰면 좋지 않다.
왜인지 간단하게 말하면, MySQL의 InnoDB같은 스토리지 엔진은 우리가 봤을 때 값이 들어가는 것처럼 보이지만 그렇지 않다.
commit을 해줘야지 값이 반영이 되는 것이고, 눈에 보이는건 반영이 된 데이터들이 아니다.
즉, InnoDB에서 데이터는 commit 하기 전까지 Primary Key를 중심으로 클러스터링(비슷한 것들끼리 모임) 하는데 UUID로 random하게 마구잡이로 만들면 sql 쿼리 처리가 느려질 수 있다.
느려지는 이유 : Primary Key가 비슷한 애들끼리 클러스터링 되는데? UUID는 random이라 클러스터링 되지 않기 때문에
이걸 해결하는 방법을 알아보면, UUID는 16진수로 된 32자리 숫자라고 했는데 앞의 8자리를 'time_low' 라고 한다.
이 'time_low'를 우리가 아는 날짜 데이터로 하는 것이 하나의 해결 방법이다.
날짜 데이터 포맷 형식인 'YYYYMMDD' 형태로 8글자를 넣어버리면, 데이터를 넣은 날의 날짜끼리 클러스터링이 돼서 그나마 효율적이게 된다.
잠시 딴 길로 샜다.. 다시 컨트롤러로 넘어오자.
originFileName 변수ext 변수saveName 변수3가지를 작성해봤다. 출력도 해보겠다.
컨트롤러 속 singleFileUpload 메소드에 추가
String originFileName = singleFile.getOriginalFilename();
System.out.println("originFileName = " + originFileName);
String ext = originFileName.substring(originFileName.lastIndexOf("."));
System.out.println("ext = " + ext);
String saveName = UUID.randomUUID().toString();
System.out.println("saveName = " + saveName);
출력된 문장 사진

하나만 더 수정해보자.
saveName 을 아예 겹칠 일 없고, 파일이름의 형태로 만들어야 하니까
"-" << 하이푼을 없애고, 뒤에 ext 확장자를 추가해줄 것이다.
saveName은 아래처럼 변경
String saveName = UUID.randomUUID().toString().replace("-", "") + ext;
출력된 문장 사진

이제 해야할 것은
1번 과정부터 해보자.
singleFile을 내가 원하는 파일 경로인 filePath 와 저장된 이름 saveName을 이용해서 업로드를 해줘야 한다.
이것을 해주는 것이 transferTo() 메소드이다.
그것을 추가할 것이고, 예외처리도 생각해야하니까 예외문도 추가해줄 것이다.
singleFileUpload() 메소드에 추가
try {
singleFile.transferTo(new File(filePath + "/" + saveName));
/* 원래는 이 부분에서 비즈니스 로직 구문 작성 (서비스 계층 -> DB) */
} catch (Exception e) {
new File(filePath + "/" + saveName).delete();
}
비즈니스 로직을 성공했다고 치고 Redirect 된 페이지에 값을 넘기기 위해 RedirectAttributes 사용할 것이다. 이것도 추가해본다.
값을 넘기기 위해서 3번째 매개변수에 추가해주고 사용했다.
public String singleFileUpload(@RequestParam MultipartFile singleFile, @RequestParam String singleFileDescription, RedirectAttributes redirectAttributes) throws IOException {
(기존 코드)
try {
(기존 코드)
redirectAttributes.addFlashAttribute("message", "파일 업로드 성공.");
redirectAttributes.addFlashAttribute("img", "uploadFiles/img/single/" + saveName);
redirectAttributes.addFlashAttribute("singleFileDescription", singleFileDescription);
} catch (Exception e) {
new File(filePath + "/" + saveName).delete();
}
(기존 코드)
}
이렇게 하면 저장은 된다.
하지만 기존 코드에서 아직 반환 화면인 result는 작성하지 않았기에 500 에러가 뜬다.
확인하는 방법 : 경로에 파일을 저장을 하긴 했기 때문에 아래 사진과 같이 URL과 saveName을 적어주면 된다.

이렇게 나오는 것을 알 수 있다.
saveName이 출력된 문장

result 화면 만들어주기
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1 th:text="${message}"></h1>
<img th:src="${img}" alt="다람쥐">
<p th:text="${singleFileDescription}"></p>
</body>
</html>
이제 잘 나온다.

단일에서 만들었던 것에 추가하면서 만들어보겠다.
main.html
기존 코드 아래에 추가
<h3>multi file 업로드</h3>
<form action="multi-file" method="post" enctype="multipart/form-data">
파일 : <input type="file" name="multiFile" multiple><br>
파일 설명 : <input type="text" name="multiFileDescription"><br>
<input type="submit" value="업로드">
</form>
파일 : <input type="file" name="multiFile" multiple><br> 여기에 single과의 차이는 name="multiFile" 까지 적는 것이 아닌, 추가로 multiple을 적어줬다.
FileUploadController에 메소드 추가 (일부만 추가했음)
@PostMapping("multi-file")
public String multiFileUpload(@RequestParam List<MultipartFile> multiFiles, @RequestParam String multiFileDescription, RedirectAttributes redirectAttributes) throws IOException {
// Resource resource = resourceLoader.getResource("classpath:static/uploadFiles/img/multi");
// String filePath = resource.getFile().getAbsolutePath();
String filePath = resourceLoader.getResource("classpath:static/uploadFiles/img/multi").getFile().getAbsolutePath();
List<Map<String, String>> files = new ArrayList<>(); // 파일 하나에 대한 정보를 지닌 map을 모아둔 리스트
List<String> saveFiles = new ArrayList<>(); // 화면 단에서 img 태그가 참조할 정적 리소스 경로 (src 속성)
for (int i = 0; i < multiFiles.size(); i++) {
String originFileName = multiFiles.get(i).getOriginalFilename();
String ext = originFileName.substring(originFileName.lastIndexOf("."));
String saveName = UUID.randomUUID().toString().replace("-", "") + ext;
Map<String, String> file = new HashMap<>();
file.put("originFileName", originFileName);
file.put("saveName", saveName);
file.put("filePath", filePath);
file.put("multiFileDescription", multiFileDescription);
files.add(file);
multiFiles.get(i).transferTo(new File(filePath + "/" + saveName));
saveFiles.add("uploadFiles/img/multi/" + saveName);
}
return "redirect:/result";
}
files 변수에서 List<Map<String, String>> 는 파일 하나에 대한 정보를 지닌 map을 모아둔 리스트를 의미한다.
2가지 방식으로 리스트에 넣는 방법이 있긴 하다.
근데 만약에 내가 3개 파일을 한 번에 넣을건데 1, 2 번 파일은 저장됐고, 3번째 파일에서 에러가 생기면 그 때 다시 1, 2 번 파일을 삭제해주는 예외처리를 해줘야 한다.
위의 코드에 try-catch 문을 작성해주자
try {
for (int i = 0; i < multiFiles.size(); i++) {
String originFileName = multiFiles.get(i).getOriginalFilename();
String ext = originFileName.substring(originFileName.lastIndexOf("."));
String saveName = UUID.randomUUID().toString().replace("-", "") + ext;
Map<String, String> file = new HashMap<>();
file.put("originFileName", originFileName);
file.put("saveName", saveName);
file.put("filePath", filePath);
file.put("multiFileDescription", multiFileDescription);
files.add(file);
multiFiles.get(i).transferTo(new File(filePath + "/" + saveName));
saveFiles.add("uploadFiles/img/multi/" + saveName);
}
} catch (Exception e) {
for(int i = 0; i < saveFiles.size(); i++) {
Map<String, String> file = files.get(i);
new File(filePath + "/" + file.get("saveName")).delete();
}
}
saveFiles 는 문제 없이 파일 업로드된 파일 경로가 담겨 있다. (즉, 1, 2 번 파일이 저장돼 있다.
이제 추가로 예외가 발생하지 않았다면 출력해줄 메시지와
예외 발생 시의 메시지만 출력해주면 끝이다.
try문에서 for문이 지난 뒤에 추가해줄 코드
redirectAttributes.addFlashAttribute("message", "다중 파일 업로드 성공");
redirectAttributes.addFlashAttribute("imgs", saveFiles);
redirectAttributes.addFlashAttribute("multiFileDescription", multiFileDescription);
catch문에서 for문이 끝난 뒤에 추가해줄 코드
redirectAttributes.addFlashAttribute("message", "다중 파일 업로드 실패");
마지막으로 수정할 result.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1 th:text="${message}"></h1>
<div th:if="${img != null}">
<img th:src="${img}" alt="업로드한 사진">
<p th:text="${singleFileDescription}"></p>
</div>
<div th:else>
<th:block th:each="img: ${imgs}">
<img th:src="${img}" width="150" height="150"/>
</th:block>
<p th:text="${multiFileDescription}"><</p>
</div>
</body>
</html>
실행 결과

용량에 관해서는 application.yml 에 대해서 정해주면 된다
spring:
servlet:
multipart:
max-file-size: 10MB
max-request-size: 10MB