파일을 첨부하기 위해서는 글 쓰기 폼(boardForm.jsp)에 file 태그만 추가해 주면 된다.
다음 코드에서 빨갛게 표시된 것처럼 uploadfile파일이라고 이름을 주고 생성하였다.
Multiple 속성은 한 번에 여러 개의 파일을 지정할 수 있다.
<input type="file" name="uploadfile" multiple="" />
이러한 첨부 파일을 전송하기 위해 폼 태그도 인코딩 타입(enctype)을 multipart/form-data로 지정해 줘야 한다.
이것은 파일이나 용량이 큰 데이터를 전송할 때 지정하는 방식으로 기억해두면 될 것 같다.
<form name="form1" action="board4Save" method="post" enctype="multipart/form-data">
<table border="1" style="width:600px">
~~ 생략 ~~
<tr>
<td>내용</td>
<td><textarea name="brdmemo" rows="5" cols="60"><c:out value="${boardInfo.brdmemo}"/></textarea></td>
</tr>
<tr>
<td>첨부</td>
<td>
<input type="file" name="uploadfile" multiple="" />
boardForm.jsp
사용자가 파일을 첨부하여 전송한 내용을 서버에서 받기 위해 스프링에서 제공하는 MultipartFile를 다음 코드와 같이 boardVO에 추가해 줘야 한다.
다수의 파일을 전송하기 때문에 List 형태로 지정해야 하고 변수 이름(uploadfile)은 파일 태그에서 지정한 이름과 같아야 한다.
public class boardVO {
private String brdno, brdtitle, brdwriter, brdmemo, brddate, brdhit, brddeleteflag;
private List<MultipartFile> uploadfile;
~~ 생략 ~~
public List<MultipartFile> getUploadfile() {
return uploadfile;
}
public void setUploadfile(List<MultipartFile> uploadfile) {
this.uploadfile = uploadfile;
}
boardVO.java
파일을 저장하기 위해서 관련 내용을 boardVO로 받아온 뒤에 처리를 하게 된다.
먼저, MultipartFile로 받아온 파일들을 서버의 지정된 디렉터리에 저장해야 한다.
본 예제에서는 FileUtil라는 클래스로 이 기능을 미리 개발해 두었다.
복잡하지 않기 때문에 원리를 이해하면 좋겠지만 여기에서는 saveAllFiles만 호출하면 된다는 것을 기억해 두자.
다만 FileUtil 안에 저장 디렉터리로 "d:\\workspace\\fileupload\\"가 지정되어 있으니 각자의 디렉터리 구조에 맞추어 수정해줘야 하다.
@RequestMapping(value = "/board4Save")
public String boardSave(HttpServletRequest request, boardVO boardInfo) throws Exception {
FileUtil fs = new FileUtil();
List<FileVO> filelist = fs.saveAllFiles(boardInfo.getUploadfile());
boardSvc.insertBoard(boardInfo, filelist);
return "redirect:/board4List";
}
board4Ctr.java
주의: 파일명은 날짜와 시간(millisecond)을 이용하여 중복되지 않게 부여하고, 각 파일은 해당 년도 디렉터리에 저장하게 된다.
디렉터리는 파일명의 앞 4자리(년도)를 잘라서 자동 생성되고 그 디렉터리에 파일이 저장된다.
하나의 디렉터리에 저장될 수 있는 파일의 개수는 한정적(약 만개로 기억)이라 년도 별로 저장하게 개발하였다.
만약, 1년간 파일의 개수가 제한된 개수를 넘는다면 년월(앞6자리)로 수정해 주면 된다.
첨부된 파일이 실제 디렉터리에 저장되면 FileVO 형의 List가 반환된다.
FileVO는 파일 첨부 기능이 있는 모든 페이지에서 비슷하게 사용하기 때문에 공통(common)으로 제작하였다.
클래스 구성은 테이블(TBL_BOARDFILE) 구조와 유사하지만
테이블의 글번호(brdno) 대신에 부모글 번호(parentPK)로 명명하여 공통으로 사용할 수 있게 구성하였다.
size2String 함수는 비트로 저장된 파일 크기(filesize)를 byte, K-byte, M-Byte 등으로
적절하게 변환하여 반환하는 함수로 공통 함수로 구성하여 사용해도 되지만
FileVO 외에서는 사용할 일이 없어서 FileVO에 넣어서 제작하였다.
사용된 공식은 몇 년 전 인터넷에서 찾은 것으로 간단해서 익혀두면 조금이나마 알고리즘 연습이 될 수 있을 것이다.
package gu.common;
public class FileVO {
private Integer fileno; // 글번호
private String parentPK; // 부모 글번호
private String filename; // 파일명
private String realname; // 실제파일명
private long filesize; // 파일 크기
public String size2String() {
Integer unit = 1024;
if (filesize < unit){
return String.format("(%d B)", filesize);
}
int exp = (int) (Math.log(filesize) / Math.log(unit));
return String.format("(%.0f %s)", filesize / Math.pow(unit, exp), "KMGTPE".charAt(exp-1));
}
public Integer getFileno() {
return fileno;
}
public void setFileno(Integer fileno) {
this.fileno = fileno;
}
~~ 생략 ~~
FileVO.java
원리는 로그(log)를 이용한 것으로 밑수가 10인 로그(상용로그)를 실행하면 10의 배수가 계산된다.
byte, K-byte, M-Byte 등은 1024배씩 커지는 것으로 약 10의 3배수씩 커진다고 보면 된다.
즉, 1 K byte = 1024 byte 이고 1024 byte 는 Log를 취하면 약 3(3.010)의 값이 나온다.
이 3은 10의 자릿수로 보면 된다 (1000으로 보면 0의 개수).
예로, 파일 크기(filesize)가 2248 byte일 경우 로그를 취하면 약 3(3.351)이 나오고
단위 (1024의 로그값 3)로 나누면 1(1.11)이 계산 된다.
즉, 단위를 나타내는 문자열("KMGTPE")에서 첫 번째 K가 선택되게 된다.
단위를 구했으면 단위에 맞게끔 파일 크기를 계산해 줘야 한다.
단위값(1024)을 거듭제곱(power)으로 1을 계산하면 1024가 나오고 이 값을 실제 파일 크기 2248에 대하여 나누면 2.19가 계산된다.
따라서 최종적으로 2.2 K byte로 표기되는 것이다.
이렇게 반환된 파일 리스트(filelist)는 게시물 내용과 같이 서비스(insertBoard)로 전달하여 데이터 베이스에 저장한다.
public void insertBoard(boardVO param, List<FileVO> filelist) throws Exception {
if (param.getBrdno()==null || "".equals(param.getBrdno()))
sqlSession.insert("insertBoard4", param);
else sqlSession.update("updateBoard4", param);
for (FileVO f : filelist) {
f.setParentPK(param.getBrdno());
sqlSession.insert("insertBoard4File", f);
}
}
board4Svc.java
서비스에서는 파일 리스트 개수만큼 반복해서 다음과 같이 Insert문으로 첨부
테이블(TBL_BOARDFILE)에 저장하면 된다.
게시물의 글번호(brdno)를 parentPK에 넣고(setParentPK), mybatis에서는 INSERT문을 #{parentPK}로 만들어 실행하게 된다.
<insert id="insertBoard4File" parameterType="gu.common.FileVO" >
INSERT INTO TBL_BOARDFILE (BRDNO, FILENAME, REALNAME, FILESIZE)
VALUES (#{parentPK}, #{filename}, #{realname}, #{filesize})
</insert>
board4.xml
게시판 테이블(TBL_BOARD)에 저장하는 insertBoard4의 SQL문도 수정이 필요하다.
위 SQL의 insertBoard4File에 게시판의 글번호(brdno) 값이 필요하다.
글 수정일 경우 글번호가 같이 넘어 오지만
신규 글 쓰기일 경우 글번호가 부여 되지 않았다.
특히, 글 쓰기는 글번호가 Insert문이 실행되면 DBMS가 자동으로 부여하기 때문에 그 번호를 알 수 없다.
따라서, 자동으로 부여하지 않고 계산해서 가져와야 한다.
자동으로 부여 하는 방법은 현재 테이블에 저장된 번호 중 최대값(MAX)을 구하고, 그 값에 1을 더하면 된다.
SELECT IFNULL(MAX(BRDNO),0)+1 FROM TBL_BOARD
IFNULL은 null일 때 0값을 주는 것으로 MAX 함수가 null 일 경우는 데이터가 하나도 없는 경우를 의미한다.
즉, 첫 데이터를 입력하기 전이다.
이렇게 반환 된 값으로 게시판 글번호로 저장하고 첨부 파일에도 저장하면 된다.
따라서 insertBoard4 서비스에 위 SELECT문을 실행할 수 있도록 하면 되지만,
Mybatis의 selectKey를 이용하면 쉽게 해결 할 수 있다.
다음 코드와 같이 selectKey의 keyProperty에 글번호(brdno)를 지정하면 신규 글번호 값이 boardVO 클래스의 brdno에 넣어져 반환 된다.
이 값이 INSERT문에서 #{brdno}로 사용해주면 새로운 게시물이 잘 저장된다.
그리고, 파라메타로 넘어온 boardVO 클래스에 저장되어 반환되기 때문에,
f.setParentPK(param.getBrdno())로 글번호를 받아와서 사용할 수 있게 된다.
<insert id="insertBoard4" parameterType="gu.board4.boardVO" >
<selectKey resultType="String" keyProperty="brdno" order="BEFORE">
SELECT IFNULL(MAX(BRDNO),0)+1 FROM TBL_BOARD
</selectKey>
INSERT INTO TBL_BOARD(BRDNO, BRDTITLE, BRDWRITER, BRDMEMO, BRDDATE, BRDHIT, BRDDELETEFLAG)
VALUES (#{brdno}, #{brdtitle}, #{brdwriter}, #{brdmemo}, NOW(), 0, 'N' )
</insert>
board4.xml
마지막으로 다음과 같이 트랜잭션(Transaction) 처리를 해주어야 한다.
앞서 insertBoard 서비스(board4Svc)에서 기존에 게시물 저장 후에
첨부한 파일 리스트들을 저장하는 SQL문을 실행하도록 추가했다.
즉 2개의 트랜잭션이 발생하는 경우에,
앞의 게시물을 저장하고 여러 가지 원인으로 오류가 발생 할 경우 첨부 파일 정보는 저장되지 않는다.
데이터 무결성에 문제가 발생한다.
이러한 경우 모든 데이터가 저장 안되게 처리하게 되는데
다음과 같이 try 문을 사용하여 오류가 생기면 앞서 작업한 내용을 모두 취소(rollback)하고
오류가 없으면 모두 저장(commit)하도록 작성한다.
txManager 클래스는 이미 applicationContext.xml에 선언되어 있는 것으로 그냥 트랜잭션에 사용되는 것으로 기억하고 넘어간다.
DefaultTransactionDefinition ~~클래스 등은 트랜잭션 사용시 나오는 상용 구문 정도로 기억하면 될 듯하다.
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
TransactionStatus status = txManager.getTransaction(def);
try{
if (param.getBrdno()==null || "".equals(param.getBrdno()))
sqlSession.insert("insertBoard4", param);
else sqlSession.update("updateBoard4", param);
for (FileVO f : filelist) {
f.setParentPK(param.getBrdno());
sqlSession.insert("insertBoard4File", f);
}
txManager.commit(status);
} catch (Exception ex) {
txManager.rollback(status);
throw ex;
}
board4Svc.java
본 예제는 첨부한 파일을 "d:\workspace\fileupload\"에 저장한다.
지정된 디렉터리에 대한 운영체제의 권한에 따라 권한오류(특히 리눅스)가 발생하면서 파일이 저장 되지 않으니 주의해야 한다.