서버에서는 파일을 저장할 때 반드시 고려해야 하는 사항은 다음과 같다.
- 파일 이름의 중복 문제
- 파일의 저장 경로에 대한 문제
- 원본 파일을 그대로 보여주는 경우 브라우저에서 보여지는 파일의 크기 문제
- FAT32 : 단일 파일 4GB, 최대 볼륨32GB, 볼륨당 파일 수 4,117,920개, 폴더당 파일수 65,534 개
- NTFS : 단일 파일 16TB, 최대 볼륨 256TB, 볼륨당 파일 수 4,294,967,295 개, 폴더당 파일 수 4,294,967,295 개
대부분 프로젝트에서는 사용자가 업로드한 이미지 파일은 썸네일(thumbnail) 이미지라는 원본 이미지 파일의 축소본(썸네일 이미지)
을 만들어서 전송한다. 물론, 이러한 작업을 하는 가장 중요한 이유는 서버에서 최소한의 데이터를 전송하기 위해서이다.
● 파일 업로드용 클래스 설계하기
1) 자동적인 폴더의 생성이 가능하며,
2) 파일 저장은 UUID를 이용해서 처리하고,
3) 썸네일 이미지를 생성하는 기능이 처리
● 썸네일 생성하기
업로드 되는 파일은 크게 브라우저에서 보여지는 이미지 종류의 파일과 사용자가 다운로드 하는 형태의 파일로 구분될 수 있다.
이미지 타입의 파일 : JPG, GIF ,PNG, JPEG (BMP 는 흔히 사용되지 않으므로 일반 파일로 간주)
다운로드 타입의 파일 : 이미지 타입의 파일을 제외한 모든 종류
파일 저장 하기
프로젝트 밖에
년월일 별로 폴더가 생성된 것을 볼 수 있다.
서버내 디렉토리 주소로 설정 하면 된다.
servlet-context.xml
<beans:bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"> <beans:property name="maxUploadSize" value="10485760" /> </beans:bean> <!-- 서버의 파일 저장 경로 설정 --> <beans:bean id="uploadPath" class="java.lang.String"> <beans:constructor-arg value="D:\dev\spring_simple_blog\.metadata\.plugins\org.eclipse.wst.server.core\tmp1\wtpwebapps\upload" /> </beans:bean>
* 라이브러리 설정
http://mvnrepository.com/artifact/commons-fileupload/commons-fileupload
<!-- https://mvnrepository.com/artifact/commons-fileupload/commons-fileupload -->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3.3</version>
</dependency>
Imgscalr A Java Image Scaling Library
<!-- https://mvnrepository.com/artifact/org.imgscalr/imgscalr-lib -->
<dependency>
<groupId>org.imgscalr</groupId>
<artifactId>imgscalr-lib</artifactId>
<version>4.2</version>
</dependency>
UploadFileUtils
package config.uploadfile;
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);
private static Integer WIDTH_SIZE=100;
public static Integer getWIDTH_SIZE() {
return WIDTH_SIZE;
}
public static void setWIDTH_SIZE(Integer wIDTH_SIZE) {
WIDTH_SIZE = wIDTH_SIZE;
}
//1.파일의 저장 경로(uploadPath), 2.원본 파일의 이름(originalName), 3.파일 데이터(byte[])
public static String uploadFile(String uploadPath, String originalName, byte[] fileData) throws Exception{
//★ 1. 고유값 생성
UUID uid =UUID.randomUUID();
String savedName=uid.toString()+"_"+originalName;
//★ 2. 년/월/일' 정보 생성
String savedPath=calcPath(uploadPath);
//★ 3. 원본파일 저장
File target =new File(uploadPath+savedPath, savedName);
FileCopyUtils.copy(fileData, target);
//★ 4. 이미지 일경우 썸네일 이미지 생성 후 url 주소로 반환
String formatName=originalName.substring(originalName.lastIndexOf(".")+1);
String uploadedFileName=null;
if(MediaUtils.getMediaType(formatName)!=null){
//이미지일 경우 썸네일 생성 후 이미지 이름 반환 ( 경로+년월일+s_이름)
uploadedFileName=makeThumbnail(uploadPath, savedPath, savedName);
}else{
uploadedFileName=makeIcon(uploadPath, savedPath, savedName);
}
// 파일 경로를 -> url 경로로 변경해서 반환
return uploadedFileName;
}
// 이미지가 아닐경우 단지 파일 경로를 -> url 경로로 변경해서 반환
private static String makeIcon(String uploadPath, String savedPath, String savedName) {
String iconName=uploadPath+savedPath+File.separator+savedName;
return iconName.substring(uploadPath.length()).replace(File.separatorChar, '/');
}
//파일이 저장될 '년/월/일' 정보 생성
private static String calcPath(String uploadPath){
Calendar cal =Calendar.getInstance();
// 역슬래시 + 2017
String yearPath=File.separator + cal.get(Calendar.YEAR);
// /2017 +/+ 10 한자리 월 일경우 01, 02 형식으로 포멧
String monthPath=yearPath + File.separator + new DecimalFormat("00").format(cal.get(Calendar.MONTH)+1);
// /2017/10 +/ + 22
String datePath = monthPath+ File.separator+ new DecimalFormat("00").format(cal.get(Calendar.DATE));
//년월일 폴더 생성하기
makeDir(uploadPath, yearPath, monthPath, datePath);
logger.info(" datePath - {}", datePath);
return datePath;
}
// 실질 적인 날짜 폴더 생성
private static void makeDir(String uploadPath, String... paths){
if(new File(paths[paths.length-1]).exists()){
//년 월 일 에서 일 배열 paths 에서 paths -1 은 일 즉 해당일의 폴더가 존재하면 return
return ;
}
for(String path :paths){
File dirPath =new File(uploadPath+path);
if(!dirPath.exists()){
//년 월일에 대한 해당 폴더가 존재하지 않으면 폴더 생성
dirPath.mkdir();
}
}
}
//썸네일 이미지 생성하기
// 1.파일 경로 2. 년월일 경로, 3. 파일 이름
private static String makeThumbnail(String uploadPath, String path, String fileName) throws Exception{
// 파일 존재하는 이미지를 메모리상 이미지에 올려 붙인다. 즉 메모리에 로딩시킨다.
BufferedImage sourceImg= ImageIO.read(new File(uploadPath+path, fileName));
//메모리상 이미지를 정해진 크기에 맞게 복사한다.
BufferedImage destImage=Scalr.resize(sourceImg, Scalr.Method.AUTOMATIC,
Scalr.Mode.FIT_TO_HEIGHT, WIDTH_SIZE);
//썸네일 이미지 이름을 정한다. 썸네일 이미지를 앞에 s_ 붙인다.
String thumbnailName=uploadPath+path+File.separator+"s_"+fileName;
// 파일 경로의 객체를 생성한다.
File newFile=new File(thumbnailName);
// 경로의 마지막 에 . 으로 중심으로 분리해서 .jpg, png, jpeg gif 의 문자를 추출한다.
String formatName=fileName.substring(fileName.lastIndexOf(".")+1);
//실질적인 썸네일 이미지를 복사한다.
ImageIO.write(destImage, formatName.toUpperCase(), newFile);
// 파일 경로인 역슬러시를 url 의 경로인 슬러시로 변경해서 해서 반환시킨다.
return thumbnailName.substring(uploadPath.length()).replace(File.separatorChar, '/');
}
}
MediaUtils
package config.uploadfile;
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("JPEG", MediaType.IMAGE_JPEG);
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());
}
}
UploadController
//년월일 폴더로 분리후 이미지일 경우 썸네일 이미지 생성
@ResponseBody
@RequestMapping(value="/test/uploadAjax2", method=RequestMethod.POST, produces="text/plain;charset=UTF-8")
public ResponseEntity<String> uploadAjax2(MultipartFile file) throws Exception{
logger.info("originalName , {} ", file.getOriginalFilename());
//사이즈 가로 설정
UploadFileUtils.setWIDTH_SIZE(200);
String uploadFile=UploadFileUtils.uploadFile(uploadPath, file.getOriginalFilename(), file.getBytes());
logger.info("파일 저장후 반환 값 - , {} ", uploadFile);
return new ResponseEntity<String>(uploadFile, HttpStatus.CREATED);
}
uploadAjax.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
<style type="text/css">
.fileDrop{
width:100%;
height:200px;
border:1px dotted blue;
}
small{
margin-left:3px;
font-weight:bold;
color:gray;
}
</style>
</head>
<body>
<h3>Ajax File Upload</h3>
<div class="fileDrop"></div>
<div class="uploadedList"></div>
<script src="//code.jquery.com/jquery-1.11.3.min.js"></script>
</body>
</html>
<script>
$(".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();
formData.append("file", file);
$.ajax({
url:'/test/uploadAjax2',
data:formData,
dataType:'text',
processData:false,
contentType:false,
type:'POST',
success:function(data){
alert(data);
}
});
});
</script>

