[Spring] File Upload & Download
2024. 7. 6. 23:08ㆍBE/Spring
0. 기존 포스팅
추가한 이유:
- java 라이선스 문제로 6.x 버전부터는
CommnonsMultipartResolver
를 사용할 수 없다. - 업로드할 파일의 정보를 저장하는 별도의
FileInfoDto
를 정의해 업로드하는 방법이니깐 알아두자.
1. File Upload
가. pom.xml
<dependencies>
...
<!-- File Upload -->
<!-- https://mvnrepository.com/artifact/commons-fileupload/commons-fileupload -->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>${commons-fileupload-version}</version>
</dependency>
</dependencies>
나. servlet-context.xml
<!-- servlet-context.xml -->
<beans:beans>
...
<!-- fileUpload -->
<!-- spring 5.x fileupload -->
<!-- <beans:bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<beans:property name="defaultEncoding" value="UTF-8"/>
<beans:property name="maxUploadSize" value="52428800"/>
<beans:property name="maxInMemorySize" value="1048576"/>
</beans:bean> -->
<!-- 6.x -->
<beans:bean id="multipartResolver" class="org.springframework.web.multipart.support.StandardServletMultipartResolver"/>
<!-- // fileUpload -->
...
</beans:beans>
- java 라이센스 문제로 6.x 버전부터는
CommnonsMultipartResolver
를 사용할 수 없다. (javax naming issue) - 대신에
StandardServletMultipartResolver
를 사용한다.
<!-- servlet-context.xml -->
<!-- 6.x -->
<beans:bean id="multipartResolver" class="org.springframework.web.multipart.support.StandardServletMultipartResolver"/>
id는 반드시 multipartResolver
로 등록한다.
다. 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>
<max-file-size>52428800</max-file-size><!-- 파일 하나당 최대 파일 크기 -->
<max-request-size>52428800</max-request-size><!-- 업로드 파일의 총 크기 -->
<file-size-threshold>0</file-size-threshold><!-- 업로드하는 파일이 임시로 파일로 저장되지 않고 메모리에서 바로 스트림으로 전달되는 크기의 한계 1024 * 1024로 설정하면 1MB 이상인 경우에만 임시 파일로 저장. -->
<!-- <location></location> --><!-- 임시저장 경로 -->
</multipart-config>
</servlet>
servlet이 Multipart를 처리하도록 설정.
설정 | 설명 |
<max-file-size> | 업로드 하는 파일의 업로드 가능한 최대 파일 사이즈. 바이트 단위. -1로 설정 시 크기 제한 없음(default) |
<max-request-size> | 한번의 요청에 포함된 전체 Multipart 요청 데이터의 최대 크기. 바이트 단위. -1로 설정 시 크기 제한 없음(default) |
<location> | 임시 저장 경로 |
<file-size-threshold>0</file-size-threshold> | 업로드하는 파일이 임시로 파일로 저장되지 않고 메모리에서 바로 스트림으로 전달되는 크기의 한계. 1024 * 1024로 설정하면 1MB 이상인 경우에만 임시 파일로 저장. |
라. JSP
<form id="form-register" method="POST" enctype="multipart/form-data" action="">
<input type="file" class="form-control border" id="upfile" name="upfile" multiple="multiple">
</form>
<input type="file" class="form-control border" id="upfile" name="upfile" multiple="multiple">
:multiple
을 설정하면 여러 파일을 업로드할 수 있다.
마. Java
1) DTO
public class BoardDto {
private int articleNo;
private String userId;
private String userName;
private String subject;
private String content;
private int hit;
private String registerTime;
private List<FileInfoDto> fileInfos;
// getter, setter method 생략
}
public class FileInfoDto {
private String saveFolder;
private String originalFile;
private String saveFile;
// gettter, setter method 생략
}
saveFolder
: 저장된 파일 경로.originalFile
: 원래 파일 이름.saveFile
: 서버에 저장될 때 파일 이름.
2) Controller
// BoardController
@Controller
@RequestMapping("/article")
public class BoardController {
@PostMapping("/write")
public String write(@ModelAttribute("boardDto") BoardDto boardDto,
@RequestParam(value = "upfile", required = false) MultipartFile[] files, HttpSession session,
RedirectAttributes redirectAttributes) throws Exception {
logger.debug("write boardDto : {}", boardDto);
MemberDto memberDto = (MemberDto) session.getAttribute("userinfo");
boardDto.setUserId(memberDto.getUserId());
// FileUpload 관련 설정.
logger.debug("MultipartFile.isEmpty : {}", files[0].isEmpty());
if (!files[0].isEmpty()) {
String realPath = servletContext.getRealPath("/upload");
// String realPath = servletContext.getRealPath("/resources/img");
String today = new SimpleDateFormat("yyMMdd").format(new Date());
String saveFolder = realPath + File.separator + today;
logger.debug("저장 폴더 : {}", saveFolder);
File folder = new File(saveFolder);
if (!folder.exists())
folder.mkdirs();
List<FileInfoDto> fileInfos = new ArrayList<FileInfoDto>();
for (MultipartFile mfile : files) {
FileInfoDto fileInfoDto = new FileInfoDto();
String originalFileName = mfile.getOriginalFilename();
if (!originalFileName.isEmpty()) {
String saveFileName = UUID.randomUUID().toString()
+ originalFileName.substring(originalFileName.lastIndexOf('.'));
fileInfoDto.setSaveFolder(today);
fileInfoDto.setOriginalFile(originalFileName);
fileInfoDto.setSaveFile(saveFileName);
logger.debug("원본 파일 이름 : {}, 실제 저장 파일 이름 : {}", mfile.getOriginalFilename(), saveFileName);
mfile.transferTo(new File(folder, saveFileName));
}
fileInfos.add(fileInfoDto);
}
boardDto.setFileInfos(fileInfos);
}
boardService.writeArticle(boardDto);
redirectAttributes.addAttribute("pgno", "1");
redirectAttributes.addAttribute("key", "");
redirectAttributes.addAttribute("word", "");
// redirectAttributes.addFlashAttribute("test", "1234");
return "redirect:/article/list";
}
@RequestParam(value = "upfile", required = false) MultipartFile[] files
:input
tag의multiple="multiple"
을 설정하면 받을 때 배열이나 리스트로 받아야 한다.originalFileName
: 원래 이름. 클라이언트에게 표시되는 이름.saveFileName
: 서버에 저장되는 이름. 서버 안에서 사용되는 이름.mfile.transferTo(new File(folder, saveFileName));
: 실제 파일을 저장하는 부분.folder
에saveFileName
으로 저장.redirectAttributes.addAttribute("pgno", "1");
: redirect 시 넘겨줄 정보.
3) DAO
// BoardDao
public void writeArticle(BoardDto boardDto) throws SQLException {
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = dataSource.getConnection();
conn.setAutoCommit(false);
StringBuilder sql = new StringBuilder();
sql.append("insert into board (user_id, subject, content, hit, register_time) \n");
sql.append("values (?, ?, ?, 0, now())");
pstmt = conn.prepareStatement(sql.toString());
pstmt.setString(1, boardDto.getUserId());
pstmt.setString(2, boardDto.getSubject());
pstmt.setString(3, boardDto.getContent());
pstmt.executeUpdate();
pstmt.close();
List<FileInfoDto> fileInfos = boardDto.getFileInfos();
if (fileInfos != null && !fileInfos.isEmpty()) {
String lastNo = "select last_insert_id()";
pstmt = conn.prepareStatement(lastNo);
rs = pstmt.executeQuery();
rs.next();
int articleno = rs.getInt(1);
pstmt.close();
StringBuilder reigsterFile = new StringBuilder();
reigsterFile.append("insert into file_info (article_no, save_folder, original_file, save_file) \n");
reigsterFile.append("values");
int size = fileInfos.size();
for (int i = 0; i < size; i++) {
reigsterFile.append("(?, ?, ?, ?)");
if (i != fileInfos.size() - 1)
reigsterFile.append(",");
}
pstmt = conn.prepareStatement(reigsterFile.toString());
int idx = 0;
for (int i = 0; i < size; i++) {
FileInfoDto fileInfo = fileInfos.get(i);
pstmt.setInt(++idx, articleno);
pstmt.setString(++idx, fileInfo.getSaveFolder());
pstmt.setString(++idx, fileInfo.getOriginalFile());
pstmt.setString(++idx, fileInfo.getSaveFile());
}
pstmt.executeUpdate();
}
conn.commit();
} catch (SQLException e) {
e.printStackTrace();
conn.rollback();
throw new SQLException();
} finally {
dbUtil.close(rs, pstmt, conn);
}
}
last_insert_id()
: 테이블의 마지막auto_increment
값을 반환하는 함수.insert into file_info (article_no, save_folder, original_file, save_file) values(?, ?, ?, ?), …
: 파일을 저장하기 위한 별도의 테이블을 사용하고 있다.conn.setAutoCommit(false);
,conn.rollback();
,conn.commit();
: 두 개의 SQL 중 하나라도 정상적으로 동작하지 않은 경우rollback
한다.
// BoardDao
@Override
public BoardDto getArticle(int articleNo) throws SQLException {
BoardDto boardDto = null;
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = dataSource.getConnection();
StringBuilder listArticle = new StringBuilder();
listArticle.append(
"select b.article_no, b.user_id, b.subject, b.content, b.hit, b.register_time, m.user_name \n");
listArticle.append("from board b, members m \n");
listArticle.append("where b.user_id = m.user_id \n");
listArticle.append("and b.article_no = ? \n");
pstmt = conn.prepareStatement(listArticle.toString());
pstmt.setInt(1, articleNo);
rs = pstmt.executeQuery();
if (rs.next()) {
boardDto = new BoardDto();
boardDto.setArticleNo(rs.getInt("article_no"));
boardDto.setUserId(rs.getString("user_id"));
boardDto.setUserName(rs.getString("user_name"));
boardDto.setSubject(rs.getString("subject"));
boardDto.setContent(rs.getString("content"));
boardDto.setHit(rs.getInt("hit"));
boardDto.setRegisterTime(rs.getString("register_time"));
PreparedStatement pstmt2 = null;
ResultSet rs2 = null;
try {
StringBuilder fileInfos = new StringBuilder();
fileInfos.append("select save_folder, original_file, save_file \n");
fileInfos.append("from file_info \n");
fileInfos.append("where article_no = ?");
pstmt2 = conn.prepareStatement(fileInfos.toString());
pstmt2.setInt(1, articleNo);
rs2 = pstmt2.executeQuery();
List<FileInfoDto> files = new ArrayList<FileInfoDto>();
while (rs2.next()) {
FileInfoDto fileInfoDto = new FileInfoDto();
fileInfoDto.setSaveFolder(rs2.getString("save_folder"));
fileInfoDto.setOriginalFile(rs2.getString("original_file"));
fileInfoDto.setSaveFile(rs2.getString("save_file"));
files.add(fileInfoDto);
}
boardDto.setFileInfos(files);
} finally {
dbUtil.close(rs2, pstmt2);
}
}
} finally {
dbUtil.close(rs, pstmt, conn);
}
return boardDto;
}
- 게시물을 읽을 때 사용하는 방법이다.
- 게시물을 먼저 조회하고 성공하면 파일을 조회한다.
2. File Download
가. JSP
<!-- view.jsp -->
<li>${file.originalFile}
<a href="#" class="filedown" sfolder="${file.saveFolder}" sfile="${file.saveFile}" ofile="${file.originalFile}">
[다운로드]
</a>
<form id="downform" action="${root}/article/download">
<input type="hidden" name="sfolder">
<input type="hidden" name="ofile">
<input type="hidden" name="sfile">
</form>
<script>
let files = document.queryselectorall(".filedown");
files.foreach(function(file) {
file.addeventlistener("click", function() {
document.queryselector("[name='sfolder']").value = file.getattribute("sfolder");
document.queryselector("[name='ofile']").value = file.getattribute("ofile");
document.queryselector("[name='sfile']").value = file.getattribute("sfile");
document.queryselector("#downform").submit();
});
});
</script>
나. servlet-context.xml
<!-- servlet-context.xml -->
<beans:beans>
...
<!-- fileDownload -->
<beans:bean id="fileDownLoadView" class="com.company.board.view.FileDownLoadView"/>
<!-- BeanNameViewResolver 설정. -->
<beans:bean id="fileViewResolver" class="org.springframework.web.servlet.view.BeanNameViewResolver">
<beans:property name="order" value="0" />
</beans:bean>
<!-- // fileDownload -->
...
</beans:beans>
<beans:property name="order" value="0" />
: 0순위로 설정.InternalResourceViewResolver
보다 우선적으로 처리한다.
다. FileDownLoadView
package com.company.board.view;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.util.Map;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.servlet.view.AbstractView;
import jakarta.servlet.ServletContext;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
public class FileDownLoadView extends AbstractView {
public FileDownLoadView() {
setContentType("apllication/download; charset=UTF-8");
}
@Override
protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
ServletContext ctx = getServletContext();
String realPath = ctx.getRealPath("/upload");
Map<String, Object> fileInfo = (Map<String, Object>) model.get("downloadFile"); // 전송받은 모델(파일 정보)
String saveFolder = (String) fileInfo.get("sfolder"); // 파일 경로
String originalFile = (String) fileInfo.get("ofile"); // 원본 파일명(화면에 표시될 파일 이름)
String saveFile = (String) fileInfo.get("sfile"); // 암호화된 파일명(실제 저장된 파일 이름)
File file = new File(realPath + File.separator + saveFolder, saveFile);
response.setContentType(getContentType());
response.setContentLength((int) file.length());
String header = request.getHeader("User-Agent");
boolean isIE = header.indexOf("MSIE") > -1 || header.indexOf("Trident") > -1;
String fileName = null;
// IE는 다르게 처리
if (isIE) {
fileName = URLEncoder.encode(originalFile, "UTF-8").replaceAll("\\+", "%20");
} else {
fileName = new String(originalFile.getBytes("UTF-8"), "ISO-8859-1");
}
response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\";");
response.setHeader("Content-Transfer-Encoding", "binary");
OutputStream out = response.getOutputStream();
FileInputStream fis = null;
try {
fis = new FileInputStream(file);
FileCopyUtils.copy(fis, out);
} catch (Exception e) {
e.printStackTrace();
} finally {
if(fis != null) {
try {
fis.close();
}catch (IOException e) {
e.printStackTrace();
}
}
}
out.flush();
}
}
라. Controller
@Controller
@RequestMapping("/article")
public class BoardController {
@GetMapping("/download")
public ModelAndView downloadFile(@RequestParam("sfolder") String sfolder, @RequestParam("ofile") String ofile,
@RequestParam("sfile") String sfile) {
Map<String, Object> fileInfo = new HashMap<String, Object>();
fileInfo.put("sfolder", sfolder);
fileInfo.put("ofile", ofile);
fileInfo.put("sfile", sfile);
return new ModelAndView("fileDownLoadView", "downloadFile", fileInfo);
}
Map<String, Object> fileInfo = new HashMap<String, Object>();
: 다운로드할 파일의 경로(sfolder
), 원본 파일명(ofile
), 저장된 파일명(sfile
)를 담는Map
을 생성한다.return new ModelAndView("fileDownLoadView", "downloadFile", fileInfo);
:ModelAndView
객체를 반환한다.
: View name은"fileDownLoadView"
: model name은"downloadFile"
: model Object는 앞서 만든Map
객체return new ModelAndView("fileDownLoadView", "downloadFile", fileInfo);
:servlet-context.xml
에서 등록한FileDownLoadView
로 가서 다운로드를 마무리짓는다.
3. Multipart
form
에는 다양한 타입의 데이터가 존재할 수 있다. (예시 : 문자열, 업로드할 파일)
이렇게 하나의 request에 서버가 해석해야 하는 Content-type이 두 가지 이상 되는 경우가 생긴다.
서버는 이를 구분해서 해석하기 위해서 mulitpart type
을 사용한다.
form
의 attribute에 반드시 enctype="multipart/form-data
를 추가해야 하고 method는 POST
만 가능하다.
4. 공식 홈페이지
위의 설명과는 다른 방식으로 파일 업로드를 구현했다.
참고.
'BE > Spring' 카테고리의 다른 글
[MyBatis] MyBatis란? (0) | 2024.07.06 |
---|---|
[Spring] Connection Pool (0) | 2024.07.06 |
[Spring] ControllerAdvice (0) | 2024.07.06 |
[Spring] Java Config (0) | 2024.07.06 |
[Spring] AOP (0) | 2024.07.06 |