본문으로 바로가기


게시글에 파일을 업로드 할 수 있게 하려고 한다. 단일 파일 업로드 방식도 있고 다중 파일 업로드 방식도 있다. 여기선 다중 파일 업로드 방식을 포스팅한다. 단일 파일 업로드와 다중 파일 업로드는 차이가 없다고 생각한다. 왜냐하면 첨부파일 업로드를 공부할 때 단일 파일 업로드를 만들고 배열을 이용해서 다중 파일 업로드로 확장시켰기 때문이다. 따라서 단일파일 업로드를 원하면 배열인 부분들만 없애주면 된다.


1. DB 생성

첨부 파일의 정보를 저장하는 테이블을 생성한다.

1
2
3
4
5
6
7
8
9
10
11
12
CREATE TABLE `tb_file` (
  `IDX` int(11NOT NULL AUTO_INCREMENT,
  `BOARD_IDX` int(11NOT NULL,
  `ORG_FILE_NAME` varchar(260NOT NULL,
  `SAVE_FILE_NAME` varchar(36NOT NULL,
  `FILE_SIZE` int(11DEFAULT NULL,
  `UPDATE_DATE` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `CREA_ID` varchar(30NOT NULL,
  `DEL_CHK` varchar(1NOT NULL DEFAULT 'N',
  `CREA_DATE` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`IDX`)
ENGINE=InnoDB DEFAULT CHARSET=utf8
cs



2. 라이브러리 추가

pom.xml

1
2
3
4
5
6
<!-- commons-fileupload -->
<dependency>
    <groupId>commons-fileupload</groupId>
    <artifactId>commons-fileupload</artifactId>
    <version>1.3.2</version>
</dependency>
cs

파일 업로드를 위한 라이브러리를 추가한다.



3. context 설정

spring 설정 폴더에 context-*.xml 파일을 생성한다. file을 위한 설정만 넣어둘거기 때문에 새로 만들었다.


context-file.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    
    <!-- MultipartResolver 설정 -->
    <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
        <property name="maxUploadSize" value="100000000" />
        <property name="maxInMemorySize" value="100000000" />
    </bean>
    
    <!-- 파일 업로드 디렉토리 설정 -->
    <bean id="uploadPath" class="java.lang.String">
        <constructor-arg value="C:\\_dev\\file\\"/>
    </bean>
    
</beans>
cs


파일 업로드를 처리하기 위해서 CommonsMultipartResolver를 등록해야 한다. property 설정을 통해 파일의 최대 크기를 제한할 수 있다. 단위는 byte로, 10MB로 제한하였다. 참고로 한글명이 깨지지 않기 위해서는 UTF-8 코딩이 되어 있어야 한다. (프로젝트를 설정할 때 필터 설정을 했을 것이다.)


파일 업로드의 디렉토리를 설정한다. 즉, 업로드되는 파일들을 저장할 경로를 설정하는 것이다. 원하는 경로를 넣으면 된다. 경로는 //가 아닌 \\로 설정해야 한다. 해당 경로에 폴더가 굳이 없어도 된다. 나중에 업로드 할 파일들을 조작하는 클래스를 생성할 때 폴더가 없으면 새로 생성하는 로직도 넣을 것이다.



4. 파일 업로드 JSP 생성

파일 추가 버튼을 눌러 파일 업로드를 할 수 있는 input 태그가 생기는 자바스크립트를 추가할 것이다. (자바스크립트는 미숙하기 때문에 완벽하지 않을 수도 있다.) 스크립트를 쓰고 싶지 않다면 등록될 파일 수를 정해서 input 태그를 정한 만큼만 보여주는 방법도 있다. jsp 부분은 많은 방법이 있으니 원하는 방법을 사용하면 된다.


boardWrite.jsp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<form action='<c:url value='/board/boardInsert'/>' method="post" enctype="multipart/form-data">
    <!-- 생략 -->
    <div class="form-group" id="file-list">
        <a href="#this" onclick="addFile()">파일추가</a>
        <div class="file-group">
            <input type="file" name="file"><a href='#this' name='file-delete'>삭제</a>
        </div>
    </div>
    <button type="submit" class="btn btn-default">작성하기</button>
</form>
 
<script type="text/javascript">
    $(document).ready(function() {
        $("a[name='file-delete']").on("click"function(e) {
            e.preventDefault();
            deleteFile($(this));
        });
    })
 
    function addFile() {
        var str = "<div class='file-group'><input type='file' name='file'><a href='#this' name='file-delete'>삭제</a></div>";
        $("#file-list").append(str);
        $("a[name='file-delete']").on("click"function(e) {
            e.preventDefault();
            deleteFile($(this));
        });
    }
 
    function deleteFile(obj) {
        obj.parent().remove();
    }
</script>
cs

- 01행 : enctype="multipart/form-data" 부분이 반드시 있어야 파일들을 가져올 수 있다.

- 04, 20-27행 : 추가 버튼을 누르면 onclick을 이용해 addFile() 함수를 실행하여 input 태그를 생성해준다.

- 06, 29-31행 : 삭제 버튼을 눌러 해당 input 태그를 없애준다.

파일추가 버튼을 누르면 파일 선택 폼이 하나씩 생기고 삭제를 누르면 해당 폼이 없어지는 형태로 구성하였다. 



5. 파일 데이터가 넘어오는 지 확인

파일 데이터들이 구분되서 넘어오는 지 확인을 해보자. 어떻게 넘어오는 지 파악해야 추가, 삭제를 손쉽게 할 수 있기 때문이다.


UploadController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    @RequestMapping("/board/boardInsert")
    public ModelAndView boardInsert(CommandMap commandMap, MultipartFile[] file) throws Exception {
        ModelAndView mav = new ModelAndView("redirect:/board/boardList");
        //boardService.insertBoard(commandMap);
        for(int i=0; i<file.length; i++) {
            log.debug("================== file start ==================");
            log.debug("파일 이름: "+file[i].getName());
            log.debug("파일 실제 이름: "+file[i].getOriginalFilename());
            log.debug("파일 크기: "+file[i].getSize());
            log.debug("content type: "+file[i].getContentType());
            log.debug("================== file   END ==================");
        }
        return mav;
    }
cs

- 02행: MultipartFile[] file 을 추가한다. 다중 파일이 넘어오기 때문에 배열로 선언한다.

- 04행: 파일들이 잘 넘어오는 지 확인 용도이기 때문에 게시글 추가 서비스는 주석처리 했다.

- 05~12행: for문을 사용해서 넘어오는 파일들이 정보를 확인할 것이다.


실행을 해서 파일을 여러개 추가해서 글을 작성해보자. 그러면 로그에도 이렇게 업로드한 파일의 실제 이름, 크기, 타입 등을 확인할 수 있다. 다중 파일 업로드가 아닌 단일 파일 업로드를 하려고 한다면, 배열로 선언된 MultipartFile[] 부분을 MultipartFile 로 선언해주면 된다. 이제 배열로 파일들이 들어온 걸 확인 했으니 컨트롤러를 원상복귀시킨 뒤에 업로드를 처리하는 클래스를 만들어보자.



6. FileUtils 클래스 생성

업로드할 파일들을 조작하고 업로드 기능들을 처리할 클래스를 생성한다. 파일 업로드는 다른 곳에서도 사용가능한 공통적인 부분이라 생각했기에 common 패키지 아래 Utils 패키지를 생성해서 그 안에 클래스를 생성했다. 


FileUtils.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
@Component("fileUtils")
public class FileUtils {
    
    private Log log = LogFactory.getLog(this.getClass());
    
    @Resource(name="uploadPath")
    String uploadPath;
 
    public List<Map<String, Object>> parseFileInfo(Map<String, Object> map, MultipartFile[] file) throws Exception {
        
        String boardIDX = String.valueOf(map.get("idx"));
        String creaID = (String) map.get("crea_id");
        
        List<Map<String, Object>> fileList = new ArrayList<Map<String, Object>>();
 
        File target = new File(uploadPath);
        if(!target.exists()) target.mkdirs();
        
        for(int i=0; i<file.length; i++) {
 
            String orgFileName = file[i].getOriginalFilename();
            String orgFileExtension = orgFileName.substring(orgFileName.lastIndexOf("."));
            String saveFileName = UUID.randomUUID().toString().replaceAll("-"""+ orgFileExtension;
            Long saveFileSize = file[i].getSize();
            
            log.debug("================== file start ==================");
            log.debug("파일 실제 이름: "+orgFileName);
            log.debug("파일 저장 이름: "+saveFileName);
            log.debug("파일 크기: "+saveFileSize);
            log.debug("content type: "+file[i].getContentType());
            log.debug("================== file   END ==================");
 
            target = new File(uploadPath, saveFileName);
            file[i].transferTo(target);
            
            Map<String, Object> fileInfo = new HashMap<String, Object>();
 
            fileInfo.put("BOARD_IDX", boardIDX);
            fileInfo.put("ORG_FILE_NAME", orgFileName);
            fileInfo.put("SAVE_FILE_NAME", saveFileName);
            fileInfo.put("FILE_SIZE", saveFileSize);
            fileInfo.put("CREA_ID", creaID);
            fileList.add(fileInfo);
            
        }
        return fileList;
    }
}
cs

- 01행: xml의 Bean으로 fileUtils를 등록한다.

- 06, 07행: context 설정에서 설정한 디렉토리를 담고 있다.

- 11, 12행: 해당 게시글의 글 번호(idx)와 작성자(crea_id)를 담는다.

- 14행: 가공된 파일들을 담을 곳이다.

- 16, 17행: 파일을 저장할 경로가 존재하지 않으면 폴더를 생성한다.

- 22, 23행: 실제 파일의 이름을 랜덤의 이름으로 변경한다. 이 이름으로 파일이 서버에 저장될 것이다.

- 33행: 서버에 실제 파일을 저장한다. (임시디렉토리에 업로드)

- 34행: 임시 디렉토리에 업로드된 파일 데이터를 지정한 폴더에 저장한다.

- 36~43행: 파일의 정보를 각각의 이름으로 fileInfo에 담은 뒤, fileList에 담아준다.

- 46행: 파일들의 정보가 담긴 fileList를 반환한다.



7. 파일 등록 DAO, SQL 추가

파일 등록과 게시글 등록의 테이블은 각자 다르기 때문에 파일 등록의 DAO, SQL을 추가해준다.


boardDAO.java

1
2
3
public void insertFile(Map<String, Object> map) {
    insert("board.insertFile", map);
}
cs


baord_sql.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<insert id="insertFile" parameterType="hashmap">
    <![CDATA[
        INSERT INTO tb_file
        (
            BOARD_IDX,
            ORG_FILE_NAME,
            SAVE_FILE_NAME,
            FILE_SIZE,
            CREA_ID,
            CREA_DATE,
            UPDATE_DATE
        )
        VALUES
        (
            #{ BOARD_IDX},
            #{ ORG_FILE_NAME},
            #{ SAVE_FILE_NAME},
            #{ FILE_SIZE},
            #{ CREA_ID},
            SYSDATE(),
            SYSDATE()
        )
    ]]>
</insert>
cs

FileUtils 클래스에서 38~42행의 name을 사용해준다.



8. 게시글 등록페이지 수정

글을 작성할 때 제목, 내용, 작성자 등은 CommandMap에 담아 오고, 파일들은 MultipartFile에 담아 온다. 그리고 게시글을 등록한 후에 게시글의 idx를 포함해서 파일을 등록 해준다.


Controller 영역

1
2
3
4
5
6
    @RequestMapping("/board/boardInsert")
    public ModelAndView boardInsert(CommandMap commandMap, MultipartFile[] file) throws Exception {
        ModelAndView mav = new ModelAndView("redirect:/board/boardList");
        boardService.insertBoard(commandMap, file);
        return mav;
    }
cs

- 02행: MutlipartFile 을 배열형태로 가져온다.

- 04행: 게시글의 정보와 파일의 정보를 같이 넘겨준다.


Service 영역

1
2
3
4
5
6
7
8
9
10
11
12
// BoardService 영역
void insertBoard(CommandMap commandMap, MultipartFile[] file) throws Exception;
 
// BoardServiceImpl 영역
@Override
public void insertBoard(CommandMap commandMap, MultipartFile[] file) throws Exception{
    boardDAO.insertBoard(commandMap);
    List<Map<String, Object>> fileList = fileUtils.parseFileInfo(commandMap.getMap(), file);
    for(int i=0; i<fileList.size(); i++) {
        boardDAO.insertFile(fileList.get(i));
    }
}
cs

- 02, 06행: 마찬가지로 MutltipartFile을 배열형태로 가져온다.

- 08~11행: fileUtils 클래스에서 가공된 정보들을 하나씩 sql처리 해서 등록한다.


board_sql.xml

1
2
3
4
5
<insert id="insertBoard" parameterType="hashmap" useGeneratedKeys="true" keyProperty="idx">
    <![CDATA[
        ...
    ]]>
</insert>
cs

게시글의 내용을 등록하는 부분인데, 여기에 useGeneratedKeys 와 keyProperty가 추가되었다. 파일을 등록할때 해당 게시글의 번호(idx)가 필요하기 때문에 이 두가지를 추가해준다.



9. 상세페이지 수정

상세 페이지에 들어왔을 때 등록된 파일들을 보여줘야 한다. 파일 이름, 파일 크기 정도를 보여주려고 한다.


BoardDAO.java

1
2
3
4
    @SuppressWarnings("unchecked")
    public List<Map<String, Object>> detailFile(Map<String, Object> map) {
        return selectList("board.detailFile", map);
    }
cs


board_sql.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    <select id="detailFile" parameterType="hashmap" resultType="hashmap">
        <![CDATA[
            SELECT
                IDX,
                ORG_FILE_NAME,
                ROUND(FILE_SIZE/1024,1) AS FILE_SIZE
            FROM
                tb_file
            WHERE
                BOARD_IDX = #{ idx}
                AND
                DEL_CHK = 'N'
        ]]>
    </select>
cs

- 06행: 파일 사이즈를 kb로 표시하기 위해 계산이 들어갔다.


boardServiceImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
13
    @Override
    public Map<String, Object> viewBoardDetail(Map<String, Object> map) {
        // TODO Auto-generated method stub
        boardDAO.updateHitBoard(map);
        Map<String, Object> detail = boardDAO.detailBoard(map);
        List<Map<String, Object>> fileDetail = boardDAO.detailFile(map);
 
        Map<String, Object> resultBoard = new HashMap<String, Object>();
        resultBoard.put("detail", detail);
        resultBoard.put("file", fileDetail);
 
        return resultBoard;
    }
cs

- 06행: 파일의 정보를 가져온다.

- 08~12행: 게시글이 정보와 파일의 정보를 한 곳에 담아서 내보낸다.


boardController.java

1
2
3
4
5
6
7
8
9
10
11
12
    @RequestMapping("/board/boardDetail")
    public ModelAndView boardDetail(CommandMap commandMap, Criteria cri) throws Exception {
        
        ModelAndView mv = new ModelAndView("/board/boardDetail");
        Map<String, Object> resultBoard = boardService.viewBoardDetail(commandMap.getMap());
        mv.addObject("detail",resultBoard.get("board"));
        mv.addObject("file",resultBoard.get("file"));
        
        /* 생략 */
        
        return mv;
    }
cs

- 05행: resultBoard에 게시글과 파일정보들을 담는다.

- 06행: 게시글은 detail에 담아서 보낸다.

- 07행: 파일은 file에 담아서 보낸다.


boardDetail.jsp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
 
<c:choose>
    <c:when test="${fn:length(file) > 0 }">
    <div class="blog-file">
        <ul>
        <c:forEach items="${file }" var="file">
            <li
                <span class="file-img"></span>
                <div class="file-info">
                    <a href='#'><i class="fa fa-camera"></i> ${file.ORG_FILE_NAME }</a>
                    <span>${file.FILE_SIZE }kb</span>
                </div>
            </li>
        </c:forEach>
        </ul>
    </div>
    </c:when>
</c:choose>
cs

- 01행: fn 함수를 사용하기 위해 추가해주어야 한다.

- 04행: file이 있을 경우에만 보여주기 위해서 조건문을 넣었다.

- 07~15행: 파일을 보여주 보여주는 부분이다.



10. 실행

게시글의 내용과 파일을 2개 등록했다. 콘솔로그로 등록되는 쿼리를 확인해보자.

파일을 제외하고 게시글의 내용만 등록하는 쿼리를 볼 수 있다.

fileUtils 클래스에서 파일의 정보들을 확인할 수 있다. 그리고 파일들이 하나씩 등록되는 것도 볼 수 있다. 파일이 업로드될 디렉토리로 가보면(context-file.xml에 설정함) 업로드한 파일들이 랜덤의 이름으로 저장되어 있는 것을 확인할 수 있다. 

파일을 등록한 게시글의 상세페이지에 들어가서 등록한 파일들이 보이면 파일 업로드가 완성되었다. 이미지외에도 다른 확장자의 파일들도 저장할 수 있다. 이제 등록된 첨부파일들을 다운로드할 수 있어야 한다. 첨부파일 다운로드는 다음 포스팅에 작성하려고 한다. 얼른 올려야겠당..