Spring - File Upload 개발요약정리

들어가기

이번에는 Spring에서 File Upload를 개발할때 필요한 라이브러리, 개념, 특정 기술들을 정리하겠다. 업로드할 파일은 이미지 파일과 일반파일로 나뉘며, 저용량 파일업로드를 기준한다. 완전한 실습이 아니라 요약정리임을 미리 알린다.(DB없이 실제 파일의 업로드만...)


필요한 라이브러리

pom.xml 파일에 아래 라이브러리를 추가한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- 파일업로드 관련 : 이미지 썸네일 생성 라이브러리-->
<dependency>
<groupId>org.imgscalr</groupId>
<artifactId>imgscalr-lib</artifactId>
<version>4.2</version>
</dependency>

<!-- 파일업로드 관련-->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3.2</version>
</dependency>


스프링 mulipartResolver 선언

화면단에서 mutipart/form-date방식으로 서버에 전송되는 데이터를 스프링 MVC의 mulipartResolver로 처리할수 있다.

servlet-context.xml 파일에 아래와 내용을 추가하자

1
2
3
4
<!-- 파일 업로드로 들어오는 데이터를 처리하는 객체 -->
<beans:bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<beans:property name="maxUploadSize" value="10485760" /> <!-- 10mb 제한 -->
</beans:bean>


업로드할 파일위치 지정하기

업로드할 파일의 위치를 지정해야 한다.

servlet-context.xml 파일을 열어서 아래 내용을 추가하자.

1
2
3
4
<!-- 업로드된 파일의 저장 위치 지정 -->
<beans:bean id="uploadPath" class="java.lang.String">
<beans:constructor-arg value="D:\\SpringUploadRepo\\upload"></beans:constructor-arg>
</beans:bean>

maxUploadSize 프로퍼티를 통해 업로드할 파일의 용량을 제한할수 있다.


web.xml 파일의 한글파일 인코딩 처리

한글파일이 업로드될때 파일명이 깨지는 것을 해결하기 위해 web.xml 파일에 아래 내용을 추가하자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- Ensure UTF-8 character encoding is used -->
<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>

<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>


서버에 파일을 저장할때 고려해야 할 사항들

1.파일업로드 방식 결정하기.

Post 방식으로 전송할지 아니면 ajax방식으로 전송할지 결정해야 한다. 아마 요즘은 주로 ajax방식을 사용하는것 같다.

2.파일이름 중복문제.

DB에 파일을 저장할수도 있지만, 일반적으로 파일시스템에 파일을 저장하게된다. 따라서 업로드 되는 파일의 이름의 중복을 해결할 방법이 필요하다. -> UUID로 해결가능

3.파일 저장경로에 대한문제.

Windows나 Linux등 운영체제에서 폴더내의 파일 개수가 너무 많아지게 되면, 속도저하 문제가 발생하게 된다. 특히 Windows의 파일 시스템의 경우 폴더내 최대 파일 개수의 제한이 있다.(100만단위가 넘어가긴 하지만...) 위 문제를 해결하기 위해서 보통 파일이 업로드 되는 시점별로 폴더를 관리한다.

예를 들어 2018년 9월 6일 파일이 업로드 되면, 그 파일은 특정 폴더의 경로의 /2018/09/06/ 경로에 저장하면 위 문제를 해결 할수있다. 즉 업로드 할때 파일을 저장할 폴더의 유무에 따라 폴더 생성로직이 필요하다.

4.이미지파일의 경우 썸네일(thumbnail) 생성.

이미지파일인 경우 저장된 파일을 다시 화면에 보여줄때, 보통 그 이미지파일의 썸네일파일을 보여주게된다. 따라서 이미지파일이 서버에 저장될때는 추가적으로 그 이미지파일의 썸네일파일을 생성해 주어야 한다. 앞서 위에 추가한 라이브러리중 imgscalr-lib가 이미지의 썸네일 생성을 해준다.


