서버에서 첨부파일 처리 방식
- cos.jar
- 2002년에 개발이 종료되어, 더 이상 잘 사용되지 않음
- 2002년에 개발이 종료되어, 더 이상 잘 사용되지 않음
- commons-fileupload
- 가장 많이 사용되는 방식
- 가장 많이 사용되는 방식
- 자체적인 파일 업로드 처리
- 서블릿 3.0 이상부터 자체적인 파일 업로드 처리가 API 상에서 지원
첨부파일시 고려해야할 것들
해당 포스팅은 아래 고려사항들을 적용하여 구현했습니다. 각 링크타고 들어가 포스팅을 읽어보는 것이 도움됩니다.
- 동일한 이름으로 파일 업로드 시 기존 파일이 사라지는 문제
- 이미지 파일의 경우 원본 파일의 용량이 큰 경우 섬네일 이미지를 생성해야 하는 문제
- 이미지 파일과 일반 파일을 구분해서 다운로드 혹은 페이지에서 조회하도록 처리하는 문제
- 첨부파일 공격에 대비하기 위한 업로드 파일의 확장자 제한
스프링 설정
서블릿 3.0 이상부터 지원되는 자체적인 파일 업로드 방식을 해보기 위해서 서블릿 버전을 바꿔준다.
SpringMVC 프로젝트를 실행하면 기본적으로 서블릿 버전이 2.5 버전이므로 web.xml에서 수정해준다.
추가로 파일 설정을 위해 <multipart-config> 태그도 추가해 준다.
▶web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
id="WebApp_ID" version="3.1">
...
<!-- Processes application requests -->
<servlet>
<servlet-name>appServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/appServlet/servlet-context.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
<!-- 저장 공간, 파일 크기, 한 번에 올리는 최대 크기, 특정 사이즈의 메모리 사용 크기 설정 -->
<multipart-config>
<location>D:\\upload\\temp</location>
<max-file-size>20971520</max-file-size> <!--1MB * 20 -->
<max-request-size>41943040</max-request-size><!-- 40MB -->
<file-size-threshold>20971520</file-size-threshold> <!-- 20MB -->
</multipart-config>
</servlet>
...
▶servlet-context.xml
id는 'multipartResolver'라는 이름으로 지정된 이름을 사용
<!-- file upload -->
<context:component-scan base-package="com.spring.boardapp"></context:component-scan>
<beans:bean id="multipartResolver"
class="org.springframework.web.multipart.support.StandardServletMultipartResolver">
</beans:bean>
※ 자바 기반 설정할 경우
▶pom.xml
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<configuration>
<failOnMissingWebXml>false</failOnMissingWebXml>
</configuration>
</plugin>
pom.xml에서 web.xml이 없어도 문제가 없도록 위 plugin을 추가해준다.
▶WebConfig.java
web.xml 대신 존재하는 WebConfig.java
public class WebConfig extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[] { RootConfig.class };
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class[] { ServletConfig.class };
}
@Override
protected String[] getServletMappings() {
return new String[] { "/" };
}
@Override
protected Filter[] getServletFilters() {
CharacterEncodingFilter characterEncodingFilter = new CharacterEncodingFilter();
characterEncodingFilter.setEncoding("UTF-8");
characterEncodingFilter.setForceEncoding(true);
return new Filter[] { characterEncodingFilter };
}
@Override
protected void customizeRegistration(ServletRegistration.Dynamic registration) {
registration.setInitParameter("throwExceptionIfNoHandlerFound", "true");
MultipartConfigElement multipartConfig =
new MultipartConfigElement("D:\\upload\\temp",
20971520,
41943040,
20971520);
registration.setMultipartConfig(multipartConfig);
}
}
▶ServletConfig.java
servlet-context.xml 대신 존재하는 ServletConfig.java
@EnableWebMvc
@ComponentScan(basePackages = { "com.example.XXX" })
public class ServletConfig implements WebMvcConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
InternalResourceViewResolver bean = new InternalResourceViewResolver();
bean.setViewClass(JstlView.class);
bean.setPrefix("/WEB-INF/views/");
bean.setSuffix(".jsp");
registry.viewResolver(bean);
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**").addResourceLocations("/resources/");
}
@Bean
public MultipartResolver multipartResolver() {
StandardServletMultipartResolver resolver
= new StandardServletMultipartResolver();
return resolver;
}
}
<form> 방식의 파일 업로드
▶UploadController.java
package com.spring.boardapp.controller;
import java.io.File;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.multipart.MultipartFile;
@Controller
public class UploadController {
@GetMapping("/uploadForm")
public void uploadForm() {
}
@PostMapping("/uploadFormAction")
public void uploaoFormPost(MultipartFile[] uploadFile, Model model) {
String uploadFolder = "D:\\upload";
for(MultipartFile multipartFile : uploadFile) {
File saveFile = new File(uploadFolder, multipartFile.getOriginalFilename());
try {
multipartFile.transferTo(saveFile);
}catch(Exception e) {
System.out.println(e.getMessage());
}
}
}
}
- MultipartFile
- getName() : 파라미터의 이름 <input> 태그의 이름
- getOriginalFileName()
- isEmpty()
- getSize()
- getBytes()
- getInputStream() : 파일데이터와 연결된 InputStream 반환
- trasferTo(File file) : 파일의 저장
▶uploadForm.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>File Upload</title>
</head>
<body>
<form action="uploadFormAction" method="post" enctype="multipart/form-data">
<input type='file' name='uploadFile' multiple>
<button>Submit</button>
</form>
</body>
</html>
- enctype의 속성값을 'multipart/form-data'
- <input type="file"> 태그의 multiple 속성은 하나의 <input> 태그로 한꺼번에 여러 개의 파일 업로드가 가능하다.
Ajax 이용 파일 업로드
▶UploadController.java
package com.spring.boardapp.controller;
import java.io.File;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.multipart.MultipartFile;
@Controller
public class UploadController {
@GetMapping("/uploadAjax")
public void uploadAjax() {
}
@PostMapping("/uploadAjaxAction")
public void uploadAjaxPost(MultipartFile[] uploadFile) {
String uploadFolder = "D:\\upload";
for (MultipartFile multipartFile : uploadFile) {
String uploadFileName = multipartFile.getOriginalFilename();
// IE의 경우 전체 파일 경로가 전송되므로, 마지막 \를 기준으로 잘라낸 문자열이 실제 파일 이름이 된다.
uploadFileName = uploadFileName.substring(uploadFileName.lastIndexOf("\\") + 1);
File saveFile = new File(uploadFolder, uploadFileName);
try {
multipartFile.transferTo(saveFile);
} catch (Exception e) {
} // end catch
} // end for
}
}
▶UploadAjax.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<div class='uploadDiv'>
<input type='file' name='uploadFile' multiple>
</div>
<button id='uploadBtn'>Upload</button>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="crossorigin="anonymous">
</script>
<script>
$(document).ready(function(){
$("#uploadBtn").on("click", function(e){
var formData = new FormData();
var inputFile = $("input[name='uploadFile']");
var files = inputFile[0].files;
console.log(files);
//add filedate to formdata
for(var i = 0; i < files.length; i++){
formData.append("uploadFile", files[i]);
}
$.ajax({
url : '/uploadAjaxAction',
processData : false,
contentType : false,
data : formData,
type : 'POST',
success : function(result) {
alert("Uploaded")
}
}); //$.ajax
});
});
</script>
</body>
</html>
💡파일 데이터 반환
※ Ajax로 파일 업로드 하는 경우 브라우저에서는 아무 데이터도 전달 받지 못하였기 때문에 브라우저에도 피드백을 주어야 한다. 피드백을 json객체로 브라우저에게 반환하도록 코드를 작성해보겠다.
그럼 브라우저에 피드백 줄 내용은 무엇이 있을까?
- 업로드된 파일 이름 & 원본 파일 이름
- 파일이 저장된 경로
- 업로드된 파일이 이미지 파일인가 아닌가
이에 대한 정보를 처리하기 위해 별도의 객체를 생성해서 처리하는 방법을 이용하도록 하겠다.
▶pom.xml
jackson-databind 관련 라이브러리를 추가 시킨다.
<!-- Jackson -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- jackson-databind 관련 라이브러리 추가 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- jackson-databind -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>${jackson.version}</version>
</dependency>
▶DTO 클래스
AttachFile.java
package com.spring.boardapp.domain;
public class AttachFile {
private String fileName;
private String uploadPath;
private String uuid;
private boolean image;
public String getFileName() {
return fileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
public String getUploadPath() {
return uploadPath;
}
public void setUploadPath(String uploadPath) {
this.uploadPath = uploadPath;
}
public String getUuid() {
return uuid;
}
public void setUuid(String uuid) {
this.uuid = uuid;
}
public boolean isImage() {
return image;
}
public void setImage(boolean image) {
this.image = image;
}
}
- uuid
이는 파일의 중복된 이름을 처리하기 위해 넣는 UUID 값으로 "중복된 파일 이름 해결"에 설명을 해놓았다.
▶Controller 클래스
package com.spring.boardapp.controller;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.UUID;
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.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
import com.spring.boardapp.domain.AttachFile;
import net.coobird.thumbnailator.Thumbnailator;
@Controller
public class UploadController {
@GetMapping("/uploadForm")
public void uploadForm() {
System.out.println("upload form");
}
@PostMapping("/uploadFormAction")
public void uploaoFormPost(MultipartFile[] uploadFile, Model model) {
String uploadFolder = "D:\\upload";
for (MultipartFile multipartFile : uploadFile) {
File saveFile = new File(uploadFolder, multipartFile.getOriginalFilename());
try {
multipartFile.transferTo(saveFile);
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
}
@GetMapping("/uploadAjax")
public void uploadAjax() {
System.out.println("upload ajax");
}
// 날짜 폴더 생성
private String getFolder() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Date date = new Date();
String str = sdf.format(date);
return str.replace("-", File.separator);
}
// 이미지 파일인지 검사
private boolean checkImageType(File file) {
try {
String contentType = Files.probeContentType(file.toPath());
return contentType.startsWith("image");
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return false;
}
@PostMapping(value = "/uploadAjaxAction",
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
@ResponseBody
public ResponseEntity<List<AttachFile>> uploadAjaxPost(MultipartFile[] uploadFile) {
List<AttachFile> list = new ArrayList<AttachFile>();
String uploadFolder = "D:\\upload";
String uploadFolderPath = getFolder();
// make folder --------
File uploadPath = new File(uploadFolder, uploadFolderPath);
if (uploadPath.exists() == false) {
uploadPath.mkdirs();
}
// make yyyy/MM/dd folder
for (MultipartFile multipartFile : uploadFile) {
AttachFile attachDTO = new AttachFile();
String uploadFileName = multipartFile.getOriginalFilename();
// IE의 경우 전체 파일 경로가 전송되므로, 마지막 \를 기준으로 잘라낸 문자열이 실제 파일 이름이 된다.
uploadFileName = uploadFileName.substring(uploadFileName.lastIndexOf("\\") + 1);
attachDTO.setFileName(uploadFileName);
UUID uuid = UUID.randomUUID();
uploadFileName = uuid.toString() + "_" + uploadFileName;
try {
File saveFile = new File(uploadPath, uploadFileName);
multipartFile.transferTo(saveFile);
attachDTO.setUuid(uuid.toString());
attachDTO.setUploadPath(uploadFolderPath);
// check image type file
if (checkImageType(saveFile)) {
attachDTO.setImage(true);
FileOutputStream thumbnail = new FileOutputStream(new File(uploadPath, "s_" + uploadFileName));
Thumbnailator.createThumbnail(multipartFile.getInputStream(), thumbnail, 100, 100);
thumbnail.close();
}
// add to List
list.add(attachDTO);
} catch (Exception e) {
e.printStackTrace();
}
} // end for
return new ResponseEntity<List<AttachFile>>(list, HttpStatus.OK);
}
}
여기엔 위에서 설명한 것 외에 여러가지 요소들을 추가하였다. 그 요소들은 아래 첨부파일시 고려해야할 것들에 기재해놨으므로 링크를 클릭하여 추가해주면 된다.
▶UploadAjax.jsp
$.ajax({
url : '/uploadAjaxAction',
processData : false,
contentType : false,
data : formData,
type : 'POST',
dataType : 'json',
success : function(result) {
alert("Uploaded")
}
}); //$.ajax
Ajax를 호출했을 때 결과 타입(dataType)은 'json' 타입으로 변경한다.
파일 여러 번 업로드하기
업로드는 <input type="file">을 통해서 이루어진다.
이 때 한 번 업로드가 끝난 후에 다시 올리기 위해서는 이를 초기화 시켜주는 작업이 필요하다.
▶UploadAjax.jsp
var cloneObj = $(".uploadDiv").clone();
$("#uploadBtn").on("click", function(e){
var formData = new FormData();
...
$.ajax({
url : '/uploadAjaxAction',
processData : false,
contentType : false,
data : formData,
type : 'POST',
dataType : 'json',
success : function(result) {
console.log(result);
$(".uploadDiv").html(cloneObj.html());
}
}); //$.ajax
- <input type="file">은 자체적으로 readonly 속성을 갖고 있다.
- 따라서 별도의 방법으로 초기화 시켜 다른 첨부파일을 또 업로드할 수 있도록 만들어주어야 한다.
- 첨부파일 업로드 전 <input type="file"> 객체가 포함된 <div>를 복사(clone) 하는 방법이다.
첨부파일을 업로드 한 뒤 복사된 객체를 <div> 내에 다시 추가해서 첨부파일 부분을 초기화시켜준다.
올린 파일 목록 출력 + 이미지 같이 출력
▶UploadAjax.jsp
<style>
.uploadResult {
width: 100%;
background-color: gray;
}
.uploadResult ul {
display: flex;
flex-flow: row;
justify-content: center;
align-items: center;
}
.uploadResult ul li {
list-style: none;
padding: 10px;
}
.uploadResult ul li img {
width: 100px;
}
</style>
...
<script>
$(document).ready(function(){
var regex = new RegExp("(.*?)\.(exe|sh|zip)$");
var maxSize = 10485760; // 10MB
// 사이즈, 확장자 체크
function checkExtension(fileName, fileSize) {
if (fileSize >= maxSize) {
alert("파일 사이즈 초과");
return false;
}
if (regex.test(fileName)) {
alert("해당 종류의 파일은 업로드할 수 없습니다.");
return false;
}
return true;
}
// 파일 추가로 올리기 위한 조치
var cloneObj = $(".uploadDiv").clone();
// 클릭시
$("#uploadBtn").on("click", function(e){
var formData = new FormData();
var inputFile = $("input[name='uploadFile']");
var files = inputFile[0].files;
//add filedate to formdata
for(var i = 0; i < files.length; i++){
if (!checkExtension(files[i].name, files[i].size)) {
return false;
}
formData.append("uploadFile", files[i]);
}
$.ajax({
url : '/uploadAjaxAction',
processData : false,
contentType : false,
data : formData,
type : 'POST',
dataType : 'json',
success : function(result) {
console.log(result);
showUploadedFile(result);
$(".uploadDiv").html(cloneObj.html());
}
}); //$.ajax
});
var uploadResult = $(".uploadResult ul");
function showUploadedFile(uploadResultArr) {
var str = "";
$(uploadResultArr).each(function(i, obj) {
if(!obj.image){
str += "<li><img src='/resources/img/attach.png'>"+obj.fileName+"</li>";
}
else{
//str += "<li>"+ obj.fileName+"</li>";
var fileCallPath = encodeURIComponent( obj.uploadPath+ "/s_"+obj.uuid+"_"+obj.fileName);
str += "<li><img src='/display?fileName="+fileCallPath+"'><li>";
}
});
uploadResult.append(str);
}
});
</script>
- encodeUIRComponent
- 파일 경로 및 이름에 공백이나 한글이 포함될 경우 문제가 발생할 수 있기 때문에 JavaScript#encodeURIComponent() 를 이용해서 URI에 문제가 없는 문자열을 생성해 처리한다.
- 일반 파일인 경우
- /resources/img/attach.png 파일 이미지를 보이게 하였다.
- 이미지 파일인 경우
- 섬네일 파일을 보여주기 위해서 서버를 통해 특정 URI 를 호출하면 보여주도록 처리하였다. 이를 위해 아래와 같이 Controller 컨트롤러에 섬네일 데이터 전송하는 코드 추가
▶UploadController.java
특정한 파일 이름을 받아서 이미지 데이터를 전송하는 코드
@GetMapping("/display")
@ResponseBody
public ResponseEntity<byte[]> getFile(String fileName) {
File file = new File("D:\\upload\\" + fileName);
ResponseEntity<byte[]> result = null;
try {
HttpHeaders header = new HttpHeaders();
header.add("Content-Type", Files.probeContentType(file.toPath()));
result = new ResponseEntity<byte[]>(FileCopyUtils.copyToByteArray(file), header, HttpStatus.OK);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return result;
}
- probeContentType()
byte[]로 이미지 파일로 전송할 때 파일의 종류에 따라 MIME 타입이 달라진다.
이 부분을 해결하기 위해 probeContentType()을 이용해서 적절한 MIME 타입 데이터를 Http의 헤더 메시지에 포함할 수 있도록 처리한다. - 호출은 '/display?fileName='년/월/일/파일이름' 형태
'ETC' 카테고리의 다른 글
Spring + Ajax 중복된 파일 이름 해결 (0) | 2022.09.25 |
---|---|
Spring + Ajax 파일 확장자, 크기 제한 (Ajax) (0) | 2022.09.25 |
[SpringMVC + MyBatis + MySql] 게시판 댓글 수 게시판 List에 출력 (0) | 2022.09.25 |
[SpringMVC + MyBatis + Ajax] 게시판 댓글 추가/삭제/List (Ajax 이용) (0) | 2022.09.24 |
[SpringMVC + MyBatis + MySql] 게시판 CRUD + 페이징처리 + 검색조건 + 조회수 (0) | 2022.09.15 |