파일 업로드
파일 업로드 기능 만들기
왜 ?
지금 만들고 있는 프로젝트는 온라인 동영상 스트리밍 서비스다. 넷플릭스나 왓챠같이 모바일 앱은 지원하지 않지만 이 프로젝트를 하나씩 완성해나가면 좋은 경험이 될 것 같아서 진행중이다. 아무튼 이 프로젝트에서 가장 기본적으로 되어야 하는 부분이라고 생각했던 것이 동영상 업로드 부분
이다.
원하는 수준
대용량 동영상
을 업로드 되어야 한다.HTTP
기본적인 구현(HTTP)
멀티파트 형식으로 데이터가 전송될 경우, 해당 데이터를 변환해주는 역할로 MultipartResolver가 필요하다.
commons-fileupload를 사용하던가 스프링에서 기본적으로 구현해놓은 StandardServletMultipartResolver를 사용해야하는데 후자를 선택해서 구현해본다.
-
메인 작성
@SpringBootApplication @Slf4j public class TestApiApplication { public static void main(String[] args) { SpringApplication.run(TestApiApplication.class, args); } @Bean public CommandLineRunner commandLineRunner(){ File saveFolder = new File("D:/upload"); if(!saveFolder.exists()) { log.info("D:/upload 폴더 생성 중..."); saveFolder.mkdir(); log.info("D:/upload 폴더 생성 완료!"); } return null; } }
-
컨트롤러 작성
@Controller @Slf4j public class FileUploadController { private String SAVE_PATH = "D:/upload"; @GetMapping("/uploadForm") public String getUploadForm() { return "uploadForm"; } @PostMapping(value = "/uploadFile", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public String upload(@RequestParam("file") MultipartFile multipartFile) throws IOException { log.info("contentType : {}", multipartFile.getContentType()); log.info("original name : {}", multipartFile.getOriginalFilename()); log.info("file size : {}", multipartFile.getSize()); File uploadFile = new File(SAVE_PATH + "/" + multipartFile.getOriginalFilename()); uploadFile.createNewFile(); try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(uploadFile)); BufferedInputStream bis = new BufferedInputStream(multipartFile.getInputStream());) { byte[] buffer = new byte[4096 * 2]; int read = 0; while ((read = bis.read(buffer)) != -1) { bos.write(buffer,0,buffer.length); } bos.flush(); } catch (Exception e) { log.info("{}", e); } return "redirect:/uploadForm"; } }
-
설정파일
spring: servlet: multipart: max-file-size: -1 max-request-size: -1
-
페이지 작성
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <script src="https://code.jquery.com/jquery-3.4.1.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script> <title>파일 업로드 페이지</title> </head> <body> <h1>파일을 업로드합니다. </h1> <form action="/uploadFile" method="post" enctype="multipart/form-data"> 업로드할 파일 : <input type="file" name="file" id="file" multiple><br> <input type="submit" value="등록하기"> </form> <br> 파일 사이즈 : <span id="fileSize"></span><br> 파일 확장자 : <span id="fileType"></span> <script> $(document).ready(function () { $('#file').change(function () { let file = $('#file').prop('files')[0]; $('#fileSize').html(Math.floor(file.size/(1024*1024))+'MB'); $('#fileType').html(file.type); }); }); </script> </body> </html>
-
테스트
집에 있는 4MB, 700MB, 1.6GB 동영상 하나씩 테스트를 하나씩 업로드하고 위에 코드외에
interceptor
를 구현해서 측정해보면 아래와 같은 결과가 나온다.4MB -> 1초 미만, 900MB ->10초 , 1.6GB -> 16초
개선하기
위에 테스트를 해보면 알겠지만 페이지에서 요청하는 순간부터 응답을 받기까지 시간이 엄청 길다. 1.6GB의 경우 처리하는데에는 16초이지만, 전체적인 시간을 측정했을 때 1분 20초 정도 나왔던 것 같다. 사람의 심리라는게 요청을 했는데 아무 동작도 없이 계속 기다리다 보니 체감상 더 느리게 느껴진다. 그래서 이러한 부분들을 조금 개선해보고자 페이지에 Progressbar를 추가해보려한다. 추가적으로 파일도 여러개 전송할 수 있도록 수정해보겠다.
Progressbar 추가
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<script src="https://code.jquery.com/jquery-3.4.1.min.js"
integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo="
crossorigin="anonymous"></script>
<!-- ##########################추가###############################-->
<script src="http://malsup.github.com/jquery.form.js"></script>
<title>파일 업로드 페이지</title>
<style>
.progress {
position: relative;
width: 400px;
border: 1px solid #ddd;
padding: 1px;
border-radius: 3px;
}
.bar {
background-color: #B4F5B4;
width: 0%;
height: 20px;
border-radius: 3px;
}
.percent {
position: absolute;
display: inline-block;
top: 3px;
left: 48%;
}
</style>
</head>
<body>
<h1>파일을 업로드합니다. </h1>
<form action="/uploadFile" method="post" enctype="multipart/form-data">
업로드할 파일 : <input type="file" name="file" id="file"><br>
<input type="submit" value="등록하기">
</form>
<br>
파일 사이즈 : <span id="fileSize"></span><br>
파일 확장자 : <span id="fileType"></span>
<!-- ##########################추가###############################-->
<div class="progress">
<div class="bar"></div >
<div class="percent">0%</div >
</div>
<div id="status"></div>
<script>
$(document).ready(function () {
$('#file').change(function () {
let file = $('#file').prop('files')[0];
$('#fileSize').html(Math.floor(file.size/(1024*1024))+'MB');
$('#fileType').html(file.type);
});
/*####################추가#######################*/
var bar = $('.bar');
var percent = $('.percent');
var status = $('#status');
$('form').ajaxForm({
// 전송 전
beforeSend: function() {
var percentVal = '0%';
bar.width(percentVal);
percent.html(percentVal);
},
// 업로드 시
uploadProgress: function(event, position, total, percentComplete) {
var percentVal = percentComplete + '%';
bar.width(percentVal);
percent.html(percentVal);
},
// // 완료 시
complete: function(xhr) {
alert(xhr.responseText);
window.location.reload();
}
});
});
</script>
</body>
</html>
컨트롤러의 return을 “Success”;로 변경해준다.
@PostMapping(value = "/uploadFile", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public String upload(@RequestParam("file") MultipartFile multipartFile) throws IOException {
log.info("contentType : {}", multipartFile.getContentType());
log.info("original name : {}", multipartFile.getOriginalFilename());
...
...
return "Success";
}
멀티 파일 업로드
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<script src="https://code.jquery.com/jquery-3.4.1.min.js"
integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo="
crossorigin="anonymous"></script>
<script src="http://malsup.github.com/jquery.form.js"></script>
<title>파일 업로드 페이지</title>
<style>
.progress {
position: relative;
width: 400px;
border: 1px solid #ddd;
padding: 1px;
border-radius: 3px;
}
.bar {
background-color: #B4F5B4;
width: 0%;
height: 20px;
border-radius: 3px;
}
.percent {
position: absolute;
display: inline-block;
top: 3px;
left: 48%;
}
</style>
</head>
<body>
<h1>파일을 업로드합니다. </h1>
<form id="fm" action="/uploadFile" method="post" enctype="multipart/form-data">
<!--#######################multiple 속성 추가#############################-->
업로드할 파일 : <input type="file" name="file" id="file" multiple><br>
<input type="submit" value="등록하기">
</form>
<br>
파일 사이즈 : <span id="fileSize"></span><br>
파일 확장자 : <span id="fileType"></span>
<div class="progress">
<div class="bar"></div >
<div class="percent">0%</div >
</div>
<div id="status"></div>
<script>
$(document).ready(function () {
/*################# 변경 ############################*/
$('#file').change(function () {
let files = $('#file').prop('files');
let len = files.length;
let totalSize = 0;
for(let i=0;i<len;i++){
totalSize += Math.floor(files[i].size/(1024*1024))
}
$('#fileSize').html(totalSize+'MB');
});
/*####################################################*/
var bar = $('.bar');
var percent = $('.percent');
var status = $('#status');
$('form').ajaxForm({
// 전송 전
beforeSend: function() {
var percentVal = '0%';
bar.width(percentVal);
percent.html(percentVal);
},
// 업로드 시
uploadProgress: function(event, position, total, percentComplete) {
var percentVal = percentComplete + '%';
bar.width(percentVal);
percent.html(percentVal);
},
// // 완료 시
complete: function(xhr) {
alert(xhr.responseText);
window.location.reload();
}
});
});
</script>
</body>
</html>
@Controller
@Slf4j
public class FileUploadController {
private String SAVE_PATH = "D:/upload";
@GetMapping("/uploadForm")
public String getUploadForm() {
return "uploadForm";
}
@PostMapping(value = "/uploadFile", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@ResponseBody
public String upload(@RequestParam("file") List<MultipartFile> multipartFile) throws IOException {
StringBuilder success = new StringBuilder("-----------upload success list ---------------");
StringBuilder fail = new StringBuilder("--------------upload fail list------------------");
multipartFile.forEach(file -> {
log.info("{}, {}, {}", file.getContentType(),file.getOriginalFilename(),file.getSize());
try {
fileUpload(file);
success.append("\n"+file.getOriginalFilename());
} catch (IOException e) {
e.printStackTrace();
fail.append("\n"+file.getOriginalFilename());
}
});
String ret = success.toString()+"\n"+fail.toString();
return ret;
}
private void fileUpload(MultipartFile multipartFile) throws IOException {
File uploadFile = new File(SAVE_PATH + "/" + multipartFile.getOriginalFilename());
uploadFile.createNewFile();
try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(uploadFile));
BufferedInputStream bis = new BufferedInputStream(multipartFile.getInputStream());) {
byte[] buffer = new byte[4096 * 2];
int read = 0;
while ((read = bis.read(buffer)) != -1) {
bos.write(buffer,0,buffer.length);
}
bos.flush();
} catch (Exception e) {
log.info("{}", e);
}
}
}