서버에 파일을 저장할 유틸리티 클래스 생성

MediaUtils.java 생성

이 유틸리티 파일은 이미지파일을 걸러주는 유틸리티 파일이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.hanumoka.sample.util;

import java.util.HashMap;
import java.util.Map;

import org.springframework.http.MediaType;

//업로드한 파일중 이미지 파일만 거르는 클래스
public class MediaUtils {

private static Map<String, MediaType> mediaMap;

static {
mediaMap = new HashMap<String, MediaType>();
mediaMap.put("JPG", MediaType.IMAGE_JPEG);
mediaMap.put("GIF", MediaType.IMAGE_GIF);
mediaMap.put("PNG", MediaType.IMAGE_PNG);
}//

public static MediaType getMediaType(String type) {
return mediaMap.get(type.toUpperCase());
}//

}

UploadFileUtils.java 생성

이 파일을 실질적으로 업로드된 파일을 저장한다. 중점적으로 볼 함수는 uploadFile 이다. 화면단에서 전달받은 파일정보를가지고 스프링의 컨트롤러는 UploadFileUtils.uploadFile를 호출함으로써 파일을 저장하게 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
package com.hanumoka.sample.util;

import java.awt.image.BufferedImage;
import java.io.File;
import java.text.DecimalFormat;
import java.util.Calendar;
import java.util.UUID;

import javax.imageio.ImageIO;

import org.imgscalr.Scalr;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.FileCopyUtils;

public class UploadFileUtils {

private static final Logger logger = LoggerFactory.getLogger(UploadFileUtils.class);

public static String uploadFile(String uploadPath, String originalName, byte[] fileData) throws Exception {

//겹쳐지지 않는 파일명을 위한 유니크한 값 생성
UUID uid = UUID.randomUUID();

//원본파일 이름과 UUID 결합
String savedName = uid.toString() + "_" + originalName;

//파일을 저장할 폴더 생성(년 월 일 기준)
String savedPath = calcPath(uploadPath);

//저장할 파일준비
File target = new File(uploadPath + savedPath, savedName);

//파일을 저장
FileCopyUtils.copy(fileData, target);

String formatName = originalName.substring(originalName.lastIndexOf(".")+1);

String uploadedFileName = null;

//파일의 확장자에 따라 썸네일(이미지일경우) 또는 아이콘을 생성함.
if(MediaUtils.getMediaType(formatName) != null) {
uploadedFileName = makeThumbnail(uploadPath, savedPath, savedName);
}else {
uploadedFileName = makeIcon(uploadPath, savedPath, savedName);
}

//uploadedFileName는 썸네일명으로 화면에 전달된다.
return uploadedFileName;
}//

//폴더 생성 함수
@SuppressWarnings("unused")
private static String calcPath(String uploadPath) {

Calendar cal = Calendar.getInstance();

String yearPath = File.separator + cal.get(Calendar.YEAR);

String monthPath = yearPath + File.separator + new DecimalFormat("00").format(cal.get(Calendar.MONTH)+1);

String datePath = monthPath + File.separator + new DecimalFormat("00").format(cal.get(Calendar.DATE));

makeDir(uploadPath, yearPath, monthPath, datePath);

logger.info(datePath);

return datePath;
}//calcPath

//폴더 생성 함수
private static void makeDir(String uploadPath, String... paths) {

if(new File(uploadPath + paths[paths.length -1]).exists()) {
return;
}//if

for(String path : paths) {

File dirPath = new File(uploadPath + path);

if(!dirPath.exists()) {
dirPath.mkdir();
}//if

}//for

}//makeDir

//음??? 아이콘? 이미지 파일이 아닌경우 썸네일을 대신?
private static String makeIcon(String uploadPath, String path, String fileName) throws Exception{
String iconName = uploadPath + path + File.separator + fileName;
return iconName.substring(uploadPath.length()).replace(File.separatorChar, '/');
}

//썸네일 이미지 생성
private static String makeThumbnail(String uploadPath, String path, String fileName) throws Exception {

BufferedImage sourceImg = ImageIO.read(new File(uploadPath + path, fileName));

BufferedImage destImg = Scalr.resize(sourceImg, Scalr.Method.AUTOMATIC, Scalr.Mode.FIT_TO_HEIGHT, 100);

String thumbnailName = uploadPath + path + File.separator + "s_" + fileName;

File newFile = new File(thumbnailName);
String formatName = fileName.substring(fileName.lastIndexOf(".")+1);

ImageIO.write(destImg, formatName.toUpperCase(), newFile);

return thumbnailName.substring(uploadPath.length()).replace(File.separatorChar, '/');
}


}


