안녕하세요, 코린이의 코딩 학습기 채니 입니다.
개인 포스팅용으로 내용에 오류 및 잘못된 정보가 있을 수 있습니다.
파일을 업로드/다운로드는 아래와 같이 입출력의 연속입니다.
사용자가 첨부파일을 보낼 때 request msg에 전송을 해주고 (브라우저가 처리), 서버 쪽에서는 이를 읽어다가 서버 컴퓨터에 저장합니다. (외부라이브러리 - cos.jar 이용 예정!)
※ 참고포스팅
https://lena-chamna.netlify.app/post/http_multipart_form-data/
cos.jar download
다운로드 완료되었다면, lib - cos.jar 파일 복사 - 이클립스 WEB-INF - lib 폴더에 붙여넣기
첨부파일이 포함된 게시글 등록
- 파일 업로드를 포함하고 있는 폼 전송은 반드시 POST
- enctype="multipart/form-data" 설정
boardEnroll.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ include file="/WEB-INF/views/common/header.jsp" %>
<% %>
<link rel="stylesheet" href="<%=request.getContextPath()%>/css/board.css" />
<section id="board-container">
<h2>게시판 작성</h2>
<form
name="boardEnrollFrm"
action="<%=request.getContextPath() %>/board/boardEnroll"
method="post"
enctype="multipart/form-data">
<table id="tbl-board-view">
<tr>
<th>제 목</th>
<td><input type="text" name="title" required></td>
</tr>
<tr>
<th>작성자</th>
<td>
<input type="text" name="writer" value="<%= loginMember.getMemberId() %>" readonly/>
</td>
</tr>
<tr>
<th>첨부파일</th>
<td>
<input type="file" name="upFile1">
<input type="file" name="upFile2">
</td>
</tr>
<tr>
<th>내 용</th>
<td><textarea rows="5" cols="40" name="content"></textarea></td>
</tr>
<tr>
<th colspan="2">
<input type="submit" value="등록하기">
</th>
</tr>
</table>
</form>
</section>
<script>
/**
* boardEnrollFrm 유효성 검사
*/
document.boardEnrollFrm.onsubmit = (e) => {
const frm = e.target;
//제목을 작성하지 않은 경우 폼제출할 수 없음.
if(!/^.+/.test(frm.title.value)) {
alert("제목을 작성해주세요.");
frm.title.focus();
return false;
}
//내용을 작성하지 않은 경우 폼제출할 수 없음.
if(!/^(.|\n)+$/.test(frm.content.value)) {
alert("내용을 작성해주세요.");
frm.content.focus();
return false;
}
}
</script>
<%@ include file="/WEB-INF/views/common/footer.jsp" %>
Servlet
① 서버 컴퓨터에 파일 저장 - cos.jar
- MultipartRequest 객체 생성
- HttpServletRequest
- saveDirectory (저장디렉토리)
- maxPostSize (용량)
- encoding
- FileRenamePolicy 객체 - DefaultFileRenamePolicy (기본) (중복된 파일 이름 재지정)
* 기존 request 객체가 아닌 MultipartRequest 객체에서 모든 사용자 입력 값을 가져와야 함
② 저장된 파일정보 attachment 레코드로 등록
Controller
BoardEnrollServlet
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
try {
// 0. 첨부파일처리
ServletContext application = getServletContext();
String saveDirectory = application.getRealPath("/upload/board");
System.out.println("saveDirectory = " + saveDirectory);
int maxPostSize = 1024 * 1024 * 10; // 10MB
String encoding = "utf-8";
FileRenamePolicy policy = new DefaultFileRenamePolicy();
MultipartRequest multiReq = new MultipartRequest(request, saveDirectory, maxPostSize, encoding, policy);
// 저장된 파일
String originalFilename = multiReq.getOriginalFileName("upFile1");
String renamedFilename = multiReq.getFilesystemName("upFile1");
System.out.println("originalFilename = " + originalFilename);
System.out.println("renamedFilename = " + renamedFilename);
if(true) return;
// 1. 사용자 입력 값 처리
String title = request.getParameter("title");
String writer = request.getParameter("writer");
String content = request.getParameter("content");
Board board = new Board(0, title, writer, content, 0, null);
// 2. 업무로직
int result = boardService.insertBoard(board);
// 3. redirect 처리
request.getSession().setAttribute("msg", "게시글을 성공적으로 등록하였습니다.");
response.sendRedirect(request.getContextPath() + "/board/boardList");
} catch(Exception e) {
e.printStackTrace();
throw e;
}
}
@콘솔출력값
saveDirectory = C:\Workspaces\web_server_workspace\hello-mvc2\src\main\webapp\upload\board
originalFilename = hyunta.jpg
renamedFilename = hyunta.jpg
파일이 잘 저장되었는 지 확인하기 위해 if문을 추가하였으며, ServletContext 객체를 통해 '/upload/board' 파일의 경로를 가져왔습니다.
사용자가 등록한 'upFile1'의 실제 파일명은 multiReq의 getOriginalFileName()으로 가져왔으며, 저장된 파일명은 multiReq의 getFilesystemName()으로 가져왔습니다.
동일한 파일명 업로드
@콘솔출력값
saveDirectory = C:\Workspaces\web_server_workspace\hello-mvc2\src\main\webapp\upload\board
originalFilename = hyunta.jpg
renamedFilename = hyunta1.jpg
renamedFilename이 hyunta1.jpg로 지정된 것을 확인할 수 있습니다.
이름을 재지정하지 않으면 파일이 덮어쓰여지기 때문이죠.
DefaultFileRenamePolicy 객체 살펴보기 - 컨트롤 클릭
DefaultFileRenamePolicy
- MutipartRequest 객체가 생성될 때 내부적으로 호출되는 rename() 메소드를 가지고 있음
// Copyright (C) 2002-2022 by Jason Hunter <jhunter_AT_servlets_DOT_com>.
// All rights reserved. Use of this class is limited.
// Please see the LICENSE for more information.
package com.oreilly.servlet.multipart;
import java.io.*;
/**
* Implements a renaming policy that adds increasing integers to the body of
* any file that collides. For example, if foo.gif is being uploaded and a
* file by the same name already exists, this logic will rename the upload
* foo1.gif. A second upload by the same name would be foo2.gif.
* Note that for safety the rename() method creates a zero-length file with
* the chosen name to act as a marker that the name is taken even before the
* upload starts writing the bytes.
*/
public class DefaultFileRenamePolicy implements FileRenamePolicy {
// This method does not need to be synchronized because createNewFile()
// is atomic and used here to mark when a file name is chosen
public File rename(File f) {
if (createNewFile(f)) {
return f;
}
String name = f.getName();
String body = null;
String ext = null;
int dot = name.lastIndexOf(".");
if (dot != -1) {
body = name.substring(0, dot);
ext = name.substring(dot); // includes "."
}
else {
body = name;
ext = "";
}
// Increase the count until an empty spot is found.
// Max out at 9999 to avoid an infinite loop caused by a persistent
// IOException, like when the destination dir becomes non-writable.
// We don't pass the exception up because our job is just to rename,
// and the caller will hit any IOException in normal processing.
int count = 0;
while (!createNewFile(f) && count < 9999) {
count++;
String newName = body + count + ext;
f = new File(f.getParent(), newName);
}
return f;
}
private boolean createNewFile(File f) {
try {
return f.createNewFile();
}
catch (IOException ignored) {
return false;
}
}
}
※ 처리 과정
처음 if문에서 createNewFile() 메소드를 실행 시키고, 기존 파일명들 중 넘어온 파일명에 정보가 없다면 true를 반환시켜 파일객체를 반환합니다.
(createNewFile() 메소드 내에 f.createNewFile()은 File의 createNewFile())
하지만, 동일한 파일명이 존재한다면 false가 되므로 이후 코드를 실행하게 되는데, 파일명과 파일확장자를 분리시키는 작업을 수행하고 파일명 + count++한 값 + 확장자의 결과값을 새로운 파일명으로 지정해줍니다.
이 때도 while()문의 조건식에 createNewFile()를 호출하여 파일명이 있는지를 체크해주는 과정을 거칩니다.
※ java.io.File
- File 객체는 실제 파일을 바라보는 객체 (파일은 존재할 수도 있고, 없을 수도!)
- 실제 파일이 존재하지 않다면, 파일을 생성해줄 수도 있음
- 존재 여부 판단, 파일 이동, 디렉토리 관리 등을 처리할 수 있음
파일명 변환 형식 지정하기
- abc.txt → 20220702_183030333_123.txt
Controller
BoardEnrollServlet
// 0. 첨부파일처리
ServletContext application = getServletContext();
String saveDirectory = application.getRealPath("/upload/board");
System.out.println("saveDirectory = " + saveDirectory);
int maxPostSize = 1024 * 1024 * 10; // 10MB
String encoding = "utf-8";
FileRenamePolicy policy = new HelloMvcFileRenamePolicy();
MultipartRequest multiReq = new MultipartRequest(request, saveDirectory, maxPostSize, encoding, policy);
// 저장된 파일
String originalFilename = multiReq.getOriginalFileName("upFile1");
String renamedFilename = multiReq.getFilesystemName("upFile1");
System.out.println("originalFilename = " + originalFilename);
System.out.println("renamedFilename = " + renamedFilename);
HelloMvcFileRenamePolicy
public class HelloMvcFileRenamePolicy implements FileRenamePolicy {
@Override
public File rename(File oldFile) {
File newFile = null;
do {
// 새 파일명 형식 지정
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd_HHmmssSSS_");
DecimalFormat df = new DecimalFormat("000");
// 확장자 추출
String oldName = oldFile.getName();
String ext = "";
int dot = oldName.lastIndexOf(".");
if(dot > -1) {
ext = oldName.substring(dot); // .txt
}
// 새 파일명 생성
String newName = sdf.format(new Date()) + df.format(Math.random() * 1000) + ext;
// File객체 새로 생성
System.out.println("oldFile.getParent() = " + oldFile.getParent());
newFile = new File(oldFile.getParent(), newName);
} while(!createNewFile(newFile));
return newFile;
}
/**
* {@link File#createNewFile()} - 실제 파일이 존재하지 않는 경우, 파일 생성 후 true 리턴 - 실제 파일이
* 존재하는 경우, IOException 발생!
*
*/
private boolean createNewFile(File f) {
try {
return f.createNewFile();
} catch (IOException ignored) {
return false;
}
}
}
@콘솔출력값
oldFile.getParent() = C:\Workspaces\web_server_workspace\hello-mvc2\src\main\webapp\upload\board
originalFilename = hyunta.jpg
renamedFilename = 20220703_154913779_960.jpg
rename 메소드를 오버라이딩하여 위와 같이 커스텀 파일명을 생성하였습니다.
(oldFile.getParent()를 이용하여 저장 위치를 알아냄!)
DB등록
- board 테이블과 attachment 테이블 모두 처리 되어야함 (트랜잭션 처리)
- 하나의 Connection 아래에서 처리되어야 함
Dto
BoardExt
public class BoardExt extends Board {
private int attachCount;
private List<Attachment> attachments = new ArrayList<>();
public BoardExt() {
super();
// TODO Auto-generated constructor stub
}
public BoardExt(int no, String title, String writer, String content, int readCount, Timestamp regDate) {
super(no, title, writer, content, readCount, regDate);
}
public int getAttachCount() {
return attachCount;
}
public void setAttachCount(int attachCount) {
this.attachCount = attachCount;
}
public List<Attachment> getAttachments() {
return attachments;
}
public void setAttachments(List<Attachment> attachments) {
this.attachments = attachments;
}
public void addAttachment(Attachment attach) {
this.attachments.add(attach);
}
@Override
public String toString() {
return "BoardExt [attachCount=" + attachCount + ", attachments=" + attachments + ", toString()="
+ super.toString() + "]";
}
}
Attachment 객체를 담을 List<Attachment> attachments 필드를 추가하였으며, 그에 대한 getter/setter도 추가하였습니다.
또한, Attachment 리스트에 추가하는 addAttachment() 메소드를 생성하였습니다.
Controller
BoardEnrollServlet
@WebServlet("/board/boardEnroll")
public class BoardEnrollServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private BoardService boardService = new BoardService();
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.getRequestDispatcher("/WEB-INF/views/board/boardEnroll.jsp").forward(request, response);
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
try {
// 0. 첨부파일처리
ServletContext application = getServletContext();
String saveDirectory = application.getRealPath("/upload/board");
System.out.println("saveDirectory = " + saveDirectory);
int maxPostSize = 1024 * 1024 * 10; // 10MB
String encoding = "utf-8";
FileRenamePolicy policy = new HelloMvcFileRenamePolicy();
MultipartRequest multiReq = new MultipartRequest(request, saveDirectory, maxPostSize, encoding, policy);
// 1. 사용자 입력 값 처리
// MultipartRequest를 사용하면 기존 request 객체 사용불가!!!!
String title = multiReq.getParameter("title");
String writer = multiReq.getParameter("writer");
String content = multiReq.getParameter("content");
BoardExt board = new BoardExt(0, title, writer, content, 0, null);
System.out.println("board = " + board);
Enumeration<String> filenames = multiReq.getFileNames(); // 파일이름을 모두 가져옴 (파일이 없어도 가져옴)
while(filenames.hasMoreElements()) {
String filename = filenames.nextElement();
File upFile = multiReq.getFile(filename); // 파일이름을 통해 실제 파일이 있는 파일객체를 리턴!
if(upFile != null) {
Attachment attach = new Attachment();
attach.setOriginalFilename(multiReq.getOriginalFileName(filename)); // 해당 파일이름을 가진 파일의 실제 파일명 set
attach.setRenamedFilename(multiReq.getFilesystemName(filename)); // 해당 파일이름을 가진 파일의 변경된 파일명 set
board.addAttachment(attach);
}
}
// 2. 업무로직
int result = boardService.insertBoard(board);
// 3. redirect 처리
request.getSession().setAttribute("msg", "게시글을 성공적으로 등록하였습니다.");
response.sendRedirect(request.getContextPath() + "/board/boardList");
} catch(Exception e) {
e.printStackTrace();
throw e;
}
}
}
MultipartRequest 객체를 사용하면 기존 request 객체 사용 불가하므로 주의!
※ 처리 과정
multiReq의 getFileName() 메소드를 이용하여 파일이름을 모두 가져왔으며, 파일이 존재하지 않아도 가져옵니다.
multiReq.getFile(filename)을 통해 실제 파일이 존재하는지 여부를 체크 해주었습니다.
존재한다면 Attachment 객체를 생성하고, OriginalFilename과 RenamedFilename을 set 해준 후 생성해두었던 BoardExt의 addAttachment() 메소드를 이용하여 추가해주었습니다.
Service
BoardService
public int insertBoard(Board board) {
Connection conn = getConnection();
int result = 0;
try {
// board 테이블 insert
result = boardDao.insertBoard(conn, board);
// 방금 등록된 board.no 컬럼값 조회
// attachment의 fk 컬럼인 board_no을 넘어온 board 객체에서 가져올 수 없으므로 조회! (boardNo은 insert된 후 생성됨)
int boardNo = boardDao.getLastBoardNo(conn);
// attachment 테이블 insert
List<Attachment> attachments = ((BoardExt)board).getAttachments();
if(attachments != null && !attachments.isEmpty()) {
for(Attachment attach : attachments) {
attach.setBoardNo(boardNo); // 조회한 boardNo를 set!!
result = boardDao.insertAttachment(conn, attach);
}
}
commit(conn);
} catch(Exception e) {
rollback(conn);
throw e;
} finally {
close(conn);
}
return result;
}
※ 처리 과정
트랜잭션 처리가 되어야하므로 하나의 Connection 안에서 처리를 하였습니다.
attachment의 중요한 fk컬럼인 board_no를 알아야하기 때문에 현재 발급된 sequence를 조회하는 .currval를 이용하여 board_no를 가져와주었습니다.
넘어온 board는 Board 객체로 받아왔기 때문에 다운캐스팅을 통해 Attachment 리스트를 가져왔으며, 첨부파일이 존재한다면 가져왔던 board_no를 set해준 후 attachment 테이블에 추가해주도록 하였습니다.
하나의 Connection에서 처리하였기 때문에 두 테이블 중 하나라도 오류가 발생하면, 같이 rollback되고 문제가 없다면 동시에 commit처리가 됩니다.
Dao
BoardDao
// getLastBoardNo = select seq_board_no.currval from dual
public int getLastBoardNo(Connection conn) {
PreparedStatement pstmt = null;
ResultSet rset = null;
int boardNo = 0;
String sql = prop.getProperty("getLastBoardNo");
try {
pstmt = conn.prepareStatement(sql);
rset = pstmt.executeQuery();
if(rset.next()) {
boardNo = rset.getInt(1);
}
} catch (SQLException e) {
throw new BoardException("게시물 번호 조회 오류", e);
} finally {
close(rset);
close(pstmt);
}
return boardNo;
}
// insertAttachment = insert into attachment values (seq_attachment_no.nextval, ?, ?, ?, default)
public int insertAttachment(Connection conn, Attachment attach) {
PreparedStatement pstmt = null;
int result = 0;
String sql = prop.getProperty("insertAttachment");
try {
pstmt = conn.prepareStatement(sql);
pstmt.setInt(1, attach.getBoardNo());
pstmt.setString(2, attach.getOriginalFilename());
pstmt.setString(3, attach.getRenamedFilename());
result = pstmt.executeUpdate();
} catch (SQLException e) {
throw new BoardException("첨부파일 등록 오류!", e);
} finally {
close(pstmt);
}
return result;
}
서버와 DB에도 모두 잘 등록이 된 것을 확인할 수 있습니다.
'Java > Servlet & JSP' 카테고리의 다른 글
JSP) XSS 공격 대비 - escape 처리 (0) | 2022.07.03 |
---|---|
JSP) 게시글 상세보기 - 게시글 수정/삭제 권한에 따라 버튼 노출하기 (0) | 2022.07.03 |
JSP) 첨부파일 있는 게시글 표시 (0) | 2022.07.02 |
JSP) 게시판 페이지 구현 및 페이징 처리 (0) | 2022.07.02 |
JSP) 관리자 권한 변경 - Referer, dataset (0) | 2022.07.02 |