JSP/Servlet_07) commons-fileupload2 라이브러리를 활용한 DB 파일 업로드 구현
2025. 10. 14. 15:46

학습 목표

  • commons-fileupload2 라이브러리의 구조와 역할 이해
  • multipart/form-data 방식의 파일 업로드 처리 과정 파악
  • 파일명 중복 방지를 위한 고유 파일명 생성 방법 학습
  • 파일 업로드와 DB 저장의 트랜잭션 처리 방법 이해
  • 실제 프로젝트에서의 파일 업로드 구현 과정 체험

<commons-fileupload2 라이브러리 개요>

- 라이브러리 구성 요소

commons-fileupload2는 파일 업로드를 처리하기 위한 Apache Commons 프로젝트의 라이브러리다. 주요 구성 요소는 다음과 같다:

// 필요한 라이브러리들
commons-fileupload2-core-2.0.0-M4.jar     // 멀티파트 요청 처리 기능
commons-fileupload2-jakarta-2.0.0-M1.jar  // Jakarta Servlet 지원
commons-io-2.20.0.jar                      // 파일 I/O 스트림 처리

- multipart/form-data 방식의 필요성

기본적인 form 전송 시 인코딩 타입은 application/x-www-form-urlencoded이다. 이 방식은 모든 데이터를 문자열로 인코딩해서 한 줄의 텍스트로 전달한다. 하지만 파일 업로드 시에는 파일의 바이너리 형태 데이터가 URL 인코딩 방식으로 변경되면서 데이터가 너무 커지고, 이 과정에서 파일이 손상될 수 있다.

<!-- 파일 업로드를 위한 form 설정 -->
<form action="${pageContext.request.contextPath}/insert.bo" 
      method="post" 
      enctype="multipart/form-data">
    <input type="file" name="upfile">
</form>

<파일 업로드 처리 과정>

- multipart 요청 검증

서버에서 multipart 요청인지 먼저 확인한다.

// multipart 요청인지 검증
if(JakartaServletFileUpload.isMultipartContent(request)) {
    // 파일 업로드 처리 로직
}

- 파일 크기 제한 설정

서버 리소스 보호를 위해 파일 크기와 전체 요청 크기를 제한한다.

// 파일 용량 제한 설정
int fileMaxSize = 1024 * 1024 * 50;    // 개별 파일 50MB
int requestMaxSize = 1024 * 1024 * 60; // 전체 요청 60MB

- 파일 저장 경로 설정

업로드된 파일을 저장할 물리적 경로를 설정한다.

// 파일 저장 경로 설정
String savePath = request.getServletContext()
    .getRealPath("/resources/board-file/");

<FileItem 객체를 통한 데이터 처리>

- DiskFileItemFactory와 JakartaServletFileUpload 설정

파일을 임시로 저장하는 객체와 HTTP 요청을 파싱하는 객체를 생성한다.

// DiskFileItemFactory: 파일을 임시로 저장하는 객체
DiskFileItemFactory factory = DiskFileItemFactory.builder().get();

// JakartaServletFileUpload: HTTP 요청 파싱 객체
JakartaServletFileUpload upload = new JakartaServletFileUpload(factory);
upload.setFileSizeMax(fileMaxSize);
upload.setSizeMax(requestMaxSize);

- 요청 데이터 파싱 및 분류

전달받은 데이터를 파싱하여 일반 폼 필드와 파일을 구분한다.

// 요청 데이터 파싱
List<FileItem> formItems = upload.parseRequest(request);

for(FileItem item : formItems) {
    if(item.isFormField()) {
        // 일반 폼 필드 처리
        switch(item.getFieldName()) {
            case "category":
                int categoryNo = Integer.parseInt(
                    item.getString(Charset.forName("UTF-8")));
                b.setCategoryNo(categoryNo);
                break;
            case "title":
                b.setBoardTitle(item.getString(Charset.forName("UTF-8")));
                break;
            case "content":
                b.setBoardContent(item.getString(Charset.forName("UTF-8")));
                break;
        }
    } else {
        // 파일 처리
        String originName = item.getName();
        // 파일 처리 로직...
    }
}