예제소스

위 설정과 유틸리티를 만들었다면, 아래예제 화면2개, 컨트롤러1 파일 업로드 테스트가 가능하다. thymeleaf를 사용한 예제이다. jsp에 html만 옮기고 url만 잡아주면 동작할 것이다.

1.Post방식으로 파일업로드.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
data-layout-decorate="~{sample/layout/sampleLayout}"
>

<head>
<script th:inline="javascript">
/*<![CDATA[*/
$(function () {


});

/*]]>*/
</script>
</head>

<div layout:fragment="content">
<!-- Content Wrapper. Contains page content -->
<div class="content-wrapper">
<!-- Content Header (Page header) -->
<section class="content-header">
<h1>
파일 업로드 테스트1
<small>Optional description</small>
</h1>
<ol class="breadcrumb">
<li><a href="#"><i class="fa fa-dashboard"></i> 예제</a></li>
<li class="active">read</li>
</ol>
</section>

<!-- Main content -->
<section class="content container-fluid">

<!--------------------------
| Your Page Content Here |
-------------------------->

<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title">파일업로드 테스트 1</h3>
</div>
<!-- /.box-header -->
<!-- form start -->
<!-- <form id="form1" action="/sample/upload/uploadForm" method="post" enctype="multipart/form-data"> -->
<form id="form" action="/sample/upload/uploadForm" method="post" enctype="multipart/form-data">
<div class="box-body">
<div class="form-group">
<!-- <label for="exampleInputBoardTitle">Title</label>
<input type="text" class="form-control"> -->
<input type="file" name="file"><input type="submit">
</div>
</div>
<!-- /.box-body -->

<div class="box-footer">
<button type="submit" class="btn btn-warning">제출</button>
<!-- <button type="submit" class="btn btn-danger">Remove</button>
<button type="submit" class="btn btn-primary">ListPage</button> -->
</div>
</form>
</div>
<!-- /.box -->

</section>
<!-- /.content -->
</div>
<!-- /.content-wrapper -->
</div>
</html>

2.ajax 파일 업로드 화면.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
data-layout-decorate="~{sample/layout/sampleLayout}"
>