만일 저장하는 파일이 이미지 타입이 아닌 파일이라면, 결과 메시지에 's_' 로 시작되지 않고, 저장 경로에도 파일은 하나만 생성된다.
콘솔에 출력되는 값은 다음과 같다.
이미지인경우
INFO : test.controller.UploadController - originalName , 1.jpg INFO : config.uploadfile.UploadFileUtils - datePath - \2017\10\22 INFO : test.controller.UploadController - 파일 저장후 반환 값 - , /2017/10/22/s_550eb43a-0369-4972-be98-9a9e093897dd_1.jpg
일반 파일인 경우
INFO : test.controller.UploadController - originalName , welcome.jsp INFO : config.uploadfile.UploadFileUtils - datePath - \2017\10\22 INFO : test.controller.UploadController - 파일 저장후 반환 값 - , /2017/10/22/26a7c77b-f7f7-4c6b-b559-58b0ed156bc7_welcome.jsp
보안
보안을 위해 이미지만 허용하거나 특정 파일만 허용가능하도록 해야 한다.
그래서 다음과 같이 클래스를 만들고 변경하였다.
추가
UploadSecurity
package test.controller;
import org.springframework.web.multipart.MultipartFile;
public class UploadSecurity {
public static boolean check(MultipartFile uploadedFile) {
if (!uploadedFile.isEmpty()) {
String fileName = uploadedFile.getOriginalFilename();
// jpg, jpeg, png, gif, 만 업로드 되도록 수정
if (fileName.toLowerCase().endsWith(".jpg") || fileName.toLowerCase().endsWith(".jpeg")
|| fileName.toLowerCase().endsWith(".png") || fileName.toLowerCase().endsWith(".gif")) {
return true;
}
}
return false;
}
}
controller
//년월일 폴더로 분리후 이미지일 경우 썸네일 이미지 생성
@ResponseBody
@RequestMapping(value="/test/uploadAjax2", method=RequestMethod.POST, produces="text/plain;charset=UTF-8")
public ResponseEntity<String> uploadAjax2(MultipartFile file) throws Exception{
logger.info("originalName , {} ", file.getOriginalFilename());
String uploadFile="";
// 보안상 , jpg, jpeg, png, gif, 만 업로드 되도록 수정
if(UploadSecurity.check(file)){
//사이즈 가로 설정
UploadFileUtils.setWIDTH_SIZE(200);
uploadFile=UploadFileUtils.uploadFile(uploadPath, file.getOriginalFilename(), file.getBytes());
}else{
uploadFile="허용되지 않은 파일 입니다.";
}
logger.info("파일 저장후 반환 값 - , {} ", uploadFile);
return new ResponseEntity<String>(uploadFile, HttpStatus.CREATED);
}
파일 화면에 표시하기
utils 또는 cofig 패키지에 다음 클래스를 만든다.
UploadFileDisplay
package config.uploadfile;
import java.io.FileInputStream;
import java.io.InputStream;
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;
//전송된 파일을 화면에 표시하는 클래스
public class UploadFileDisplay {
private static final Logger logger = LoggerFactory.getLogger(UploadFileDisplay.class);
private static UploadFileDisplay instance;
//싱글톤
private UploadFileDisplay (){
}
public static UploadFileDisplay getInstance(){
if(instance==null){
instance=new UploadFileDisplay();
}
return instance;
}
public ResponseEntity<byte[]> disPlay(String fileName, String uploadPath) throws Exception{
//fileName 은 /년/월/일/파일명의 형태로 입력을 받는다.
InputStream in=null;
//ResponseEntity<byte[]> 로 결과는 실제로 파일의 데이터가 된다.
//컨트롤에서 @ResponseBody 를 이용해야 하며
//byte[] 데이터가 그대로 전송될 것임을 명시한다.
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{
//이미지가 아닌 경우
//MIME 타입을 다운로드 용으로 사용되는 'application/octet-stream 으로 지정한다.
//브라우저는 이 MIME 타입을 보고 사용자에게 자동으로 다운로드 창을 열어 준다.
fileName=fileName.substring(fileName.indexOf("_")+1);
//한글 깨짐 방지 설정
//다운로드 할 때 사용자에게 보이는 파일의 이름이므로 한글 처리를 해서
// 전송합니다. 한글 파일의 경우 다운로드 하면 파일의 이름이 깨져서
// 나오기 반드시 인코딩 처리를 할 필요가 있다.
String attach="\""+new String(fileName.getBytes("UTF-8"), "ISO-8859-1")+"\"";
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
headers.add("Content-Disposition", "attachment; filename="+attach);
}
//실제로 데이터를 읽는 부분은 commons 라이브러리의 기능을 활용해서 대상
// 파일에서 데이터를 읽어내는 IOUtils.toByteArray() 이다.
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;
}
}
controller
@ResponseBody
@RequestMapping("/displayFile")
public ResponseEntity<byte[]> displayFile(String fileName) throws Exception{
UploadFileDisplay display=UploadFileDisplay.getInstance();
return display.disPlay(fileName, uploadPath);
}
테스트
테스트 먼저 이미지와 일반 파일을 업로드 시킨다.
콘솔창에 찍히는 파일 주소를 url 상에 다음과 같이 입력해 본다.
콘솔창
INFO : test.controller.UploadController - 파일 저장후 반환 값 - , /2017/10/22/1c9c0b2a-a16f-44fa-b548-f4f5f74d4ff8_최준호.hwp
url 입력 값:
http://localhost:7070/displayFile?fileName=/2017/10/22/s_24bef728-c974-49c7-b2283d3754d3be0_%EC%9D%B4%EB%AF%B8%EC%A7%80%202.png
http://localhost:7070/displayFile?fileName=/2017/10/22/1c9c0b2a-a16f-44fa-b548-f4f5f74d4ff8_최준호.hwp
이미지 표시 , 파일 링크 처리

