[Spring] 파일첨부
파일첨부 방식에는 페이지 이동방식(form태그 이용)과 Ajax 이용방식, iframe을 이용하는 방식이 있다.
이중 iframe을 이용하는 방식은 화면이 하나 더 뜬다는 점과, 보안에 취약해 대부분의 웹사이트에서 이용하지 않고 있는 방식이다.
파일 업로드 API
cos.jar
cos.jar을 이용하는 방식은 2002년 이후로 개발이 종료되었고, 더 이상 사용하지 않는 것을 권장한다.
commos-fileupload
commos-fileupload는 일반적으로 많이 활용되며, 서블릿 3.0 이전에도 사용 가능하다.
서블릿의 API상에서 지원
서블릿 버전 3.0 이상부터는 API상에서 파일 업로드를 지원한다.
서블릿 버전 변경 및 설정
pom.xml 버전변경
...
<!-- Servlet -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
...
위와 같이 서블릿 api 버전을 3.1.0버전으로 변경
web.xml 수정
<multipart-config>
<location>/Users/heekng/upload/temp</location>
<max-file-size>20971520</max-file-size>
<max-request-size>41943040</max-request-size>
<file-size-threshold>20971520</file-size-threshold>
</multipart-config>
<servlet>태그 안에 multipart-config를 추가
- <location> : 문제가 발생했을 때 파일이 저장되는 임시 경로
되도록 location의 경로는 파일 업로드 처리가 완료된 디렉토리 속에 temp폴더를 생성하여 설정하는 것이 좋다. - <max-file-size> : 업로드 되는 파일의 최대 크기(20M)
- <max-request-size> : 한번에 올릴 수 있는 최대 크기(40M)
- <file-size-threshold> : 20M의 메모리를 사용한다.
Form submit을 통한 파일 업로드
<form enctype="multipart/form-data">태그를 submit하여 파일을 저장한다면 페이지를 이동하는 과정을 거친다.
UploadController
@PostMapping("/uploadFormAction")
public void uploadFormPost(MultipartFile[] uploadFile) {
String uploadFolder="/Users/heekng/upload";
for(MultipartFile multipartFile : uploadFile) {
log.info("================");
log.info("업로드 파일명" + multipartFile.getOriginalFilename());
log.info("업로드 파일크기" + multipartFile.getSize());
File saveFile = new File(uploadFolder, multipartFile.getOriginalFilename());
try {
multipartFile.transferTo(saveFile);
} catch (Exception e) {
e.printStackTrace();
}
}
}
- 외부에서 여러 개의 파일이 전달될 수 있으므로 배열로 받는다.
MultipartFile타입으로 받는다. - foreach문을 이용하여 각 multipart객체를 순서대로 가져온다
- File객체에 경로와 multipartFile.getOriginalFilename()을 매개변수로 담아 빈 파일을 생성하고
- multipartFile.transferTo메소드를 이용하여 multipartFile에 담겨있는 파일을 새로 만든 파일에 담아준다.
Ajax를 통한 파일 업로드
input태그 속의 파일을 javascript ajax통신을 이용해 파일을 업로드하는 방법이다.
$("#uploadBtn").on("click", function(){
var formData = new FormData();
var inputFile = $("input[name='uploadFile']");
var files = inputFile[0].files;
console.log(files);
for(let i=0; i < files.length; i++){
if(!check(files[i].name, files[i].size)){
return false;
}
formData.append("uploadFile", files[i]);
}
$.ajax({
url: contextPath + "/uploadAjaxAction",
processData: false,
contentType : false,
data: formData,
type: "post",
dataType: "json",
success: function(result){
console.log(result);
showUploadFile(result.succeedList, uploadResult);
if(result.failureList.length != 0){
showUploadFile(result.failureList, uploadFail);
$(".uploadFail").show();
}else{
$(".uploadFail").hide();
}
$(".uploadDiv").html(cloneObj.html());
}
});
});
- FormData객체를 만들어 파일의 유효성 검사 이후 map방식으로 담는다.
- processData: false;는 ajax로 데이터를 보낼 때 내부적으로 쿼리스트링을 만들어주는데, true로 값을 주게되면 파일에 &가 있을 때 문제가 생기므로 false로 줘야 한다.
pom.xml Thumbnailator라이브러리 설치
용량이 큰 파일을 모바일과 같은 환경에서 처리한다면 많ㅇ느 데이터를 소비해야 한다.
따라서 특별한 경우가 아니라면 썸네일을 제작해야 한다.
하나의 이미지 파일을 업로드하면 Application에서 사용되는 다양한 해상도 썸네일로 만들어 낼 수 있으며, 다운로드 하지 않고도 미리보기가 가능하다.
<!-- Thumbnailator -->
<dependency>
<groupId>net.coobird</groupId>
<artifactId>thumbnailator</artifactId>
<version>0.4.8</version>
</dependency>
UploadCotroller
@PostMapping(value="/uploadAjaxAction", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
@ResponseBody
public ResponseEntity<AllFileDTO> uploadAjaxAction(MultipartFile[] uploadFile) {
log.info("upload ajax post........");
String uploadFolder = "/Users/heekng/upload";
String uploadFolderPath = getFolder();
File uploadPath = new File(uploadFolder, uploadFolderPath);
AllFileDTO allFile = new AllFileDTO();
List<AttachFileDTO> succeedList = new ArrayList<AttachFileDTO>();
List<AttachFileDTO> failureList = new ArrayList<AttachFileDTO>();
if(!uploadPath.exists()) {
uploadPath.mkdirs();
}
for(MultipartFile multipartFile : uploadFile) {
log.info("================");
log.info("업로드 파일명" + multipartFile.getOriginalFilename());
log.info("업로드 파일크기" + multipartFile.getSize());
AttachFileDTO attachDTO = new AttachFileDTO();
String uploadFileName = multipartFile.getOriginalFilename();
uploadFileName = uploadFileName.substring(uploadFileName.lastIndexOf("\\") + 1);
log.info("실제 파일 명 : " + uploadFileName);
attachDTO.setFileName(uploadFileName);
UUID uuid = UUID.randomUUID();
uploadFileName = uuid.toString() + "_" + uploadFileName;
InputStream in = null;
try {
File saveFile = new File(uploadPath, uploadFileName);
multipartFile.transferTo(saveFile);
in = new FileInputStream(saveFile);
attachDTO.setUuid(uuid.toString());
attachDTO.setUploadPath(uploadFolderPath);
log.info("들어옴");
if(checkImg(saveFile)) {
attachDTO.setImage(true);
FileOutputStream thumbnail = new FileOutputStream(new File(uploadPath, "s_" + uploadFileName));
Thumbnailator.createThumbnail(in, thumbnail, 100, 100);
thumbnail.close();
}
succeedList.add(attachDTO);
} catch (Exception e) {
failureList.add(attachDTO);
log.info("오류다");
log.info(e);
log.error(e.getMessage());
} finally {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
allFile.setSucceedList(succeedList);
allFile.setFailureList(failureList);
return new ResponseEntity<AllFileDTO>(allFile,HttpStatus.OK);
}
- 컨트롤러가 @Restcontroller가 아닐 경우이기 때문에 메소드에 @ResponseBody어노테이션을 할당한다.
- 전체적인 과정은 form을 이용하는 방식과 동일하다.
- UUID란 네트워크 상에서 각각의 객체들을 식별하기 위하여 사용된다.
중복될 가능성이 거의 없다고 인정되기 때문에 많이 사용한다.
UUID uuid = UUID.randoomUUID(); uuid.toString();의 형태로 사용한다.
파일 이름이 중복되더라도 이름 앖에 UUID를 붙여주며 중복될 가능성이 희박하게 만든다. - FileInputStream을 이용하여 저장한 파일을 불러온다.
- 저장한 파일이 이미지일 경우에 FileOutputStream을 이용하여 썸네일파일을 저장할 경로(파일)을 만든다.
Thumbnailator.createThumbnail(fileInputStream, fileOutputStream, x축px, y축px);으로 이미지 파일의 해상도를 변환한다.
private boolean checkImg(File file) throws IOException{
log.info("메소드들어옴");
log.info("file"+file);
boolean result;
MimetypesFileTypeMap mime = new MimetypesFileTypeMap();
log.info("check: "+mime.getContentType(file));
log.info("path: "+file.toPath());
log.info("check2: "+Files.probeContentType(file.toPath()));
if(mime.getContentType(file).contains("image")) {
result = true;
}else {
result = false;
}
return result;
// return Files.probeContentType(file.toPath()).startsWith("image");
}
- 파일의 확장자가 이미지인지 확인한다.
- 가장 쉽고 간단하게 확인하는 방법은 Files.probeContentType(file.toPath()).startsWith("image");를 이용한다.
하지만 probeContentType의 오류로 인해 null만 리턴되는 상황이 있다. - 때문에 MimetypesFileTypeMap을 이용하여 ContentType을 확인하고, boolean타입의 결과를 리턴했다.
private String getFolder() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
//현재 날짜에서 -를 \\로 변경해준다.
Date date = new Date();
String str = sdf.format(date);
return str.replace("-", File.separator); //separator : 파일 경로에서의 //
}
파일 다운로드
파일을 다운로드할 때에는 byte단위로 데이터를 전송하기 위해 MediaType.APPLICATION_OCTET_STREAM_VALUE로 데이터를 return 한다.
@GetMapping(value = "/download", produces=MediaType.APPLICATION_OCTET_STREAM_VALUE)
@ResponseBody
public ResponseEntity<Resource> downloadFile(String fileName, @RequestHeader("User-Agent") String userAgent) {//userAgent는 브라우저 정보를 받아온다.
log.info("downlopad file: " + fileName);
Resource resource = new FileSystemResource("/Users/heekng/upload/" + fileName);
log.info("resource : " + resource);
String resourceName = resource.getFilename();
String originalName = resourceName.substring(resourceName.indexOf("_")+1);
//파일을 전송할 때에는 UTF-8이 아닌 ISO-8859-1을 이용
HttpHeaders headers = new HttpHeaders();
//다운로드 시 저장되는 이름: Content-Disposition
try {
String downloadName = null;
if(userAgent.contains("Trident")) {
//Trident : MSIE
log.info("IE Browser");
downloadName = URLEncoder.encode(originalName, "UTF-8");
}else if(userAgent.contains("Edg")) {
//Edg : 엣지
downloadName = URLEncoder.encode(originalName, "UTF-8");
}else {
//그 외(크롬)
downloadName = new String(originalName.getBytes("UTF-8"), "ISO-8859-1");
}
System.out.println(userAgent);
headers.add("Content-Disposition", "attachment; filename=" + downloadName);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return new ResponseEntity<Resource>(resource, headers, HttpStatus.OK);
}
- 파일의 이름과 @RequestHeader("User-Agent")를 이용하여 브라우저 정보를 받아온다.
- FileSystemResource생성자에 파일 이름에 따른 파일 경로를 입력하여 resource 객체를 생성한다.
이 resource객체를 통해 파일의 정보를 받아온다. - userAgent를 판단하여 IE, 엣지에서는 URLEncoder.encode를 이용하여 파일명을 UTF-8로 인코딩해준다.
그 외의 브라우저는 기존의 파일명에서 getBytes("UTF-8")하고, ISO-8859-1로 저장한다.
new String(byte[], charset) : 해당 바이트 배열을 charset으로 설정한다. - HttpHeaders객체에.add("Content-Disposition", "attachment; filename=" + downloadName);하여 파일의 정보를 담는다.
Content-Disposition: 다운로드시 저장되는 이름
파일 삭제
@PostMapping("/deleteFile")
@ResponseBody
public ResponseEntity<String> deleteFile(String fileName, String type){
log.info("deleteFile : " + fileName);
File file = null;
try {
file = new File("/Users/heekng/upload/" + URLDecoder.decode(fileName, "UTF-8"));
file.delete();
if(type.equals("image")) {
String imageFileName = file.getPath().replace("s_", "");
file = new File(imageFileName);
file.delete();
}
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
return new ResponseEntity<String>(HttpStatus.NOT_FOUND);
}
return new ResponseEntity<String>("deleted", HttpStatus.OK);
}
- 매개변수로 fileName과 type을 받아온다.
- File객체에 삭제하려는 파일의 경로를 넣어 생성한다.
이 때 경로상에 역슬래시(\)와 같이 경로에 영향을 줄 수 있는 예외를 피하기 위해 인코딩했던 fileName을 URLDecoder.decode를 이용하여 "UTF-8"로 디코딩한다.
uploadAjax.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>File Upload - Ajax</title>
<style>
.uploadResult{
width: 100%;
}
.uploadResult ul{
display: flex;
justify-content: center;
}
.uploadResult ul li {
list-style: none;
padding: 10px;
}
.bigPicture{
text-align: center;
}
</style>
</head>
<body>
<h1>File Upload - Ajax</h1>
<div class="uploadDiv">
<input type="file" name="uploadFile" multiple>
</div>
<div class="uploadResult">
<h3>지원하는 형식입니다.</h3>
<ul></ul>
</div>
<div class="uploadFail" style="display:none;">
<h3>지원하지 않는 형식입니다.</h3>
<ul></ul>
</div>
<button id="uploadBtn">upload</button>
<div class="bigPictureWrapper">
<div class="bigPicture"></div>
</div>
</body>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script>
var check = false;
function showImage(fileCallPath){
//alert(fileCallPath);
if(check){return;}
$(".bigPictureWrapper").css("display", "flex").show();
$(".bigPicture").html("<img src='/upload/display?fileName=" + encodeURIComponent(fileCallPath) + "'>")
.animate({width:"100%", height:"100%"}, 3000);
check = true;
}
$(document).ready(function(){
var contextPath = "${pageContext.request.contextPath}"
var uploadResult = $(".uploadResult ul");
var uploadFail = $(".uploadFail ul");
var cloneObj = $(".uploadDiv").clone();
$(".bigPictureWrapper").on("click", function(e){
if(!check){return;}
$(".bigPicture").animate({width:"0%", height:"0%"}, 1000);
check = false;
setTimeout(function(){
$(".bigPictureWrapper").hide();
}, 1000)
});
function showUploadFile(uploadResults, tag){
str = "";
$(uploadResults).each(function(i, obj){
if(!obj.image){
var fileCallPath = encodeURIComponent(obj.uploadPath + "/" + obj.uuid + "_" + obj.fileName);
str += "<li><div><a href='/upload/download?fileName="+fileCallPath+"'><img src='/upload/resources/image/noImage.png'>" + obj.fileName + "</a>";
/*
data속성은 map구조로 DOM객체에서 사용할 수 있다.
<span data-key="value">
$("span").data("key") == "value"
*/
str += "<span data-file='"+fileCallPath+"' data-type='file'>x</div></span></li>";
}else{
//encodeURIComponent("문자열값")
//get방식으로 전송 시 파라미터로 전달할 때, 값에 인식할 수 없는 문자가 있을 경우 쿼리 스트링 문법에 맞게
//변경해야 한다.
//이 때 사용하는 메소드이다.
var fileCallPath = encodeURIComponent(obj.uploadPath + "/s_" + obj.uuid + "_" + obj.fileName);
var originPath = obj.uploadPath + "/" + obj.uuid + "_" + obj.fileName;
originPath = originPath.replace(new RegExp(/\\/g), "/");
str += "<li><div><a href=\"javascript:showImage(\'" + originPath + "\')\"><img src='/upload/display?fileName=" + fileCallPath + "'>" + obj.fileName + "</a>";
str += "<span data-file='"+fileCallPath+"' data-type='image'>x</div></span></li>";
}
});
$(tag).append(str);
}
$(".uploadResult").on("click", "span", function(e){
var targetFile = $(this).data("file");
var type = $(this).data("type");
var thisTag = $(this);
console.log(targetFile);
$.ajax({
url: contextPath + "/deleteFile",
data: {fileName:targetFile, type:type},
dataType:"text",
type:"post",
success:function(result){
alert(result);
$(thisTag).closest("li").remove();
}
});
})
function check(fileName, fileSize){
//확장자 확인 정규식
var regex = new RegExp("(.*?)\.(exe|sh|zip|alz)$");
var maxSize = 5242880; //5MB
if(regex.test(fileName)){
alert("업로드 할 수 없는 파일의 형식입니다.");
return false;
}
if(fileSize > maxSize){
alert("파일 사이즈 초과");
return false;
}
return true;
}
$("#uploadBtn").on("click", function(){
var formData = new FormData();
var inputFile = $("input[name='uploadFile']");
var files = inputFile[0].files;
console.log(files);
for(let i=0; i < files.length; i++){
if(!check(files[i].name, files[i].size)){
return false;
}
formData.append("uploadFile", files[i]);
}
$.ajax({
url: contextPath + "/uploadAjaxAction",
processData: false, //내부적으로 쿼리스트링을 만들어주는데 true로 하면 파일에&가있을때 문제가 생기므로 false로 줘야한다.
contentType : false,
data: formData,
type: "post",
dataType: "json",
success: function(result){
console.log(result);
showUploadFile(result.succeedList, uploadResult);
if(result.failureList.length != 0){
showUploadFile(result.failureList, uploadFail);
$(".uploadFail").show();
}else{
$(".uploadFail").hide();
}
$(".uploadDiv").html(cloneObj.html());
}
});
});
});
</script>
</html>
'Java & Kotlin > Spring' 카테고리의 다른 글
[Spring] UnmarshallingFailureException 에러 해결하기 (0) | 2022.06.06 |
---|---|
[Spring] Validation을 이용해 검증하기 (0) | 2022.05.29 |
[Spring] IDE에서 잘 접근되던 페이지가 서버에서는 접속되지 않을 때 (0) | 2022.04.30 |
[Spring] 트랜잭션 (0) | 2021.05.26 |
[Spring] AOP (0) | 2021.05.26 |
[Spring] REST Controller 댓글 기능 Rest방식 구현 (0) | 2021.05.23 |