<head>
<script th:inline="javascript">
/*<![CDATA[*/
$(function () {

$(".fileDrop").on("dragenter dragover", function(event){
event.preventDefault();
});

$(".fileDrop").on("drop", function(event){
event.preventDefault();

var files = event.originalEvent.dataTransfer.files;

var file = files[0];

//console.log(file);

var formData = new FormData(); // HTML5
formData.append("file", file);

$.ajax({
url: '/sample/upload/uploadAjax',
data: formData,
dataType: 'text',
processData: false,
contentType: false,
type: 'POST',
success: function(data){
//alert(data);
//서버로 파일을 전송한 다음에 그 파일을 다시 받아온다.?

//이미지 인경우 썸네일을 보여준다.
if(checkImageType(data)){
str = "<div>"
+ "<a href='/sample/upload/displayFile?fileName=" + getImageLink(data) + "'>"
+ "<img src='/sample/upload/displayFile?fileName=" + data + "'/>"
+ "</a>"
+ "<small data-src='" + data + "'>X</small></div>";
}else {
str = "<div>"
+ "<a href='/sample/upload/displayFile?fileName=" + data + "'>"
+ getOriginalName(data) + "</a>"
+ "<small data-src='" + data + "'>X</small></div>";
}//else

$(".uploadedList").append(str);
},
});// ajax

});


/* 컨트롤러로 부터 전송받은 파일이 이미지 파일인지 확인하는 함수 */
function checkImageType(fileName){
var pattern = /jpg$|gif$|png$|jpeg$/i;
return fileName.match(pattern);
}//checkImageType

//파일 이름 처리 : UUID 가짜 이름 제거
function getOriginalName(fileName){
if(checkImageType(fileName)){
return;
}

var idx = fileName.indexOf("_") + 1;
return fileName.substr(idx);

}//getOriginalName

//이미지 원본 링크 제공
function getImageLink(fileName){

if(!checkImageType(fileName)){
return;
}//if

var front = fileName.substr(0, 12);
var end = fileName.substr(14);

return front + end;

}//getImageLink


//업로드 파일 삭제 처리
$(".uploadedList").on("click", "small", function(event){

var that = $(this);

alert($(this).attr("data-src"));

$.ajax({
url: "/sample/upload/deleteFile",
type: "post",
data: {fileName:$(this).attr("data-src")},
dataType: "text",
success : function(result){
if(result == 'deleted'){
//alert("deleted");
that.parent("div").remove();
}//
},
});

});//uploadedList

});
</script>
<style>

.fileDrop{
width: 100%;
height: 200px;
border: 1px dotted blue;
}

small {
margin-left: 3px;
font-weight: bold;
color: gray;
}

</style>
</head>

<div layout:fragment="content">
<!-- Content Wrapper. Contains page content -->
<div class="content-wrapper">
<!-- Content Header (Page header) -->
<section class="content-header">
<h1>
Ajax 파일 업로드
<small>Optional description</small>
</h1>
<ol class="breadcrumb">
<li><a href="#"><i class="fa fa-dashboard"></i> 예제</a></li>
<li class="active">read</li>
</ol>
</section>

<!-- Main content -->
<section class="content container-fluid">

<!--------------------------
| Your Page Content Here |
-------------------------->

<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title">Ajax File Upload</h3>
</div>
<!-- /.box-header -->
<!-- form start -->
<!-- <form id="form1" action="/sample/upload/uploadForm" method="post" enctype="multipart/form-data"> -->
<form id="form" action="/sample/upload/uploadForm" method="post" enctype="multipart/form-data">
<div class="box-body">
<div class="form-group">
<div class="fileDrop"></div>
</div>
</div>
<!-- /.box-body -->

<div class="box-footer">
<!-- <button type="submit" class="btn btn-warning">제출</button> -->
<div class="uploadedList"></div>
</div>
</form>
</div>
<!-- /.box -->

</section>
<!-- /.content -->
</div>
<!-- /.content-wrapper -->
</div>
</html>

UploadController.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
package com.hanumoka.sample.controller;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.UUID;

import javax.annotation.Resource;

import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;

import com.hanumoka.sample.util.MediaUtils;
import com.hanumoka.sample.util.UploadFileUtils;