<고유 파일명 생성 및 파일 저장>

- 파일명 중복 방지 전략

동일한 파일명이 업로드될 경우 덮어쓰기를 방지하기 위해 고유한 파일명을 생성한다.

if(originName.length() > 0) {
    // 고유 파일명 생성
    String tmpName = "kh_" + System.currentTimeMillis() + 
                    ((int)(Math.random() * 100000) + 1);
    String type = originName.substring(originName.lastIndexOf("."));
    String changeName = tmpName + type;

    // 파일 저장
    File f = new File(savePath, changeName);
    item.write(f.toPath());

    // Attachment 객체 생성
    at = new Attachment();
    at.setOriginName(originName);
    at.setChangeName(changeName);
    at.setFilePath("resources/board-file/");
}

실행 결과:

원본 파일명: document.pdf
변경된 파일명: kh_17030012345678901234.pdf

<DB 저장 및 트랜잭션 처리>

- Attachment VO 클래스 구조

파일 정보를 저장하기 위한 VO 클래스를 정의한다.

@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class Attachment {
    private int fileNo;
    private int refBoardNo;
    private String originName;    // 원본 파일명
    private String changeName;    // 변경된 파일명
    private String filePath;      // 파일 경로
    private Date uploadDate;
    private int fileLevel;
    private String status;
}

- 게시글과 첨부파일 동시 저장

게시글과 첨부파일을 하나의 트랜잭션으로 처리하여 데이터 일관성을 보장한다.

// BoardService의 insertBoard 메서드
public int insertBoard(Board b, Attachment at) {
    Connection conn = getConnection();
    BoardDao bDao = new BoardDao();

    // 1. 게시글 먼저 저장
    int result = bDao.insertBoard(conn, b);

    // 2. 첨부파일이 있으면 저장
    if(at != null) {
        result *= bDao.insertAttachment(conn, at);
    }

    // 3. 트랜잭션 처리
    if(result > 0) {
        commit(conn);
    } else {
        rollback(conn);
    }

    close(conn);
    return result;
}

- SQL 매퍼 설정

게시글과 첨부파일 저장을 위한 SQL 쿼리를 정의한다.

<!-- 게시글 저장 -->
<entry key="insertBoard">
    INSERT INTO BOARD(
        BOARD_NO, BOARD_TYPE, CATEGORY_NO, 
        BOARD_TITLE, BOARD_CONTENT, BOARD_WRITER
    ) VALUES(
        SEQ_BNO.NEXTVAL, 1, ?, ?, ?, ?
    )
</entry>

<!-- 첨부파일 저장 -->
<entry key="insertAttachment">
    INSERT INTO ATTACHMENT(
        FILE_NO, REF_BNO, ORIGIN_NAME, 
        CHANGE_NAME, FILE_PATH
    ) VALUES(
        SEQ_FNO.NEXTVAL, SEQ_BNO.CURRVAL, ?, ?, ?
    )
</entry>

<에러 처리 및 롤백>

- 실패 시 파일 삭제 처리

DB 저장이 실패할 경우 이미 업로드된 파일을 삭제하여 불필요한 파일이 남지 않도록 한다.

int result = new BoardService().insertBoard(b, at);

if(result > 0) {
    // 성공 시
    session.setAttribute("alertMsg", "일반게시글 작성 성공");
    response.sendRedirect(request.getContextPath() + "/list.bo");
} else {
    // 실패 시 업로드된 파일 삭제
    if(at != null){
        new File(savePath + at.getChangeName()).delete();
    }

    request.setAttribute("errorMsg", "일반게시글 작성 실패");
    request.getRequestDispatcher("views/common/error.jsp")
        .forward(request, response);
}