본문 바로가기
Java/Servlet & JSP

JSP) 첨부파일이 포함된 게시글 등록 - 첨부파일 처리

by 박채니 2022. 7. 3.

안녕하세요, 코린이의 코딩 학습기 채니 입니다.

 

개인 포스팅용으로 내용에 오류 및 잘못된 정보가 있을 수 있습니다.


파일을 업로드/다운로드는 아래와 같이 입출력의 연속입니다.

사용자가 첨부파일을 보낼 때 request msg에 전송을 해주고 (브라우저가 처리), 서버 쪽에서는 이를 읽어다가 서버 컴퓨터에 저장합니다. (외부라이브러리 - cos.jar 이용 예정!)

※ 참고포스팅

https://lena-chamna.netlify.app/post/http_multipart_form-data/

 

HTTP multipart/form-data 이해하기

Understanding about HTTP multipart/form-data

lena-chamna.netlify.app

 

 

cos.jar download

http://www.servlets.com/cos/

 

Servlets.com | com.oreilly.servlet

 

www.servlets.com

다운로드 완료되었다면, 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

'upFIle1'의 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 객체 살펴보기 - 컨트롤 클릭

코드가 안 뜬다면, Attach Source - External location - 다운한 cos.zip

 

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에도 모두 잘 등록이 된 것을 확인할 수 있습니다.