@Controller
@RequestMapping("/sample/upload/*")
public class UploadController {

private static final Logger logger = LoggerFactory.getLogger(UploadController.class);

@Resource(name = "uploadPath")
private String uploadPath;

@RequestMapping(value = "/uploadForm", method = RequestMethod.GET)
public String uploadFormGET() {
return "/sample/upload/uploadForm";
}

//Post 방식 파일 업로드
@RequestMapping(value = "/uploadForm", method = RequestMethod.POST)
public String uploadFormPOST(MultipartFile file, Model model) throws Exception {

logger.info("uploadFormPost");

if(file != null) {
logger.info("originalName:" + file.getOriginalFilename());
logger.info("size:" + file.getSize());
logger.info("ContentType:" + file.getContentType());
}

String savedName = uploadFile(file.getOriginalFilename(), file.getBytes());

model.addAttribute("savedName", savedName);

return "/sample/upload/uploadForm";
}

//업로드된 파일을 저장하는 함수
private String uploadFile(String originalName, byte[] fileDate) throws IOException {

UUID uid = UUID.randomUUID();

String savedName = uid.toString() + "_" + originalName;
File target = new File(uploadPath, savedName);

//org.springframework.util 패키지의 FileCopyUtils는 파일 데이터를 파일로 처리하거나, 복사하는 등의 기능이 있다.
FileCopyUtils.copy(fileDate, target);

return savedName;

}

//Ajax 파일 업로드
@RequestMapping(value="/sample/upload/uploadAjax", method = RequestMethod.GET)
public String uploadAjaxGET() {
return "/sample/upload/uploadAjax";
}


//Ajax 파일 업로드 produces는 한국어를 정상적으로 전송하기 위한 속성
@ResponseBody
@RequestMapping(value="/sample/upload/uploadAjax", method = RequestMethod.POST, produces = "text/plain;charset=UTF-8")
public ResponseEntity<String> uploadAjaxPOST(MultipartFile file) throws Exception {

logger.info("originalName:" + file.getOriginalFilename());
logger.info("size:" + file.getSize());
logger.info("contentType:" + file.getContentType());

//String savedName = uploadFile(file.getOriginalFilename(), file.getBytes());

//HttpStatus.CREATED : 리소스가 정상적으로 생성되었다는 상태코드.
//return new ResponseEntity<>(file.getOriginalFilename(), HttpStatus.CREATED);
return new ResponseEntity<>(UploadFileUtils.uploadFile(uploadPath, file.getOriginalFilename(), file.getBytes()), HttpStatus.CREATED);
}

//화면에 저장된 파일을 보여주는 컨트롤러 /년/월/일/파일명 형태로 입력 받는다.
// displayFile?fileName=/년/월/일/파일명
@ResponseBody
@RequestMapping(value="/sample/upload/displayFile", method = RequestMethod.GET)
public ResponseEntity<byte[]> displayFile(String fileName) throws Exception {

InputStream in = null;
ResponseEntity<byte[]> entity = null;

logger.info("File name: " + fileName);

try {
String formatName = fileName.substring(fileName.lastIndexOf(".")+1);

MediaType mType = MediaUtils.getMediaType(formatName);

HttpHeaders headers = new HttpHeaders();

in = new FileInputStream(uploadPath + fileName);


if(mType != null) {
headers.setContentType(mType);
}else {
fileName = fileName.substring(fileName.indexOf("_")+1);
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
headers.add("Content-Disposition", "attachment; filename=\"" + new String(fileName.getBytes("UTF-8"), "ISO-8859-1") + "\"");
}// else

entity = new ResponseEntity<byte[]>(IOUtils.toByteArray(in), headers, HttpStatus.CREATED);

} catch(Exception e) {
e.printStackTrace();
entity = new ResponseEntity<byte[]>(HttpStatus.BAD_REQUEST);
} finally {
in.close();
}

return entity;
}// displayFile


//업로드된 파일 삭제 처리
@ResponseBody
@RequestMapping(value="/sample/upload/deleteFile", method = RequestMethod.POST)
public ResponseEntity<String> deleteFile(String fileName) throws Exception {

logger.info("delete file:" + fileName);

String formatName = fileName.substring(fileName.lastIndexOf(".")+1);

MediaType mType = MediaUtils.getMediaType(formatName);

if(mType != null) {
String front = fileName.substring(0, 12);
String end = fileName.substring(14);
new File(uploadPath + (front+end).replace('/', File.separatorChar)).delete();
}//if

new File(uploadPath + fileName.replace('/', File.separatorChar)).delete();

return new ResponseEntity<String>("deleted", HttpStatus.OK);

}

}

동작예시

참고자료

코드로 배우는 스프링 웹 프로젝트(남가람북스)