[Spring] 파일 업로드/다운로드
스프링이 제공하는 파일 업로드 처리 기능을 사용하려면 MultipartResolver를 빈으로 등록해야 한다.
스프링은 Apache Commons FileUpload API를 이용하여 파일 업로드를 처리하는 CommonsMultipartResolver 클래스를 제공한다
* 파일 다운로드는 어떤 프레임워크건 라이브러리가 없다. 직접 코딩해서 모듈화 해놔야함
실제주소 - /spring/fileTest/
가상주소 - /spring/file/
web.xml에 MultipartResolver 빈으로 등록
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<property name="defaultEncoding" value="UTF-8"/>
<property name="maxUploadSize" value="10000000"/>
</bean>
1. DB 테이블 생성
create table fileTest (
num number(7) not NULL,
subject varchar2(50) not NULL,
saveFileName varchar2(50),
originalFileName varchar2(50));
2-1. iBatis 셋팅- sqlMapConfig.xml에 resource 등록
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE sqlMapConfig
PUBLIC "-//ibatis.apache.org//DTD SQL Map Config 2.0//EN"
"http://ibatis.apache.org/dtd/sql-map-config-2.dtd">
<sqlMapConfig>
<settings
cacheModelsEnabled="false"
useStatementNamespaces="true"/>
<sqlMap resource="com/util/sqlMap/temp_sqlMap.xml"/>
<sqlMap resource="com/util/sqlMap/fileTest_sqlMap.xml"/>
<sqlMap resource="com/util/sqlMap/bbs_sqlMap.xml"/>
</sqlMapConfig>
2-2. fileTest_sqlMap.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE sqlMap
PUBLIC "-//ibatis.apache.org//DTD SQL Map 2.0//EN"
"http://ibatis.apache.org/dtd/sql-map-2.dtd">
<sqlMap namespace="fileTest">
<!-- maxNum 뽑아오기 -->
<select id="maxNum" resultClass="int">
select nvl(max(num),0) from fileTest
</select>
<!-- 전체 데이터 갯수 -->
<select id="dataCount" resultClass="int">
select nvl(count(num),0) from fileTest
</select>
<!-- 전체 데이터 리스트 뽑아오기 -->
<select id="listData" resultClass="com.fileTest.FileCommand"
parameterClass="map">
<![CDATA[
select * from (select rownum rnum, data.* from (
select num,subject,saveFileName,originalFileName
from fileTest order by num desc) data)
where rnum>=#start# and rnum<=#end#
]]>
</select>
<!-- 파일 delete 하기 -->
<!-- 1. 해당 num 정보 가져오기 -->
<select id="readData" resultClass="com.fileTest.FileCommand"
parameterClass="int">
select num,subject,saveFileName,originalFileName
from fileTest where num = #num#
</select>
<!-- 2. 삭제 하기 -->
<delete id="deleteData" parameterClass="int">
delete fileTest where num=#num#
</delete>
<!-- 파일 올리기 (insert) -->
<insert id="insertData" parameterClass="com.fileTest.FileCommand">
insert into fileTest(num,subject,saveFileName,originalFileName)
values (#num#,#subject#,#saveFileName#,#originalFileName#)
</insert>
</sqlMap>
3-1. 파일업로드 페이지 - write.jsp
<%@ page contentType="text/html; charset=UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%
request.setCharacterEncoding("UTF-8");
String cp = request.getContextPath();
%>
<!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>파일업로드(Spring2.5)</title>
</head>
<body>
<form action="<%=cp %>/file/created.action" method="post" enctype="multipart/form-data">
제목:<input type="text" name="subject"/><br/>
파일:<input type="file" name="upload"/><br/>
<input type="hidden" name="mode" value="save"/><br/>
<input type="submit" value="파일업로드"/>
</form>
</body>
</html>
뷰는 HTML의 input 태그 file 속성으로 작성하여 form 방식으로 파일 업로드 함
form 태그의 enctype 속성은 multipart/form-data로 세팅하여 브라우져가 파일 업로드 방식으로 동작하도록 설정
enctype 속성 값 목록
값 |
설명 |
multipart/form-data |
파일 업로드시 사용 (인코딩 하지 않음) |
application/x-www-form/urlencoded |
디폴트값으로 모든 문자를 인코딩 |
text/plain |
공백은 + 기호로 변환함. 특수문자는 인코딩하지 안함 |
3-2. 파일 리스트 페이지 - view.jsp
<%@ page contentType="text/html; charset=UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%
request.setCharacterEncoding("UTF-8");
String cp = request.getContextPath();
%>
<!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>파일업로드(String2.5)</title>
</head>
<body>
<br/><br/>
<table width="500" align="center">
<tr height="30">
<td align="right">
<input type="button" value="파일 올리기"
onclick = "javascript:location.href='<%=cp %>/file/created.action';"/>;
</td>
</tr>
</table>
<table width="500" align="center" border="1" style="font-size:10pt;">
<tr height="30">
<td width="50">번호</td>
<td width="200">제목</td>
<td width="200">파일</td>
<td width="50">삭제</td>
</tr>
<c:forEach var="dto" items="${lists }">
<tr>
<td align="center" width="50">${dto.listNum }</td>
<td align="left" width="150">${dto.subject }</td>
<td align="left" width="350">
<a href="${downloadPath }?num=${dto.num}">${dto.originalFileName }</a>
</td>
<td align="center" width="50">
<a href="${deletePath }?num=${dto.num}&pageNum=${pageNum}">삭제</a></td>
</tr>
</c:forEach>
<tr>
<td colspan="4" align="center">
<c:if test="${totalDataCount!=0}">
${pageIndexList }
</c:if>
<c:if test="${totalDataCount == 0 }">
등록된 게시물이 없습니다
</c:if>
</td>
</tr>
</table>
</body>
</html>
4. FileCommand.java (DTO역할) 생성
package com.fileTest;
import java.io.InputStream;
import org.springframework.web.multipart.MultipartFile;
public class FileCommand {
private int listNum;
private InputStream inputStream;
private MultipartFile upload;
private int num;
private String subject;
private String saveFileName;
private String originalFileName;
public int getListNum() {
return listNum;
}
public void setListNum(int listNum) {
this.listNum = listNum;
}
public InputStream getInputStream() {
return inputStream;
}
public void setInputStream(InputStream inputStream) {
this.inputStream = inputStream;
}
public MultipartFile getUpload() {
return upload;
}
public void setUpload(MultipartFile upload) {
this.upload = upload;
}
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
public String getSubject() {
return subject;
}
public void setSubject(String subject) {
this.subject = subject;
}
public String getSaveFileName() {
return saveFileName;
}
public void setSaveFileName(String saveFileName) {
this.saveFileName = saveFileName;
}
public String getOriginalFileName() {
return originalFileName;
}
public void setOriginalFileName(String originalFileName) {
this.originalFileName = originalFileName;
}
}
5. FileController.java
업로드한 파일을 컨트롤러에서 MultipartFile 변수를 사용하여 전달받는다.
MultipartFile 클래스는 파일에 대한 정보와 파일 관련 메소드를 제공한다
대표적으로 사용하는 메소드
- transferTo() : 파일을 저장한다
- getOriginalFilename() : 파일 이름을 String 값으로 반환한다
- getSize() : 파일 크기를 반환한다
- getInputStream() : 파일에 대한 입력 스트림을 얻어온다
package com.fileTest;
import java.io.InputStream;
import org.springframework.web.multipart.MultipartFile;
public class FileCommand {
private int listNum;
private InputStream inputStream;
private MultipartFile upload;
private int num;
private String subject;
private String saveFileName;
private String originalFileName;
public int getListNum() {
return listNum;
}
public void setListNum(int listNum) {
this.listNum = listNum;
}
public InputStream getInputStream() {
return inputStream;
}
public void setInputStream(InputStream inputStream) {
this.inputStream = inputStream;
}
public MultipartFile getUpload() {
return upload;
}
public void setUpload(MultipartFile upload) {
this.upload = upload;
}
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
public String getSubject() {
return subject;
}
public void setSubject(String subject) {
this.subject = subject;
}
public String getSaveFileName() {
return saveFileName;
}
public void setSaveFileName(String saveFileName) {
this.saveFileName = saveFileName;
}
public String getOriginalFileName() {
return originalFileName;
}
public void setOriginalFileName(String originalFileName) {
this.originalFileName = originalFileName;
}
}
6. 파일 업로드, 다운로드, 삭제 메소드 FileManager.java
- Struts1 업로드 방식 - FormFile 이용
- Struts2 - 따로없음. upload용 메소드를 따로 FileManager.java안에 doFileUpload 모듈화 시켜놓음
doFileUpload(File uploadFile, String originalFileName, String path)
- Spring2.5 - 따로없음. 모듈화. doFileUpload(InputStream is, String originalFileName, String path)
package com.util;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Calendar;
import javax.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Service;
// 일반 class에서는 어노테이션 @Service 붙여주면 됨
// @Service : 이렇게만 쓰면 FileManager(class명과 똑같이)란 이름으로 객체가 생성된거임
// @Service("fileManager") : fileManager 하고 객체 생성이 된 것임
// FileManager fileManager = new FileManager();
@Service("fileManager")
public class FileManager {
public static String doFileUpload(InputStream is,
String originalFileName, String path) throws IOException {
String newFileName = null;
// A.txt 올려줘 -> 새로운파일의이름(newFileName)을 만들어서 업로드시킴(A.txt가 아니라)
// -> saveFileName 에 따로 담아서 저장해두겠다
// 즉, db에는 A.txt가 새로운파일이름을 가지고 있다고 저장해둬야함
if(originalFileName.equals(""))
return null;
//파일 확장자 A.txt에서 .txt (확장자) 만 딱 빼내서 보관해뒀다가
// 바꾼 파일이름에다가 이 저장해둔 확장자(.txt)를 끝에 붙이려고 함
String fileExt = originalFileName
.substring(originalFileName.lastIndexOf("."));
// 뒤에서부터 발견한 점의 위치 부터 끝까지 추출한것을 fileExt에 집어넣겠다
if (fileExt == null || fileExt.equals(""))
return null; // .이 없으면 파일명이 이상한거임 => null 리턴
newFileName =
String.format("%1$tY%1$tm%1$td%1$tH%1$tM%1$tS",
Calendar.getInstance());
// 1$ => 뒤의 매개변수 쓰는것을 하나로 대체
// Calendar.getInstance() 이 객체를 나눠 쓰라는 것
// 년/월/일/시/분/초 까지 만들어짐
newFileName += System.nanoTime(); // 파일 이름이 중복되는걸 막으려고 nanoTime으로 줌
newFileName += fileExt; // .txt 같은 확장자를 뒤에 붙임
//새로운 파일 이름 완성!
//파일경로
File f = new File(path);
if (!f.exists())
f.mkdirs();
String fullFileName = path + File.separator + newFileName;
//struts1처럼 formFile이 알아서 해주는게 아니기 때문에 직접 써야함
FileOutputStream fos = new FileOutputStream(fullFileName); // 내보내니깐
int data = 0;
byte [] buffer = new byte[1024];
while((data=is.read(buffer, 0, 1024))!=-1) {
fos.write(buffer, 0, data);
}
is.close();
fos.close();
return newFileName;
//새롭게 만든 fileName을 반환해줘야만 db에 같이 저장할 수 있음
}
// 파일 다운로드 필수 요건 (매개변수)
// 1. 서버에서 내 컴퓨터로 '다운로드' 받는거기 때문에 response가 있어야함.
// 2. originalFileName serverFileName 을 매핑시키는 작업을 해야하므로 두개 다 필요
// 3. 다운로드 받을 경로
public static boolean doFileDownload(
HttpServletResponse response,String saveFileName,
String originalFileName, String path) {
try {
String fullPath = path + File.separator + saveFileName;
if(originalFileName==null||originalFileName.equals("")) {
originalFileName = saveFileName;
}
// 한글이름 파일을 다운받을때 깨짐 방지
originalFileName = new String(
originalFileName.getBytes("euc-kr"),"ISO-8859-1"); // 이렇게 써줘도 됨 8859_1
File f = new File(fullPath);
if(!f.exists())
return false;
// 파일을받는건 response로 처리해주면 됨
response.setContentType("application/octet-stream"); // 공식
response.setHeader("Content-disposition",
"attachment;fileName="+originalFileName);
// 받은 파일의 header에 딸려있는 파일의 이름을 지정해준 것
//파일은 서버에 저장돼있음
//서버에있는 파일을 내보내려면 일단 "먼저 파일을 읽어내서" 내보내야함
BufferedInputStream bis =
new BufferedInputStream(new
FileInputStream(f));
// 읽어들인 파일을 "출력"한다
OutputStream out = response.getOutputStream();
int data;
byte[] bytes = new byte[4096]; //4096 byte만큼 읽어서 클라이언트한테 내보낼것임
while((data=bis.read(bytes,0,4096))!=-1) {//0~4096까지 읽어서 data에 넣을것임
out.write(bytes,0,data); //0~data의size까지 내보낼것임
}
out.flush();
out.close();
bis.close(); // 사용한 메소드는 다 닫아주기
} catch (Exception e) {
System.out.println(e.toString());
return false; //에러가 나면 false값 리턴
}
return true; //정상 실행 시
}
// <파일 삭제>
//어떤 파일을 지울건지 매개변수로 경로,파일이름 받아야함
//static으로 만들어놔서 메모리 상에 올라가있기때문에
// 다른 곳에서 이 메소드 쓸 때 객체 생성할필요없이
// 바로 FileManager.doFileDelete(); 써주면 됨
public static void doFileDelete(String fileName, String path) {
//파일을 관리하는거니깐 try catch로 묶어줌
try {
String fullPath = path + File.separator + fileName;
File f = new File(fullPath);
if(f.exists()) {
f.delete(); //경로까지 찾아가서 파일이 있으면 삭제해라
}
} catch (Exception e) {
System.out.println(e.toString());
}
}
}
7. 컨트롤러 - FileController.java
package com.fileTest;
import java.io.File;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.multipart.MultipartFile;
import com.util.FileManager;
import com.util.MyUtil;
import com.util.dao.CommonDAO;
//1.controller로 등록시키기
@Controller("fileTest.fileController")
public class FileController {
@Resource(name="dao")
private CommonDAO dao;
@Resource(name="myUtil")
private MyUtil myUtil;
@RequestMapping(value="/file/created.action",
method= {RequestMethod.POST, RequestMethod.GET})
public String created(String mode, HttpSession session,
FileCommand command,HttpServletRequest request) throws Exception{
// 창 띄워주기
if (command.getUpload()==null) {
request.setAttribute("mode", "insert");
return "fileTest/write";
}
MultipartFile file = command.getUpload();
InputStream is = file.getInputStream();
// 1. 파일 업로드
String root = session.getServletContext().getRealPath("/");
System.out.println(root);
String savePath = root + File.separator +
"pds" + File.separator + "data";
// /pds/data/ 경로에 파일 저장할것임
String originalFileName = file.getOriginalFilename();
String saveFileName =
FileManager.doFileUpload(is,originalFileName, savePath);
// 2. DB에 파일정보 저장
if(saveFileName!=null) {
int maxNum = dao.getIntValue("fileTest.maxNum");
command.setNum(maxNum+1);
command.setSaveFileName(saveFileName);
command.setOriginalFileName(originalFileName);
// Spring에 의해 input값인 subject는 이미 command에 들어가 있는 상태
dao.insertData("fileTest.insertData", command);
}
//insert 해줬으니깐 redirect
return "redirect:/file/view.action";
}
// list는 무조건 GET방식으로만 옴
@RequestMapping(value="/file/view.action",
method= {RequestMethod.POST, RequestMethod.GET})
public String view(HttpServletRequest request) throws Exception {
HttpSession session = request.getSession();
String cp = request.getContextPath();
//request에 의해서 만들어 내는 데이터는 spring이 갖다주는거 아님.
// 1. 넘어오는 pageNum
String pageNum = request.getParameter("pageNum");
String downloadPath = cp + "/file/download.action";
String deletePath = cp + "/file/deleted.action";
String imagePath = cp + "/pds/data";
int currentPage = 1;
// 2. delete할 때 세션으로 넘어오는 pageNum
if(pageNum==null || pageNum.equals(""))
pageNum = (String)session.getAttribute("pageNum");
session.removeAttribute("pageNum");
if(pageNum!=null) // 처음 실행된게 아니면
currentPage = Integer.parseInt(pageNum);
//전체 데이터 구하기
int totalDataCount = dao.getIntValue("fileTest.dataCount");
//총 페이지 수 구하기
int numPerPage = 3;
int totalPage = myUtil.getPageCount(numPerPage, totalDataCount);
if (currentPage>totalPage)
currentPage = totalPage;
//db에서 가져올 데이터의 시작과 끝
int start = (currentPage-1)*numPerPage+1;
int end = currentPage*numPerPage;
// 전체 파일 list 뽑아오기
Map<String,Object> hMap = new HashMap<String,Object>();
hMap.put("start", start);
hMap.put("end", end);
List<Object> lists =
dao.getListData("fileTest.listData", hMap);
// 고정 일렬번호를 만들기
Iterator<Object> it = lists.iterator();
int listNum,n=0;
while(it.hasNext()) {
FileCommand dto = (FileCommand)it.next();
listNum = totalDataCount - (start + n-1 );
dto.setListNum(listNum);
n++;
}
// ◀이전 6 7 8 9 10 다음▶ 표시 링크
String urlList = cp + "/file/view.action";
request.setAttribute("lists", lists);
request.setAttribute("pageIndexList",
myUtil.pageIndexList(currentPage, totalPage, urlList));
request.setAttribute("pageNum", pageNum);
request.setAttribute("totalDataCount", totalDataCount);
request.setAttribute("downloadPath", downloadPath);
request.setAttribute("deletePath", deletePath);
request.setAttribute("imagePath", imagePath);
return "fileTest/view";
}
@RequestMapping(value="/file/download.action",
method= {RequestMethod.GET})
public String download(HttpServletRequest request,
HttpServletResponse response) throws Exception {
int num = Integer.parseInt(request.getParameter("num"));
// 파일 저장할 위치정보 불러오기
HttpSession session = request.getSession();
String root = session.getServletContext().getRealPath("/");
String savePath = root + File.separator +
"pds" + File.separator + "data";
// DB에서 파일 정보 불러오기
FileCommand command =
(FileCommand)dao.getReadData("fileTest.readData", num);
FileManager.doFileDownload(response,command.getSaveFileName(),
command.getOriginalFileName(), savePath);
return null;
// 다운로드는 반환값이 null !
}
@RequestMapping(value="/file/deleted.action",
method= {RequestMethod.GET})
public String deleted(HttpServletRequest request)
throws Exception {
String pageNum = request.getParameter("pageNum");
int num = Integer.parseInt(request.getParameter("num"));
dao.deleteData("fileTest.deleteData", num);
return "redirect:/file/view.action?pageNum=" + pageNum;
// 여긴 pageNum이 바로 왔으므로, 세션에 올릴필요 없이 GET방식으로 줘도 됨
}
}