Spring으로 파일을 업로드하는 방법에 대해 알아보자.
스프링이 기본으로 제공하는 MultipartResolver 2가지
- 아래 두가지 MultipartResolver 구현체 중 하나를 스프링 Bean으로 등록해주면 MultipartResolver 사용이 가능하다.
★ 주의: 스프링 Bean의 이름은 반드시 'multipartResolver'로 지정해주어야 함
① o.s.web.multipart.commons.CommonsMultipartResolver
- Commons FileUpload API를 이용해 멀티파트 데이터 처리
② o.s.web.multipart.support.StandardServletMultipartResolver
- 서블릿 3.0의 Part를 이용해 멀티파트 데이터 처리
<!-- fileupload -->
<!-- https://mvnrepository.com/artifact/commons-fileupload/commons-fileupload -->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3</version>
</dependency>
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"/>
<!-- pg 439 파일 업로드 방법 2번째 -->
<multipart-config>
<location>C:\\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>
<bean id="multipartResolver" class="org.springframework.web.multipart.support.StandardServletMultipartResolver"/>
<!-- fileupload -->
<!-- https://mvnrepository.com/artifact/commons-fileupload/commons-fileupload -->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3</version>
</dependency>
<!-- spring io 파일입출력 시 필요-->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>1.4</version>
</dependency>
<!-- cglib -->
<!-- https://mvnrepository.com/artifact/cglib/cglib -->
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>2.2</version>
</dependency>
<!-- tomcat-dbcp 사용 시 필요-->
<!-- https://mvnrepository.com/artifact/org.apache.tomcat/tomcat-dbcp -->
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-dbcp</artifactId>
<version>8.5.84</version>
</dependency>
<!-- p 439 -->
<!-- 반드시 이름은 multipartResolver 으로 설정 -->
<beans:bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<beans:property name="maxUploadSize" value="-1"></beans:property>
<beans:property name="maxInMemorySize" value="1024"></beans:property>
<beans:property name="defaultEncoding" value="ISO-8859-1"></beans:property>
</beans:bean>
private CommonsMultipartFile file;
<form action="" method="post" enctype="multipart/form-data">
<input type="file" id="txtFile" name="file"/>
//getFileNameCheck() 함수
// 저장되는 파일명이 중복되는지 여부를 확인하고, 중복된다면 index를 붙여주는 함수
// 서버에 업로드 되는 폴더에 해당 파일이 저장될 이름(물리적 이름)을 확인해서 중복되면 잘라서 index를 붙임
private String getFileNameCheck(String uploadRealPath, String originalFilename) {
int index = 1;
while( true ) {
File f = new File(uploadRealPath, originalFilename);
if( !f.exists() ) return originalFilename;
// upload 폴더에 originalFilename 파일이 존재한다는 의미 a.txt (4자리)
String fileName = originalFilename.substring(0, originalFilename.length() - 4 ); // a
String ext = originalFilename.substring(originalFilename.length() - 4 ); // .txt
// asdfasf-3.txt
originalFilename = fileName+"-"+(index)+ext;
index++;
} // while
}
//새 글
//@RequestMapping(value="/noticeReg.htm", method=RequestMethod.POST)
@PostMapping("/noticeReg.htm")
public String noticeReg(NoticeVO notice, HttpServletRequest request) throws Exception{
//책 pg 358 커맨드 객체 NoticeVO notice 사용
//1. 첨부파일 유무확인
CommonsMultipartFile multipartFile = notice.getFile();
//2. 첨부파일 저장
String uploadRealPath = null;
if(!multipartFile.isEmpty()) {
//HttpServletRequest request 매개변수 위에 추가해준다.
uploadRealPath = request.getServletContext().getRealPath("/customet/upload"); //upload 폴더에 저장
File saveDir = new File(uploadRealPath);
if(!saveDir.exists())saveDir.mkdirs(); //저장하고자 하는 경로가 없다면 새로 폴더를 만들겠다는 뜻
System.out.println("uploadRealPath:" + uploadRealPath);
String originalFilename = multipartFile.getOriginalFilename(); //서버에 올릴 때 원래 파일명
String filesystemName = getFileNameCheck(uploadRealPath, originalFilename); //getFileNameCheck()함수
//filesystemName은 물리적 이름(실제 업로드 되는 이름), originalFilename은 원래 사용자가 올릴 떄의 이름
File dest = new File(uploadRealPath, filesystemName);
multipartFile.transferTo(dest); //실제 서버에 파일이 업로드 된다.
notice.setFilesrc(filesystemName); //DB에 물리적인 이름으로 저장되도록 함
}//if
NullPointException
- 위 단계까지 완료 한 뒤 이제 파일 업로드를 하면 NullPointException 에러가 발생한다.
- web.xml에서 다음 태그를 주석처리 해준다.
- Why? 우리는 Commons FileUpload 방식을 사용하고 있는데 태그는 StandardServletMultipartResolver 방식이기 때문.
- 중간에 StandardServletMultipartResolver가 먼저 파일을 가져가기 때문에 NullPointException이 발생하는 것!
<!-- pg 439 파일 업로드 방법 2번째 --> <!-- <multipart-config> <location>C:\\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> -->
403 error!
- 위 단계까지 완료 한 뒤 이제 파일 업로드를 하면 403 에러가 발생한다.(Spring Security 사용 시)
- multipart/form-data로 전송할 경우 action경로 뒤에 csrf토큰을 입력해주어야 한다.
- jsp 파일의 form 태그를 다음과 같이 변경해주면 403 에러 해결 완료!
<form action="/customer/noticeReg.htm?${_csrf.parameterName}=${_csrf.token}" method="post" enctype="multipart/form-data">
그러면 이제 다음과 같이 첨부파일이 잘 등록된 것을 확인할 수 있다.
만약 첨부파일을 클릭했을 때 바로 다운로드 되는 것이 아닌 새 창으로 띄우고 싶다면 noticeDetail.jsp 에서 첨부파일이 등록되는 부분의 코드를 수정해주면 된다.
<dl class="article-detail-row">
<dt class="article-detail-title">
첨부파일
</dt>
<dd class="article-detail-data">
<a href="upload/${notice.filesrc}">${notice.filesrc}</a>
</dd>
</dl>
<!-- <a class="btn-del button" href="noticeDel.htm?seq=${notice.seq}">삭제</a> -->
<a class="btn-del button" href="noticeDel.htm?seq=${ notice.seq }&filesrc=${ notice.filesrc }&${_csrf.parameterName}=${_csrf.token}">삭제</a>
//첨부파일이 있다면 넘어오게끔 ?seq=1&filesrc=사진파일.png
//글 삭제
@GetMapping("/noticeDel.htm")
public String noticeDel(
@RequestParam("seq") String seq
,@RequestParam("filesrc") String delFilesrc
,HttpServletRequest request) throws Exception {
//1. 실제 업로드 경로에서 파일을 삭제
String uploadRealPath = request.getServletContext().getRealPath("/customer/upload");
File delFile = new File(uploadRealPath, delFilesrc);
if(delFile.exists())delFile.delete();
//2. DB에서 테이블 레코드 삭제
int deleteCount = this.noticeDao.delete(seq);
if(deleteCount==1) {
return "redirect:notice.htm";
} else return "redirect:noticeDetail.htm?seq=" + seq + "&error";
}
<form action="/customer/noticeEdit.htm?${_csrf.parameterName}=${_csrf.token}" method="post" enctype="multipart/form-data">
<dl class="article-detail-row">
<dt class="article-detail-title">
첨부파일
</dt>
<dd class="article-detail-data">
<input type="file" id="txtFile" name="file" />
<!-- 해당 공지사항 글의 첨부파일명을 출력 -->
<input type= "text" name="o_filesrc" value="${notice.filesrc}"></input>
</dd>
</dl>
<button class="btn-save button" type="submit">수정</button>
//글 수정 제출
@RequestMapping(value = {"/noticeEdit.htm"}, method = RequestMethod.POST)
public String noticeEdit(
NoticeVO notice
, @RequestParam("o_filesrc") String oFilesrc
, HttpServletRequest request) throws Exception {
//1. 첨부파일 유무확인
CommonsMultipartFile multipartFile = notice.getFile();
//2. 첨부파일 저장
String uploadRealPath = null;
if(!multipartFile.isEmpty()) {
uploadRealPath = request.getServletContext().getRealPath("/customer/upload"); //upload 폴더에 저장
File delFile = new File(uploadRealPath, oFilesrc);
if(delFile.exists()){
delFile.delete(); //수정하고자 하는 글에 파일이 이미 존재한다면 파일 삭제
}
// 여기서 이미 이전에 있던 파일은 삭제된 상태
String originalFilename = multipartFile.getOriginalFilename(); //서버에 올릴 때 원래 파일명
String filesystemName = getFileNameCheck(uploadRealPath, originalFilename); //getFileNameCheck()함수
//filesystemName은 물리적 이름(실제 업로드 되는 이름), originalFilename은 원래 사용자가 올릴 떄의 이름
File dest = new File(uploadRealPath, filesystemName);
multipartFile.transferTo(dest); //실제 서버에 파일이 업로드 된다.
notice.setFilesrc(filesystemName);
//기존에 있던 파일 그대로 유지할거면 DB에 물리적인 이름 그대로 저장되도록 함
} else {
notice.setFilesrc(oFilesrc); //새로 파일 올릴거라면 새로운 파일명으로 저장되도록 함
}
//2. 수정 제출
int updatecount = this.noticeDao.update(notice);
if(updatecount ==1) {
return "redirect:noticeDetail.htm?seq=" + notice.getSeq(); //redirect: == response.sendRedirect()
}else
return "redirect:notice.htm";
}
//글 수정 ?seq=1
@GetMapping("/noticeEdit.htm")
public String noticeEdit(@RequestParam("seq") String seq, Model model) throws Exception{
NoticeVO notice = this.noticeDao.getNotice(seq);
model.addAttribute("notice", notice);
return "noticeEdit.jsp";
}
디버깅
- action 속성값이 없어야 url이 seq를 포함해 그대로 넘어가는데, form 태그를 수정하며 seq값이 같이 넘어가지 못한 상황
- form 태그에 seq를 추가해주는 방법과 form 태그 안에 hidden 속성으로 seq를 넘겨주는 방법 두가지가 있다.
- 이렇게 form 태그를 수정해주거나(noticeEdit.jsp)
<form action="/customer/noticeEdit.htm?seq=${ param.seq }&${_csrf.parameterName}=${_csrf.token}" method="post" enctype="multipart/form-data">
- 혹은 form 태그가 닫히기 전에 input 태그를 hidden 속성으로 추가(noticeEdit.jsp)
<input type="hidden" name="seq" value="${ param.seq }"/>
<dl class="article-detail-row">
<dt class="article-detail-title">
첨부파일
</dt>
<dd class="article-detail-data">
<%-- <a href="upload/${notice.filesrc}">${notice.filesrc}</a> --%>
<a href="download.htm?dir=customer/upload&file=${notice.filesrc}">${notice.filesrc}</a>
</dd>
</dl>
// ?dir=customer/upload&file=${ notice.filesrc }
@RequestMapping( "/customer/download.htm")
public void download(
@RequestParam("dir") String d
, @RequestParam("file") String fname
, HttpServletResponse response
, HttpServletRequest request
) throws Exception{
response.setHeader("Content-Disposition","attachment;filename="+ new String(fname.getBytes(), "ISO8859_1"));
String fullPath = request.getServletContext().getRealPath( d + "/" + fname);
FileInputStream fin = new FileInputStream(fullPath);
ServletOutputStream sout = response.getOutputStream();
byte[] buf = new byte[1024];
int size = 0;
while((size = fin.read(buf, 0, 1024)) != -1) {
sout.write(buf, 0, size);
}
fin.close();
sout.close();
}