Part 21. 파일 업로드 방식
- 첨부파일을 서버에 전송하는 방식은 크게 < form > 태그를 이용해 업로드 하는 방식과 Ajax를 이용하는 방식으로 나눠볼 수 있다.
- 브라우저상에서 첨부파일에 대한 처리는 주로 다음과 같은 방식들을 이용한다.
- < form > 태그를 이용하는 방식 : 브라우저의 제한이 없어야 하는 경우에 사용
- 일반적으로 페이지 이동과 동시에 첨부파일을 업로드 하는 방식
- < iframe > 을 이용해 화면의 이동 없이 첨부파일을 업로드 하는 방식
- Ajax를 이용하는 방식 : 첨부파일을 별도로 처리하는 방식
- < input type='file' > 을 이용하고 Ajax로 처리하는 방식
- HTML5의 Drag And Drop 기능이나 jQuery 라이브러리를 이용해서 처리하는 방식
- 브라우저 상에서 첨부파일을 처리하는 방식은 다양하게 있지만, 서버 쪽에서의 처리는 거의 대부분 비슷하다.
- 응답을 HTML 코드로 하는지 아니면 JSON 등으로 처리하는지 정도의 구분만 하면 된다.
- 예제는 Ajax를 위주로 처리할 것이다.
- 서버에서 주의해야 하는 점은 첨부파일의 처리를 위해 어떤 종류의 라이브러리나 API 등을 활용할 것인지에 대한 부분이다.
- 서버에서 첨부파일을 처리하는 방식은 크게 다음과 같은 API들을 활용한다.
- cos.jar: 2002년도 이후에 개발이 종료되었으므로, 더 이상 사용하는 것을 권장하지 않는다.
- commons-fileupload: 가장 일반적으로 많이 활용되고, 서블릿 스펙 3.0 이전에도 사용 가능
- 서블릿 3.0 이상: 3.0 이상부터는 자체적인 파일 업로드 처리가 API 상에서 지원
- 위의 방식에서 가장 일반적인 형태는 commons-fileupload를 이용한 설정이지만, Tomcat 7버전 이후에는 서블릿 3.0 이상을 지원하므로, 예제에서는 이를 활용하는 방식으로 설정해서 사용한다.
- 첨부파일은 실제 서버가 동작하는 머신 내에 있는 폴더에 업로드 시켜야 하므로 C 드라이브 밑에 upload 폴더와 임시 업로드 파일을 저장할 temp 아래와 같은 구조로 생성한다.
21.1 스프링의 첨부파일을 위한 설정
- 예제를 위해 'ex05' 프로젝트를 Spring Legacy Project로 생성하고, 생성된 프로젝트의 pom.xml의 일부는 버전을 변경한다.
< pom.xml >
<properties>
<java-version>1.8</java-version>
<org.springframework-version>5.0.7.RELEASE</org.springframework-version>
<org.aspectj-version>1.9.0</org.aspectj-version>
<org.slf4j-version>1.7.25</org.slf4j-version>
</properties>
- 서블릿 3.0 이상을 활용하기 위해서 pom.xml에 설정된 서블릿의 버전(기존 2.5)을 수정하고, Lombok 등을 추가한다.
< pom.xml >
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.0</version>
<scope>provided</scope>
</dependency>
21.1.1 web.xml을 이용하는 경우의 첨부파일 설정
- 프로젝트가 web.xml을 이용하는 경우라면 첨부파일의 처리에 대한 설정 역시 web.xml을 이용해 처리한다.
- Spring Legacy Project로 생성된 경우에는 서블릿 버전은 2.5버전이므로 이를 수정하는 작업이 필요하다.
- web.xml의 상단에 XML 네임스페이스가 2.5버전으로 된 설정을 찾아 수정한다.
< 서블릿 2.5 버전의 web.xml >
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee https://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
- web.xml은 아래와 같이 내용을 변경한다.
< web.xml >
<web-app
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/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">
- web.xml의 < servlet > 태그 내에는 < multipart-config > 태그를 추가한다.
< web.xml >
<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>
<max-request-size>41943040</max-request-size>
<file-size-threshold>20971520</file-size-threshold>
</multipart-config>
</servlet>
- < multipart-config > 의 설정은 특정 사이즈의 메모리 사용(file-size-threshold), 업로드 되는 파일을 저장할 공간(location)과 업로드 되는 파일의 최대 크기(max-file-size)와 한 번에 올릴 수 있는 최대 크기(max-request-size)를 지정할 수 있다.
- web.xml의 설정은 WAS(Tomcat) 자체의 설정일 뿐이고, 스프링에서 업로드 처리는 MultipartResolver라는 타입의 객체를 빈으로 등록해야만 가능하다.
- Web과 관련된 설정이므로 servlet-context.xml을 이용해 설정한다.
< servlet-context.xml >
<beans:bean id="multipartResolver"
class="org.springframework.web.multipart.support.StandardServletMultipartResolver">
</beans:bean>
- 첨부파일을 처리하는 빈을 설정할 때 id는 'multipartResolver'라른 이름으로 지정된 이름을 사용한다.
2.1.2 Java 설정을 이용하는 경우
- Java 설정을 이용하는 프로젝트는 'jex05'로 생성한다.
- Java 설정을 이용하는 경우에는 먼저 pom.xml에서 web.xml이 없어도 문제가 없도록 < plugin > 을 추가한다.
< pom.xml >
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<configuration>
<failOnMissingWebXml>false</failOnMissingWebXml>
</configuration>
</plugin>
- org.zerock.config 패키지를 생성하고 설정과 관련된 클래스들을 이전 Java 설정을 참고하여 추가한다.
WebConfig 수정
< WebConfig 클래스 >
@Override
protected void customizeRegistration(ServletRegistration.Dynamic
registration) {
registration.setInitParameter("throwExceptionIfNoHandlerFound", "true");
MultipartConfigElement multipartConfig =
new MultipartConfigElement("D:\\upload\\temp",
20971520,
41943040,
20971520);
registration.setMultipartConfig(multipartConfig);
}
ServletConifg 수정
- servlet-config.xml을 대신하는 ServletConfig 클래스 파일은 MultipartResolver를 아래와 같이 스프링의 빈으로 추가해야 한다.
< ServletConfig 클래스 >
@Bean
public MultipartResolver multipartResolver() {
StandardServletMultipartResolver resolver
= new StandardServletMultipartResolver();
return resolver;
}
- 서버상에서 첨부파일의 처리는 컨트롤러에서 이루어지므로, 실습을 위해 UploadController를 작성한다.
- UploadController는 GET 방식으로 첨부파일을 업로드 할 수 있는 화면을 처리하는 메서드와 POST 방식으로 첨부파일 업로드를 처리하는 메서드를 추가한다.
< UploadController >
package org.zerock.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import lombok.extern.log4j.Log4j;
@Controller
@Log4j
public class UploadController {
@GetMapping("/uploadForm")
public void uploadForm() {
log.info("upload form");
}
}
- UploadController에는 클래스 선언부에 @RequestMapping이 적용되지 않았으므로, WEB-INF/views 폴더에 uploadForm.jsp 파일을 추가한다.
< 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>Insert title here</title>
</head>
<body>
<form action="uploadFormAction" method="post" enctype="multipart/form-data">
<input type='file' name='uploadFile' multiple>
<button>Submit</button>
</form>
</body>
</html>
- uploadForm.jsp는 간단하게 < form > 태그만을 생성하고 < input type='file' > 을 추가한다.
- 실제 전송은 uploadFormAction 경로를 이용해 처리한다.
- 파일 업로드에서 가장 신경 써야 하는 부분은 enctype의 속성값을 'multipart/form-data'로 지정하는 것이다.
- < input type='file' >의 경우 최근 브라우저에서는 'multiple'이라는 속성을 지원하는데 이를 이용하면 하나의 < input > 태그로 한꺼번에 여러 개의 파일을 업로드 할 수 있다.
- multiple 속성은 브라우저의 버전에 따라 지원 여부가 달라지므로 IE의 경우 10 이상에서만 사용할 수 있다.
- 작성된 프로젝트는 '/' 경로를 이용해 서비스하도록 지정하고, Tomcat을 통해 확인한다.
21.2.1 MultipartFile 타입
- 스프링 MVC에는 MultipartFile 타입을 제공해서 업로드되는 파일 데이터를 쉽게 처리할 수 있다.
- 위의 < input type='file name='uploadFile' >의 name 속성으로 변수를 지정해 처리한다.
< UploadController >
@Controller
@Log4j
public class UploadController {
@GetMapping("/uploadForm")
public void uploadForm() {
log.info("upload form");
}
@PostMapping("/uploadFormAction")
public void uploadFormPost(MultipartFile[] uploadFile, Model model) {
for (MultipartFile multipartFile : uploadFile) {
log.info("---------------------------------");
log.info("Upload File Name:" +multipartFile.getOriginalFilename());
log.info("Upload File Size:" +multipartFile.getSize());
}
}
}
- 파일 처리는 스프링에서 제공하는 MultipartFile이라는 타입을 이용한다.
- 화면에서 첨부파일을 여러 개 선택할 수 있으므로 배열 타입으로 설정한 후 파일을 업로드 해 본다.(아직 파일 이름이 한글이 경우에 대한 설정이 없으므로 영문 이름의 파일만을 업로드해 테스트 한다.)
- 결과 페이지(uploadFormAction.jsp)를 작성하지 않았기 때문에 404 에러가 뜨기는 하지만, UploadController에서는 정상적으로 팡리 데이터가 감지되는 것을 확인할 수 있다.
- IE의 경우는 getOriginalFilename()의 결과가 조금 다르게 파일의 이름이 나오지 않고, 전체 경로가 출력된다.
- IE까지 같이 처리하려면 마지막에 경로까지 잘라낸 문자만을 취해야 하는데, 이에 대한 처리는 Ajax 처리 시 알아보도록 한다.
- MultipartFile은 다음과 같은 메서드들을 가지고 있다.
파일의 저장
- 업로드되는 파일을 저장하는 방법은 간단히 transferTo()를 이용해 처리할 수 있다.
< UploadController >
@PostMapping("/uploadFormAction")
public void uploadFormPost(MultipartFile[] uploadFile, Model model) {
String uploadFolder = "D:\\upload";
for (MultipartFile multipartFile : uploadFile) {
log.info("---------------------------------");
log.info("Upload File Name:" +multipartFile.getOriginalFilename());
log.info("Upload File Size:" +multipartFile.getSize());
File saveFile = new File(uploadFolder, multipartFile.getOriginalFilename());
try {
multipartFile.transferTo(saveFile);
} catch (Exception e) {
log.error(e.getMessage());
} //end catch
} //end for
}
- transferTo()의 파라미터로는 java.io.File의 객체를 지정하면 되기 때문에 업로드 되는 원래 파일의 이름으로 D드라이브 upload 폴더에 저장된다.
21.3 Ajax를 이용하는 파일 업로드
- 첨부파일을 업로드하는 또 다른 방식은 Ajax를 이용해 파일 데이터만을 전송하는 방식이다.
- Ajax를 이용하는 첨부파일 처리는 FormData라는 객체를 이용하는데 IE의 경우 10 이후의 버전부터 지원되므로 브라우저에 제약이 있을 수 있다.
- UploadController에 GET 방식으로 첨부파일을 업로드하는 페이지를 제작한다.
< UploadController >
@GetMapping("/uploadAjax")
public void uploadAjax() {
log.info("upload ajax");
}
- WEB-INF/views 폴더에는 uploadAjax.jsp 페이지를 작성한다.
< uploadAjax.jsp >
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text:html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
<h1>Upload with Ajax</h1>
<div class='uploadDiv'>
<input type='file' name='uploadFile' multiple>
</div>
<button id='uploadBtn'>Upload</button>
</body>
</html>
- uploadAjax.jsp는 순수한 JavaScript를 이요해 처리할 수도 있지만, jQuery를 이용해서 처리하는 것이 편리하다.
- uploadAjax.jsp 내에 jQuery 라이브러리의 경로를 추가하고(jQuery cdn으로 검색하면 쉽게 링크를 찾을 수 있다.) < script > 를 이ㅛㅇ해 첨부파일을 처리한다.
< uploadAjax.jsp >
<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);
});
});
</script>
</body>
</html>
- jQuery를 이용하는 경우에 파일 업로드는 FormData라는 객체를 이용하게 된다(브라우저의 제약이 있으므로 주의한다.).
- FormData는 쉽게 말해 가상의 < form > 태그와 같다고 생각하면 된다.
- Ajax를 이용하는 파일 업로드는 FormData를 이용해 필요한 파라미터를 담아 전송하는 방식이다.
- 본격적으로 첨부파일 데이털르 전송하기 전에 여러 개의 파일을 선택했을 대 jQuery에서 파일 데이터를 처리가 가능한지 브라우저에서 먼저 확인해야 한다.
21.3.1 jQuery를 이용한 첨부파일 전송
- Ajax를 이용해 첨부파일을 전송하는 경우 가장 중요한 객체는 FormData 타입의 객체에 각 파일 데이터를 추가하는 것과 이를 Ajax로 전송할 때 약간의 옵션이 붙어야 한다는 점이다.
< uploadAjax.jsp >
<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);
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");
}
});
});
});
</script>
- 첨부파일 데이터는 fileData를 formData에 추가한 뒤에 Ajax를 통해 formData 자체를 전송한다.
- 이때 processData와 contentType은 반드시 'false'로 지정해야만 전송되므로 주의해야 한다.
- UploadController에서는 기존과 동일하게 MultipartFile 타입을 이용해 첨부파일 데이터를 처리한다.
< UploadController >
@PostMapping("/uploadAjaxAction")
public void uploadAjaxPost(MultipartFile[] uploadFile) {
log.info("update ajax post.........");
String uploadFolder = "D:\\upload";
for (MultipartFile multipartFile : uploadFile) {
log.info("---------------------------------");
log.info("Upload File Name:" +multipartFile.getOriginalFilename());
log.info("Upload File Size:" +multipartFile.getSize());
String uploadFileName = multipartFile.getOriginalFilename();
// IE has file path
uploadFileName = uploadFileName.substring(uploadFileName.lastIndexOf("\\") + 1);
log.info("only File name : " + uploadFileName);
File saveFile = new File(uploadFolder, uploadFileName);
try {
multipartFile.transferTo(saveFile);
} catch (Exception e) {
log.error(e.getMessage());
} //end catch
} //end for
}
- uploadAjaxPost()는 기존의 < form > 태그를 이용하던 방식과 아무런 차이가 없지만, 조금 뒤에 Ajax 방식으로 결과 데이터를 전달하면서 리턴 타입이 달라지도록 한다.
- 파라미터에서는 Ajax 방식을 이용하기 때문에 Model을 사용할 일이 없으므로 사용하지 않는다.
- IE의 경우에는 전체 파일 경로가 전송되므로, 마지막'₩'를 기준으로 잘라낸 문자열이 실제 파일 이름이 된다.
- 브라우저를 이용해 '/uploadAjax'에서 정상적으로 첨부파일이 업로드가 되는지 확인한다.
- 예제는 첨부파일의 처리를 Ajax를 이용할 것이므로 지금까지의 문제점 등을 파악하고 해결해 나가는 방식으로 진행한다.
21.3.2 파일 업로드에서 고려해야 하는 점들
- 첨부파일을 서버에 전송하고 저장하는 일은 그다지 복잡한 일은 아니지만, 생각해야 하는 일들이 많다.
- 동일한 이름으로 파일이 업로드 되었을 때 기존 파일이 사라지는 문제
- 이미지 파일의 경우에는 원본 파일의 용량이 큰 경우 섬네일 이미지를 생성해야 하는 문제
- 이미지 파일과 일반 파일을 구분해서 다운로드 혹은 페이지에서 조회하도록 처리하는 문제
- 첨부파일 공격에 대비하기 위한 업로드 파일의 확장자 제한