<script>
$(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();
formData.append("file", file);
$.ajax({
url:'/test/uploadAjax2',
data:formData,
dataType:'text',
processData:false,
contentType:false,
type:'POST',
success:function(data){
var str="";
if(checkImageType(data)){
str="<div>"
+"<a href='/displayFile?fileName="+getImageLink(data)+"' ><img src='/displayFile?fileName="+data+"'/>"
+getOriginalName(data)+"</a>";
}else{
str ="<div>"+getOriginalName(data);
}
str +="<button data-src="+data+">X</button></div>";
$(".uploadedList").append(str);
}
});
});
});
//이미지인지 패턴으로 확인
function checkImageType(fileName){
var pattern=/jpg|gif|png|jpeg/i;
return fileName.match(pattern);
}
//원본이미지 이름 출력
function getOriginalName(fileName){
if(checkImageType(fileName)){
//이미지 일경우
var idx=fileName.lastIndexOf("_")+1;
return fileName.substr(idx);
}
//일반 파일 일 경우
var idx=fileName.indexOf("_")+1;
return fileName.substr(idx);
}
//이미지 원본 파일 찾아서 링크로 연결 시키기
// /2017/10/23/s_4beb6430-7dfb-4def-bcfd-35f4425b538f_a1.jpg
function getImageLink(fileName){
//이미지가 아니면 반환
if(!checkImageType(fileName)){
return ;
}
var front =fileName.substr(0,12);
var end=fileName.substr(14);
return front +end;
}
</script>
파일 삭제하기
@Controller
//삭제 처리
@ResponseBody
@RequestMapping(value="/deleteFile", method=RequestMethod.POST)
public ResponseEntity<String> deleteFile(String fileName){
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();
}
//일반 파일 삭제, 썸네일 이미지 삭제
new File(uploadPath+fileName.replace('/', File.separatorChar)).delete();
return new ResponseEntity<String>("deleted", HttpStatus.OK);
}
<div class="uploadedList"> <div><a href="/displayFile?fileName=/2017/10/23/4275f192-011f-469f-afba-e7e291e252b6_a1.jpg"> <img src="/displayFile?fileName=/2017/10/23/s_4275f192-011f-469f-afba-e7e291e252b6_a1.jpg">a1.jpg</a> <button data-src="/2017/10/23/s_4275f192-011f-469f-afba-e7e291e252b6_a1.jpg">X</button></div> <div>스타크래프트 시디키.txt <button data-src="/2017/10/23/1ceb5610-b5ec-4032-bfd1-aa6c0bdb47a4_스타크래프트" 시디키.txt="">X</button> </div> </div>
//첨부파일 삭제 처리
$(".uploadedList").on("click", "button", function(event){
var that =$(this);
$.ajax({
url:"/deleteFile",
type:"post",
data:{
fileName:$(this).attr("data-src")
},
dataType:"text",
success:function(result){
if(result=='deleted'){
//삭제후 화면에서 제거
that.parent("div").remove();
}
}
});
});
제작 : macaronics.net - Developer Jun Ho Choi
소스 : 소스가 필요한 분은 비밀 댓글로 작성해 주세요.


















댓글 ( 9)
댓글 남기기