파일 첨부를 하기 위해서는 라이브러리를 더 다운로드해서 적용해야 한다.
필요한 라이브러리는 아래와 같다.
- commons-fileupload-1.4
- commons-io-2.11.0
- thumbnailator-0.4.17
MVN Repository에서 필요한 라이브러리를 찾아 Eclipse에 WEB-INF > lib에 라이브러리들을 넣어둔다.
MVN Repository의 링크는 다음과 같다.
일단, 게시물을 등록할 때 파일 첨부도 할 수 있게 하는 것만 코드를 작성한다.
border > register.jsp에서 파일을 첨부할 수 있도록 <input> 태그를 생성한다.
그리고 form에 enctype를 추가로 달아준다.
enctype는 form 데이터를 서버로 전송할 때 사용되는 인코딩 방식을 의미하는데
파일 업로드와 같은 이진데이터를 전송할 때 사용되는 multipart/from-data로 지정하였다.
<form action="/brd/insert" method="post" encytype="multipart/form-data">
파일 첨부의 input 태그는 accept를 추가하여 사용자에게 허용된 파일 유형을 제한 할 수 있으나
나는 별도로 제한을 두지 않을 것이므로 accept는 작성하지 않았다.
<!-- 업로드 파일 유형을 제한 할 때
예) 이미지 파일들만 허용할 때-->
<input type="file" name="file" accept="image/png, image/jsp, image/gif, imag/jpeg"><br>
<!-- 업로드 파일 유형을 제한하지 않을 때-->
<input type="file" name="file">
register.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>자유게시판</title>
</head>
<body>
<h1>자유게시판 글쓰기</h1>
<form action="/brd/insert" method="post" enctype="multipart/form-data">
제목 : <input type="text" name="title"><br>
<!-- 작성자는 글쓰기에서 노출 안시키고 값만 서버로
(리스트에는 작성자 명을 띄워야 하니까) -->
<input type="hidden" value="${ses.id }"><br>
내용 :<br>
<textarea rows="10" cols="30" name=content></textarea><br>
첨부파일 : <input type="file" name="board_file"><br>
<button type="submit">등록</button>
<a href="/brd/list"><button type="button">취소</button></a>
</form>
</body>
</html>
첨부 파일 업로드 가능한 글쓰기 화면
게시글을 쓸 때 첨부한 파일이 저장될 수 있도록 webapp 하위에 _fileUpload 폴더를 생성했다.
BoardVO에 첨부파일 변수를 선언하고 DB의 board 테이블에는 첨부파일 컬럼을 추가해야 된다.
비교적 간단하게 진행할 수 있는 DB의 컬럼 추가를 먼저 진행했다.
sql.sql
# ... (기존 코드)
-- 2023-12-11
ALTER TABLE board ADD boardFile VARCHAR(100);
BoardVO는 변수 선언 뒤에 detail에서 사용할 생성자에만 매개 변수로 주었다.
잊지 않고 getter/setter도 생성해 주었고 toString에도 추가하였다.
BoardVO.java
package domain;
public class BoardVO {
private int bno;
private String title;
private String writer;
private String content;
private String regdate;
private String moddate;
private int readcount;
private String boardFile;
// ... (기존 코드)
// detail : 전부 다 + boardFile
public BoardVO(int bno, String title, String writer, String content,
String regdate, String moddate, int readcount, String boardFile) {
this.bno = bno;
this.title = title;
this.writer = writer;
this.content = content;
this.regdate = regdate;
this.moddate = moddate;
this.readcount = readcount;
this.boardFile = boardFile;
}
// ... (기존 코드)
public String getBoardFile() {
return boardFile;
}
public void setBoardFile(String boardFile) {
this.boardFile = boardFile;
}
@Override
public String toString() {
return "BoardVO [bno=" + bno + ", title=" + title + ", writer=" + writer + ", content=" + content + ", regdate="
+ regdate + ", moddate=" + moddate + ", readcount=" + readcount + ", boardFile=" + boardFile + "]";
}
}
BoardController는 case insert 부분을 싹 갈아 엎야아한다.
기존에 작성했던 코드는 파일 업로드가 없는 경우를 가정해서 만든 코드이기 때문이다.
// 파일 업로드가 없는 경우를 가정하고 썼던 코드
// ses.id 값 받아와야 하니까
HttpSession ses = request.getSession();
MemberVO mvo = (MemberVO) ses.getAttribute("ses");
String writer = mvo.getId();
//register.jsp에서 받아오는 것은 title / content
String title = request.getParameter("title");
String content = request.getParameter("content");
BoardVO bvo = new BoardVO(title, writer, content);
log.info(">>> 글쓰기 >>> {}", bvo);
isOk = bsv.insert(bvo);
log.info(">>> insert " + (isOk > 0 ? "OK" : "FAIL"));
if(isOk > 0) {
request.setAttribute("msg_new", "new");
}
destPage = "/brd/list";
case insert를 갈아엎기 전에 첨부한 파일이 저장되는 경로 변수를 먼저 선언한다.
private String savePath;
변수를 선언 했다면 일단, BoardController의 case insert 코드가 어떻게 변했는지 전체적으로 먼저 살펴보자
BoardController.java
package controller;
import java.io.File;
import java.io.IOException;
import java.util.List;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import domain.BoardVO;
import domain.MemberVO;
import domain.PagingVO;
import handler.PagingHandler;
import net.coobird.thumbnailator.Thumbnails;
import service.BoardService;
import service.BoardServiceImpl;
@WebServlet("/brd/*")
public class BoardController extends HttpServlet {
private static final long serialVersionUID = 1L;
private static final Logger log = LoggerFactory.getLogger(BoardController.class);
private RequestDispatcher rdp;
private String destPage;
private int isOk;
private BoardService bsv;
private String savePath;
public BoardController() {
bsv = new BoardServiceImpl();
}
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// ... (기존 코드)
case "register":
destPage = "/board/register.jsp";
break;
case "insert":
try {
//파일을 업로드할 물리적인 경로 설정
savePath = getServletContext().getRealPath("/_fileUpload");
File fileDir = new File(savePath);
log.info(">>> 파일 저장위치 >>> {}", savePath);
DiskFileItemFactory fileItemFactory = new DiskFileItemFactory();
fileItemFactory.setRepository(fileDir); // 저장할 위치를 file 객체로 지정
fileItemFactory.setSizeThreshold(1024*1024*3); // 파일 저장을 위한 임시 메모리 설정 : byte 단위
// 미리 객체 설정
BoardVO bvo = new BoardVO();
// multipart/form-date 형식으로 넘어온 request 객체를 다루기 쉽게 변환해주는 역할
ServletFileUpload fileUpload = new ServletFileUpload(fileItemFactory);
List<FileItem>itemList = fileUpload.parseRequest(request);
for(FileItem item : itemList) {
switch(item.getFieldName()) {
case "title":
bvo.setTitle(item.getString("utf-8"));
break;
case "writer":
bvo.setWriter(item.getString("utf-8"));
break;
case "content":
bvo.setContent(item.getString("utf-8"));
break;
case "board_file":
// 이미지 있는지 체크
if(item.getSize() > 0) { // 데이터의 크기를 바이트 단위로 리턴 / 크기가 0보다 큰지 체크
String fileName = item.getName()
.substring(item.getName().lastIndexOf(File.separator)+1); // 이름만 분리
// File.separator : 파일 경로 기호를 저장
// 시스템의 기간을 이용하여 파일을 구분 / 시간_dog.jpg
fileName = System.currentTimeMillis()+"_"+fileName;
File uploadFilePath = new File(fileDir + File.separator+fileName);
log.info(" >>> uploadFilePath >>> {}", uploadFilePath.toString());
// 저장
try {
item.write(uploadFilePath); // 자바 객체를 디스크에 쓰기
bvo.setBoardFile(fileName); // bvo에 저장할 값 설정
// 썸네일 작업 : 리스트 페이지에서 트래픽 과다사용 방지
Thumbnails.of(uploadFilePath).size(75, 75)
.toFile(new File(fileDir + File.separator + "th_" + fileName));
} catch (Exception e) {
log.info(">>> file writer on dist error");
e.printStackTrace();
}
}
break;
}
}
isOk = bsv.insert(bvo);
log.info(">>> insert " + (isOk > 0 ? "OK" : "FAIL"));
if(isOk > 0) {
request.setAttribute("msg_new", "new");
}
destPage = "/brd/list";
} catch (Exception e) {
log.info(">>> insert error");
e.printStackTrace();
}
break;
// ... (기존 코드)
}
여지껏 비슷비슷한 느낌으로 코드를 짜다가 갑자기 이게 무슨 일인가 싶다.
매일매일이 새로운 지식 너무 고마와.. 절거워... 호호...
신나게 코드를 하나하나 뜯어서 들여다보자...
파일을 업로드할 물리적인 경로를 설정하기 위해 _fileUpload 폴더를 savePath로 지정하고,
이를 이용하여 File 객체를 생성한다.
File 객체는 파일이나 디렉토리에 관한 경로 및 속성을 다루는데 사용한다.
savePath = getServletContext().getRealPath("/_fileUpload");
File fileDir = new File(savePath);
다음 코드들은 전부 Apache Commons FileUpload 라이브러리를 사용한다.
DiskFileItemFactory는 파일 아이템 (업로드된 파일이나 폼 필드)을 메모리나 디스크에 저장하는데
사용되는 facotry이다. 파일 업로드를 위한 기본적인 설정을 담당하고 실제로 업로드된 파일의 처리 방식을 지정한다.
fileDir은 앞서 설정한 파일 시스템 경로를 가리키며 파일 아이템을 저장할 위치로 설정하고 있다.
이렇게 설정하면 업로드된 파일은 해당 디렉토리에 저장된다.
fileItemFactory.setSizeThreshold(1024*1024*3)은 파일의 크기가 메모리에 저장될 한계치를 설정한다.
나는 3MB로 설정했고 이보다 큰 파일은 디스크에 저장된다.
DiskFileItemFactory fileItemFactory = new DiskFileItemFactory();
fileItemFactory.setRepository(fileDir);
fileItemFactory.setSizeThreshold(1024*1024*3);
fileItemFactory를 사용해서 파일 업로드 설정을 적용한다.
이를 기반으로 한 ServletFileUpload 객체는 클라이언트로부터 전송된 HTTP 요청에서
파일 아이템을 추출하는 주체로 사용된다.
* 클라이언트 : 웹 개발에서 사용자의 웹 브라우저를 가리킴.
그리고 FileItem은 추출된 파일이나 폼 필드를 나타내는 객체이다.
ServletFileUpload fileUpload = new ServletFileUpload(fileItemFactory);
List<FileItem>itemList = fileUpload.parseRequest(request);
switch는 파일 아이템의 이름(필드 이름)에 따라 분기한다.
title, writer, content는 문자열 값을 UTF-8 인코딩으로 읽어서 bvo 객체에 설정된다.
for(FileItem item : itemList) {
switch(item.getFieldName()) {
case "title":
bvo.setTitle(item.getString("utf-8"));
break;
case "writer":
bvo.setWriter(item.getString("utf-8"));
break;
case "content":
bvo.setContent(item.getString("utf-8"));
break;
fileName은 업로드된 파일의 원래 이름을 가져와서 파일 경로에서 파일 이름만 추출한다. 그 후 업로드된 파일 이름 앞에 현재 시간을 추가해서 중복을 방지한다.
case "board_file":
if(item.getSize() > 0) {
String fileName = item.getName()
.substring(item.getName().lastIndexOf(File.separator)+1);
fileName = System.currentTimeMillis()+"_"+fileName;
파일을 저장할 경로와 이름을 결합해서 File 객체를 생성한다.
예를들어서 fileDir = "/home/user/fileUpload" 이고, fileName = "example.txt"라면
uploadFilePath = "/home/user/fileUpload/example.txt"가 된다.
File uploadFilePath = new File(fileDir + File.separator+fileName);
지정해준 경로에 파일을 저장하고 bvo 객체 속성 boardFile에 fileName을 설정한다.
item.write(uploadFilePath);
bvo.setBoardFile(fileName);
썸네일을 생성할 원본 이미지 파일의 경로를 지정하고, 75x75 pixel 크기로 생성하도록 했다.
그리고 그 썸네일을 지정된 경로에 파일로 저장한다.
썸네일 생성은 리스트 페이지 등에서 빠른 로딩을 위해 사용될 수 있다.
Thumbnails.of(uploadFilePath).size(75, 75)
.toFile(new File(fileDir + File.separator + "th_" + fileName));
controller 작성이 완료되면 DB에도 첨부 파일이 업데이트 될 수 있도록 sql 구문 안에 boardFile을 추가한다.
boardMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="BoardMapper">
<!-- ... (기존 코드) -->
<insert id="reg">
insert into board(title, writer, content, boardFile)
value (#{title}, #{writer}, #{content}, #{boardFile})
</insert>
<!-- ... (기존 코드) -->
</mapper>
게시글 등록 후 상세 페이지에서 첨부 파일을 띄울 수 있는지 확인을 하기 위해서 detail.jsp에도 코드를 추가한다.
detail.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>자유 게시판</title>
</head>
<body>
<h1>상세 페이지</h1>
<img alt="" src="/_fileUpload/${bvo.boardFile }">
<table>
<!-- ... (기존 코드) -->
</table>
<!-- ... (기존 코드) -->
</body>
</html>
글쓰기 화면 파일 첨부 시
상세 페이지 파일 첨부 시
게시판 리스트의 제목 앞에도 이미지를 썸네일로 한 번 띄워보자.
list.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>자유게시판</title>
</head>
<body>
<h1>자유게시판</h1>
<!-- ... (기존 코드) -->
<table>
<tr>
<th>No</th>
<th>제목</th>
<th>작성자</th>
<th>작성일</th>
<th>조회수</th>
</tr>
<!-- DB에서 가져온 리스트를 c:forEach를 통해 반복
var="bvo"는 BoardVO 객체를 참조한다. -->
<c:forEach items="${list}" var="bvo">
<tr>
<td>${bvo.bno }</td>
<td><a href="/brd/detail?bno=${bvo.bno }"><img alt="" src="/_fileUpload/${bvo.boardFile }">${bvo.title }</a></td>
<td>${bvo.writer }</td>
<td>${bvo.regdate }</td>
<td>${bvo.readcount }</td>
</tr>
</c:forEach>
</table>
<!-- ... (기존 코드) -->
</body>
</html>
게시판 리스트 썸네일 추가 화면
가지고 있는 이미지들이 너무 커서 보기에 좋지 않다.
나중에 css로 화면을 좀 다듬어 봐야겠다.
[JSP/Servlet] 13. 게시글 파일첨부 - 등록 / 상세 / 리스트
(다음 게시물 예고편)
[JSP/Servlet] 14. 게시글 파일첨부 - 수정과 삭제
얼렁뚱땅 주니어 개발자
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!