파일 업로드
파일 업로드 기능 만들기
왜 ?
지금 만들고 있는 프로젝트는 온라인 동영상 스트리밍 서비스다. 넷플릭스나 왓챠같이 모바일 앱은 지원하지 않지만 이 프로젝트를 하나씩 완성해나가면 좋은 경험이 될 것 같아서 진행중이다. 아무튼 이 프로젝트에서 가장 기본적으로 되어야 하는 부분이라고 생각했던 것이 동영상 업로드 부분이다.
원하는 수준
대용량 동영상을 업로드 되어야 한다.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);
}
}
}