0. 기존 포스팅
![Featured image for [Spring] 도서 등록, 검색](https://tired-i.com/wp-content/uploads/2026/01/spring-image-8.png)
[Spring] 도서 등록, 검색
0. 출처 아직 배우고 있는 중이라 부정확한 정보가 포함되어 있을 수 있습니다!주의하세요! 올인원 스프링 프레임워크 참고. 1. 관리자 관련 기능 2. 신규 도서 등록 가. 환경 설정 필요한 패키지와 클래스를 추가한다. BookVo를 구현한다. file은 없다. 데이터베이스에 테이블을 생성한다. 나. 도서 등록 화면 신규 도서를 등록하는 페이지로 이동하는 기능 구현하기… 더 읽기
추가한 이유:
- 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>
Code language: HTML, XML (xml)
나. 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>
Code language: HTML, XML (xml)
- java 라이센스 문제로 6.x 버전부터는
CommnonsMultipartResolver를 사용할 수 없다. (javax naming issue) - 대신에
StandardServletMultipartResolver를 사용한다.
<!-- servlet-context.xml -->
<!-- 6.x -->
<beans:bean id="multipartResolver" class="org.springframework.web.multipart.support.StandardServletMultipartResolver"/>
Code language: HTML, XML (xml)
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>
Code language: HTML, XML (xml)
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>
Code language: HTML, XML (xml)
<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 생략
}
Code language: PHP (php)
public class FileInfoDto {
private String saveFolder;
private String originalFile;
private String saveFile;
// gettter, setter method 생략
}
Code language: PHP (php)
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";
}
Code language: JavaScript (javascript)
@RequestParam(value = "upfile", required = false) MultipartFile[] files
:inputtag의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);
}
}
Code language: JavaScript (javascript)
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;
}
Code language: PHP (php)
- 게시물을 읽을 때 사용하는 방법이다.
- 게시물을 먼저 조회하고 성공하면 파일을 조회한다.
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>
Code language: HTML, XML (xml)
나. 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>
Code language: HTML, XML (xml)
<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();
}
}
Code language: JavaScript (javascript)
라. 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);
}
Code language: JavaScript (javascript)
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. 공식 홈페이지
위의 설명과는 다른 방식으로 파일 업로드를 구현했다.
참고.
