[34] 파일 업로드 처리 - 프로젝트의 첨부파일 - 등록
기존에 작성해 두었던 게시판 프로젝트에 필요한 패키지와 리소스 등을 복사하여 ex02에 붙여넣기 하고 프로젝트가 잘 실행되는지 확인한다.
pom.xml, web.xml, servlet-context.xml, root-context.xml 에서 빠진게 없는지 확인한다.
1. 첨부파일 정보를 위한 준비
첨부파일이 게시물과 합쳐지면 가장 먼저 진행해야 하는 일은 게시물과 첨부파일의 관계를 저장하는 테이블의 설계가 우선이다. 게시물의 첨부파일은 각자 고유한 UUID를 가지고 있기 때문에 별도의 PK를 지정할 필요는 없지만, 게시물 등록할 때 첨부파일 테이블 역시 같이 insert 작업이 진행되어야 하므로 트랜잭션 처리가 필요하다.
첨부파일을 보관하는 테이블은 TBL_ATTACH로 설계한다. TBL_BOARD는 TBL_REPLY와 이미 외래키의 관계를 가지고 있으므로 첨부파일이 추가되면 아래와 같은 구조가 된다.
---------------------------------------
-- 파일 첨부 테이블
CREATE TABLE TBL_ATTACH (
UUID VARCHAR2(200) NOT NULL,
UPLOADPATH VARCHAR2(200) NOT NULL,
FILENAME VARCHAR2(200) NOT NULL,
-- FILETYPE CHAR(1) DEFAULT 'I',
FILETYPE CHAR(1) CHECK(FILETYPE IN(0, 1)), -- 파일타입이 이미지인지 구분하기 위해 지정한다. 0 또는 1이 참이어야지 들어가는 조건식
BNO NUMBER(10,0)
);
ALTER TABLE TBL_ATTACH ADD CONSTRAINT PK_ATTACH PRIMARY KEY (UUID);
ALTER TABLE TBL_ATTACH ADD CONSTRAINT FK_BOARD_ATTACH FOREIGN KEY(BNO) REFERENCES TBL_BOARD(BNO);
SELECT * FROM TBL_ATTACH;
첨부파일의 보관은 UUID가 포함된 이름을 PK로 하는 uuid 칼럼과 실제 파일이 업로드된 경로를 의미하는 uploadPath, 파일 이름을 의미하는 fileName, 이미지 파일 여부를 판단할 수 있는 fileType, 해당 게시물 번호를 지정하는 bno 컬럼을 이용한다.
SQL을 처리하기 위해서는 파일 정보를 처리하기 위해 파라미터를 여러 개 사용해야 하는 불편함이 있으므로, com.koreait.domain 패키지에 아예 BoardAttachVO 클래스를 설계하는 것이 유용하다. (AttachFileDTO와 거의 유사하지만 게시물의 번호가 추가되었고, 혼란을 피하기 위해서 새로 클래스를 작성한다.)
- BoardAttachVO.java 모델 객체를 생성한다.
package com.koreait.domain;
import lombok.Data;
@Data
public class BoardAttachVO {
private String uuid;
private String uploadPath;
private String fileName;
private boolean fileType;
private Long bno;
}
기존의 BoardVO는 등록 시 한 번에 BoardAttachVO를 처리할 수 있도록 List<BoardAttachVO>를 추가한다.
- BoardVO.java 수정
package com.koreait.domain;
import java.util.List;
import lombok.Data;
@Data
public class BoardVO {
// VO는 컬럼에 매핑된 것을 사용할 때
// DTO는 사용자 정의 모델된 것을 사용할 때
private Long bno;
private String title;
private String content;
private String writer;
private String regDate;
private String updateDate;
private int replyCnt;
private List<BoardAttachVO> attachList;
}
1-1. 첨부파일 처리를 위한 Mapper 처리
첨부파일 정보를 데이터베이스를 이용해서 보관하므로 이를 처리하는 SQL을 Mapper 인터페이스와 XML을 작성해서 처리한다.
- BoardAttachMapper 인터페이스 생성
package com.koreait.mapper;
import java.util.List;
import com.koreait.domain.BoardAttachVO;
public interface BoardAttachMapper {
public void insert(BoardAttachVO vo);
public void delete(String uuid);
public void deleteAll(Long bno);
public List<BoardAttachVO> findByBno(Long bno);
}
BoardAttachMapper의 경우는 첨부파일의 수정이라는 개념이 존재하지 않기 때문에, insert()와 delete() 작업만을 처리한다. 특정 게시물의 번호로 첨부파일을 찾는 작업이 필요하므로 findByBno() 메서드를 정의한다.
Mapper 인터페이스의 SQL을 처리하는 BoardAttachMapper.xml을 추가한다.
- BoardAttachMapper.xml 추가
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.koreait.mapper.BoardAttachMapper">
<insert id="insert">
INSERT INTO TBL_ATTACH (UUID, UPLOADPATH, FILENAME, FILETYPE, BNO)
VALUES (#{uuid}, #{uploadPath}, #{fileName}, #{fileType}, #{bno})
</insert>
<delete id="delete">
DELETE FROM TBL_ATTACH WHERE UUID = #{uuid}
</delete>
<select id="findByBno" resultType="com.koreait.domain.BoardAttachVO">
SELECT * FROM TBL_ATTACH WHERE BNO = #{bno}
</select>
</mapper>
2. 등록을 위한 화면 처리
첨부파일 자체의 처리는 Ajax를 통해서 이루어지므로, 게시물의 등록 시점에는 현재 서버에 업로드된 파일들에 정보를 등록하려는 게시물의 정보와 같이 전송해서 처리한다. 이 작업은 게시물의 등록 버튼을 클릭했을 때 현재 서버에 업로드 된 파일의 정보를
<input tytpe="hidden">으로 만들어서 한 번에 전송하는 방식을 사용한다.
게시물의 등록을 담당하는 /board/register.jsp 파일에서 첨부파일을 추가할 수 있도록 수정하는 작업부터 시작한다.
-register.jsp의 일부
<div class="field">
<h4>첨부파일</h4>
<input type="file" name="uploadFile" multiple />
</div>
<div class="uploadResult">
<ul></ul>
</div>
2-1. Javascript 처리
복잡한 부분을 파일을 선택하거나 'Submit Button'을 클릭했을 때의 Javascript 처리다. 가장 먼저 'Submit Button'을 클릭했을 때 첨부파일 관련된 처리를 할 수 있도록 기본 동작을 막는 작업부터 시작한다.
-register.jsp 일부 <script>
<script>
$(document).ready(function(e) {
var formObj = $("form[role='form']");
$("button[type='submit']").on("click", function(e) {
e.preventDefault(); // 로직이 수행된 후 서브밋을 하기 위해 서브밋을 막기위한 처리.
console.log("submit clicked");
});
var regex = new RegExp("(.*?)\.(exe|sh|zip|alz)$");
var maxSize = 1024 * 1024 * 5; // 5MB
function checkExtension(fileName, fileSize) {
if(fileSize >= maxSize){
alert("파일 사이즈 초과");
return false;
}
if(regex.test(fileName)){
alert("해당 종류의 파일은 업로드할 수 없습니다.");
return false;
}
return true;
}
$("input[type='file']").change(function(e){
var formData = new FormData();
var inputFile = $("input[name='uploadFile']");
var files = inputFile[0].files;
for(var i = 0; i < files.length; i++ ) {
if(!checkExtension(files[i].name, files[i].size)){
return false;
}
formData.append("uploadFile", files[i]);
}
$.ajax({
url : "/uploadAjaxAction",
processData : false,
contentType : false,
data : formData,
type : "POST",
dataType : "JSON",
success: function(result) {
console.log(result);
}
});
});
});
</script>
첨부된 파일의 처리는 기존과 동일하지만 아직은 썸네일이나 파일 아이콘을 보여주는 부분은 처리하지 않는다. 브라우저의 콘솔창을 이용해서 업로드가 정상적으로 처리되는지 만을 확인한다. 아래 화면은 첨부파일을 3개 추가하는 경우 업로드 결과를 콘솔창에서 확인한 모습이다.
컨트롤러 부분과 domain 추가 및 수정을 해야한다.
- UploadController.java
package com.koreait.controller;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.file.Files;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
import com.koreait.domain.AllFileDTO;
import com.koreait.domain.BoardAttachVO;
import lombok.extern.log4j.Log4j;
import net.coobird.thumbnailator.Thumbnailator;
@Controller
@Log4j
public class UploadController {
final String uploadFolder = "/Users/corner-macmini/upload";
// final String uploadFolder = "/Users/corner/upload";
final String fileFolder = "/Users/corner-macmini/upload/";
// final String fileFolder = "/Users/corner/upload/";
@GetMapping("/display")
@ResponseBody
public ResponseEntity<byte[]> getFile(String fileName){
log.info("fileNAme : " + fileName);
File file = new File(fileFolder+fileName);
log.info("file : " + file);
ResponseEntity<byte[]> result = null;
HttpHeaders header = new HttpHeaders();
try {
// 헤더에 적절한 파일의 타입을 probeContentType을 통하여 포함시킨다.
header.add("Content-Type", Files.probeContentType(file.toPath()));
result = new ResponseEntity<byte[]>(FileCopyUtils.copyToByteArray(file), header, HttpStatus.OK);
} catch (IOException e) {
e.printStackTrace();
}
return result;
}
@PostMapping(value="/uploadAjaxAction", produces=MediaType.APPLICATION_JSON_UTF8_VALUE)
@ResponseBody
public ResponseEntity<AllFileDTO> uploadAjaxAction(MultipartFile[] uploadFile) {
log.info("upload ajax post.....");
List<BoardAttachVO> list = new ArrayList<>();
// String uploadFolder = "/Users/corner/upload";
// 사용자가 업로드를 한 시간인 년, 월, 일을 디렉토리로 만드는 getFolder()를 사용한다.
String uploadFolderPath = getFolder();
File uploadPath = new File(uploadFolder, uploadFolderPath);
// 만약 해당 디렉토리가 존재하지 않으면
if(!uploadPath.exists()) {
// 만들어 준다. (상위 폴더들까지 전부)
uploadPath.mkdirs();
}
AllFileDTO allFile = new AllFileDTO();
List<BoardAttachVO> succeedList = new ArrayList<>();
List<BoardAttachVO> failureList = new ArrayList<>();
for(MultipartFile multipartFile : uploadFile) {
log.info("==============================");
log.info("업로드 파일 명 : " + multipartFile.getOriginalFilename());
log.info("업로드 파일 크기 : " + multipartFile.getSize());
BoardAttachVO boardAttachVO = new BoardAttachVO();
String uploadFileName = multipartFile.getOriginalFilename();
// IE에서는 파일 이름만 가져오지 않고 전체 경로를 가져오기 때문에 마지막에 위치한 파일 이름만 가져오도록 한다.
// IE 이외의 브라우저에서는 \\가 없기 때문에 -1 +1로 연산되어 0번째, 즉, 파일 이름을 의미한다.
uploadFileName.substring(uploadFileName.lastIndexOf("\\") + 1);
log.info("업로드 실제 파일명 : " + uploadFileName);
boardAttachVO.setFileName(uploadFileName);
// 랜덤한 UUID를 담아놓는다.
UUID uuid = UUID.randomUUID();
// 파일 이름이 중복되더라도 이름 앞에 UUID를 붙여주기 때문에 중복될 가능성이 희박하다.
// 덮어씌워지는 것을 방지한다.
uploadFileName = uuid.toString() + "_" + uploadFileName;
InputStream in = null;
try {
File saveFile = new File(uploadPath, uploadFileName);
// 업로드
multipartFile.transferTo(saveFile);
// 업로드 된 파일 읽어오기
in = new FileInputStream(saveFile);
boardAttachVO.setUuid(uuid.toString());
boardAttachVO.setUploadPath(uploadFolderPath);
if(checkImg(saveFile)) {
boardAttachVO.setFileType(true);
// Stream은 파일을 통신할 때 byte가 이동할 경로이다.
// 썸네일 파일 업로드
FileOutputStream thumbnail = new FileOutputStream(new File(uploadPath, "s_" + uploadFileName));
// 사용자가 첨부한 파일은 multipartFile을 통해서 가져오고,
// 원하는 width, height를 지정한 후 변경된 이미지 파일을 FileOutputStream 객체를 통해서 업로드 한다.
// Thumbnailator는 중간 관리의 역할을 한다.
Thumbnailator.createThumbnail(in, thumbnail, 100, 100);
thumbnail.close();
}
succeedList.add(boardAttachVO);
} catch (Exception e) {
failureList.add(boardAttachVO);
log.error(e.getMessage());
}finally {
if(in != null) {
try {
in.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
allFile.setSucceedList(succeedList);
allFile.setFailureList(failureList);
return new ResponseEntity<AllFileDTO>(allFile, HttpStatus.OK);
}
@PostMapping("/deleteFile")
@ResponseBody
public ResponseEntity<String> deleteFile(String fileName, String type){
log.info("deleteFile : "+ fileName);
File file;
/*
encode : 헤더에 담은 데이터에 명령어로 인식 될 수 있꺼나 특수문자 등이 포함되어 있을 때에는
해당 문자에 대한 코드번호로 대체하는 작업
\\ ---> %2F : encoding
%2F ---> \\ : decoding
*/
try {
// 슬래쉬(/) 때문에 인코딩을 하는 것인데, javascript에서 헤더에 파일을 인코딩했던 것들을
// 다시 원상 복구하여 파일을 삭제해야한다.
file = new File(fileFolder + URLDecoder.decode(fileName, "UTF-8"));
file.delete();
System.out.println(fileName);
System.out.println(type);
// 원본 파일이라면
if(type.equals("image")) {
// 썸네일 s_ 를 지워서 경로를 변수에 담는다.
String largeFileName = file.getPath().replace("s_", "");
// String largeFileName = file.getAbsolutePath().replace("s_", "");
// file 객체에 new File(URI) 매개변수에 위 largeFileName 경로 변수로 담는다.
file = new File(largeFileName);
// 삭제
file.delete();
}
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
return new ResponseEntity<String>("deleted", HttpStatus.OK);
}
private String getFolder() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Date date = new Date();
String str = sdf.format(date);
return str.replace("-", File.separator); // 구분점 빼줌
}
private boolean checkImg(File file) throws IOException{
// 사용자가 업로드한 파일의 타입 중 앞부분이 image로 시작한다면 이미지 파일이다.
log.info("test: "+Files.probeContentType(file.toPath()));
return Files.probeContentType(file.toPath()).startsWith("image");
}
}
- AllFileDTO
package com.koreait.domain;
import java.util.List;
import lombok.Data;
@Data
public class AllFileDTO {
List<BoardAttachVO> succeedList;
List<BoardAttachVO> failureList;
}
이 후 오류가 없다면 서버를 재실행하고 다시 글 등록으로 들어가서 첨부파일 등록을 해본다.
테스트
업로드된 결과를 화면에 썸네일 등을 만들어서 처리하는 부분은 별도의 showUploadResult() 함수를 제작하고 결과를 반영한다.
function showUploadResult(uploadResultArr) {
if(!uploadResultArr || uploadResultArr.length == 0){return;}
var uploadUL = $(".uploadResult ul");
var str = "";
$(uploadResultArr).each(function(i, obj) {
if(!obj.image){
var fileCallPath = encodeURIComponent(obj.uploadPath+ "/" + obj.uuid + "_" + obj.fileName);
var fileLink - fileCallPath.replace(new RegExp(/\\/g), "/");
str += "<li><a href='/download?fileName = " + fileCallPath +"'><img src='/resources/image/attach.png'>"
+ obj.fileName+"</a>" + "<span data-file=\'"+ fileCallPath +"\' data-type='file'>x</span>"
+ "<div></li>"
} else {
var fileCallPath = encodeURIComponent(obj.uploadPath + "/s_" + obj.uuid)
var originPah = obj.uploadPath + "\\" + obj.uuid + "_" + obj.fileName;
originPath = originPath.replace(new RegExp(/\\/g),"/");
str += "<li><a href=\'javascript:showImage(\"
+ originPath + "\')\"><mg src ='/display?fileName='" + obj,fileName + ")</a>";
+ "<span data-file\'" + fileCallPath + "\data=type='image'>x</span></li>";
}
});
uploadResult.append(str);
}
register.jsp 중 ajax부분 수정
$.ajax({
url : "/uploadAjaxAction",
processData : false,
contentType : false,
data : formData,
type : "POST",
dataType : "JSON",
success: function(result) {
console.log(result);
showUploadResult(result);
}
});
위의 함수를 추가한 뒤,
$.ajax() 호출 부분에 함수를 호출하는 코드를 넣는다.
이미지 파일인 경우와 일반 파일의 경우에 보여지는 화면의 내용은 showUploadResult() 내에 아래와 같은 HTML 태그들을 이용해서 작성한다.
function showUploadResult(uploadResultArr) {
if(!uploadResultArr || uploadResultArr.length == 0){return;}
var str = "";
$(uploadResultArr).each(function(i, obj) {
if(!obj.fileType){
// 일반 파일
var fileCallPath = encodeURIComponent( obj.uploadPath+"/"+ obj.uuid +"_"+obj.fileName);
var fileLink = fileCallPath.replace(new RegExp(/\\/g),"/");
str += "<li><div>";
str += "<span> "+ obj.fileName+"</span>";
str += "<button type='button' data-file=\'"+fileCallPath+"\' data-type='file' class='btn btn-warning btn-circle'><i class='fa fa-times'></i></button><br>";
str += "<img src='/resources/img/attach.png'></a>";
str += "</div>";
str +"</li>";
}else {
// 이미지 파일
var fileCallPath = encodeURIComponent( obj.uploadPath+ "/s_"+obj.uuid +"_"+obj.fileName);
str += "<li><div>";
str += "<span> "+ obj.fileName+"</span>";
str += "<button type='button' data-file=\'"+fileCallPath+"\' data-type='image' class='btn btn-warning btn-circle'><i class='fa fa-times'></i></button><br>";
str += "<img src='/display?fileName="+fileCallPath+"'>";
str += "</div>";
str +"</li>";
}
});
uploadUL.append(str);
}
2-2. 첨부파일의 변경 처리
첨부파일의 변경은 사실상 업로드된 파일의 삭제이므로 'x' 모양의 아이콘을 클릭할 때 이루어지도록 이벤트를 처리한다.
- register.jsp
$(".uploadResult").on("click", "button", function(e){
console.log("delete File");
});
삭제를 위해서는 업로드된 파일의 경로와 UUID가 포함된 파일 이름이 필요하므로 앞서 작성된 부분을 수정한다.
<button> 태그에 'data-file'과 'data-type' 정보를 추가한다.
'x' 아이콘을 클릭하면 서버에서 삭제하도록 이벤트를 처리한다.
$(".uploadResult").on("click", "button", function(e){
console.log("delete File");
var targetFile = $(this).data("file");
var type = $(this).data("type");
var targetLi = $(this).closest("li");
$.ajax({
url : 'deleteFile',
data : {fileName : targetFile, type : type},
dataType : 'text',
type : 'POST',
success : function(result) {
alert(result);
targetLi.remove();
}
});
});
2-3. 게시물 등록과 첨부파일의 데이터베이스 처리
게시물의 등록 과정에서는 첨부파일의 상세조회는 의미가 없고, 단순히 새로운 첨부파일을 추가하거나 삭제해서 자신이 원하는 파일을 게시물 등록할 때 같이 포함호도록 한다. Ajax를 이용하는 경우 이미 어떠한 파일을 첨부로 할 것인지는 이미 완료된 상태이므로 남은 작업은 게시물이 등록될 때 첨부파일과 관련된 자료를 같이 전송하고, 이를 데이터베이스에 등록하는 것이다. 게시물의 등록은 <form> 태그를 통해서 이루어지므로, 이미 업로드 된 첨부파일의 정보는 별도의 <input type='hidden'> 태그를 생성해서 처리한다.
이를 위해서는 첨부파일의 정보를 태그로 생성할 때 첨부파일과 관련된 정보(data-uuid, data-filename, data-type)를 추가한다.
function showUploadResult(uploadResultArr) {
if(!uploadResultArr || uploadResultArr.length == 0){return;}
var str = "";
$(uploadResultArr).each(function(i, obj) {
if(!obj.fileType){
// 일반 파일
var fileCallPath = encodeURIComponent( obj.uploadPath+"/"+ obj.uuid +"_"+obj.fileName);
var fileLink = fileCallPath.replace(new RegExp(/\\/g),"/");
str += "<li data-path='" + obj.uploadPath + "' data-uuid='"+obj.uuid+"' data-filename='"+obj.fileName+"' data-type='"+obj.fileType+"'>";
str += "<div>";
str += "<span> "+ obj.fileName+"</span>";
str += "<button type='button' data-file=\'"+fileCallPath+"\' data-type='file' class='btn btn-warning btn-circle'><i class='fa fa-times'></i></button><br>";
str += "<img src='/resources/img/attach.png'></a>";
str += "</div>";
str +"</li>";
}else {
// 이미지 파일
var fileCallPath = encodeURIComponent( obj.uploadPath+ "/s_"+obj.uuid +"_"+obj.fileName);
str += "<li data-path='" + obj.uploadPath + "' data-uuid='"+obj.uuid+"' data-filename='"+obj.fileName+"' data-type='"+obj.fileType+"'>";
str += "<div>";
str += "<span> "+ obj.fileName+"</span>";
str += "<button type='button' data-file=\'"+fileCallPath+"\' data-type='image' class='btn btn-warning btn-circle'><i class='fa fa-times'></i></button><br>";
str += "<img src='/display?fileName="+fileCallPath+"'>";
str += "</div>";
str +"</li>";
}
});
uploadUL.append(str);
}
<input type='hidden'> 으로 처리된 첨부파일의 정보는 BoardVO로 수집된다.
BoardVO에는 attachList 라는 이름의 변수로 첨부파일의 정보를 수집하기 때문에 <input type='hidden'>의 name은 'attachList[인덱스번호]'와 같은 이름을 사용하도록 한다.
JSP 화면에서는 javascript를 이용해서 기존에 <form> 태그를 전송하는 부분을 아래와 같이 수정한다.
var formObj = $("form[role='form']");
$("button[type='submit']").on("click", function(e) {
e.preventDefault(); // 로직이 수행된 후 서브밋을 하기 위해 서브밋을 막기위한 처리.
console.log("submit clicked");
var str = "";
var jobj = $(obj);
$(".uploadResult ul li").each(function(i, obj) {
console.log("파일 이름 . : "+ jobj.data("fileName"));
str += "<input type='hidden' name='attachList["+ i +"].uploadPath' value='"+ jobj.data("path")+"'>";
str += "<input type='hidden' name='attachList["+ i +"].uuid' value='"+ jobj.data("uuid")+"'>";
str += "<input type='hidden' name='attachList["+ i +"].fileName' value='"+ jobj.data("filename")+"'>";
str += "<input type='hidden' name='attachList["+ i +"].fileType' value='"+ jobj.data("type")+"'>";
});
formObj.append(str).submit();
});
브라우저에서 게시물 등록을 선택하면 이미 업로드된 항목들을 내부적으로 <input type='hidden'>태그들로 만들어서 <form> 태그가 ㄴubmit 될 때 같이 전송되도록 한다.
3. BoardController, BoardService의 처리
파라미터를 수집하는 BoardController는 별도의 처리 없이 전송되는 데이터가 제대로 수집되었는지를 먼저 확인한다.
-BoardController.java의 일부
// 전송방식은 forward 방식이 Default로 지정되어 있다.
@PostMapping("/register")
public String register(BoardVO board, RedirectAttributes rttr) {
log.info("register : " + board);
if(board.getAttachList() != null ) {
board.getAttachList().forEach(attach -> log.info(attach));
}
service.register(board);
rttr.addFlashAttribute("result", board.getBno());
return "redirect:/board/list";
}
BoardController의 register()는 BoardService를 호출하기 전에 log를 이용해서 확인하는 작업을 먼저 진행한다. 브라우저에서 첨부파일을 추가하고 게시물을 등록하면 서버에서는 아래와 같은 로그들이 출력되는 것을 볼 수 있다. 이때 첨부파일이 이미지인지 여부에 따라서 file Type 등이 제대로 처리되는지 확인한다.
3-1. BoardServiceImple 처리
BoardMapper와 BoardAttachMapper는 이미 작성해 두었기 때문에 남은 작업은 BoardServiceImpl에서 두 개의 Mapper 인터페이스 타입을 주입하고, 이를 호출하는 일이다. 2개의 Mapper를 주입 받아야 하기 때문에 자동주입 대신에 Setter 메서드를 이용하도록 수정한다.
-BoardService
// 전체 첨부파일 가져오기
public List<BoardAttachVO> getAttachList(Long bno);
- BoardServiceImpl 클래스의 일부
@Service
@Log4j
@AllArgsConstructor
public class BoardServiceImple implements BoardService {
// Autowired가 없는 이유는 생성자로 초기화했기 때문에 Setter를 사용하지 않는다.
private BoardMapper mapper;
private BoardAttachMapper attachMapper;
게시물의 등록 작업은 TBL_BOARD 테이블과 TBL_ATTACH 테이블 양쪽 모두 insert가 진행되어야 하기 때문에 트랜잭션 처리가 필요하다. 일반적인 경우라면 오라클의 시퀀스를 이용해서 nextval과 currval을 이용해서 처리하겠지만, 예제는 이미 MyBatis의 selectkey를 이용했기 때문에 currval을 매번 호출할 필요는 없다.
@Transactional
@Override
public void register(BoardVO board) {
log.info("register........."+ board);
mapper.insertSelectKey_bno(board);
if(board.getAttachList() == null || board.getAttachList().size() <= 0) {
return;
}
board.getAttachList().forEach(attach -> {
attach.setBno(board.getBno());
attachMapper.insert(attach);
});
}
@Transactional
@Override
public boolean modify(BoardVO board) {
log.info("modify.........."+ board);
// 첨부파일이 게시글보다 우선순위가 높다(cardinality)
// 첨부파일 작업이 모두 잘 삭제되고, 게시글의 내용이 수정된다면,
// 첨부파일 추가 시 충돌이 발생되지 않는다.
// 만약 이 부분을 지키지 않을 경우 다른 트랜젝션에 의해 롤백 될 수 있다(방지할 수 있지만, 안전하게 설계).
// 이해를 돕고자 설명 하자면,
// *테이블 2개 이상
// 1순위 : 전체 삭제
attachMapper.deleteAll(board.getBno());
// 2순위 : 전체 수정 (2개 이상)
boolean modifyResult = (mapper.update(board) == 1);
// 수정 성공 시, 새롭게 추가된 첨부파일을 추가
// 3순위 : DML
if(modifyResult && board.getAttachList().size() > 0 && board.getAttachList() != null) {
board.getAttachList().forEach(attach ->{
attach.setBno(board.getBno());
attachMapper.insert(attach);
});
}
return modifyResult;
}
@Transactional
@Override
public boolean remove(Long bno) {
log.info("remove..........." + bno);
attachMapper.deleteAll(bno);
return mapper.delete(bno) == 1;
}
@Override
public List<BoardAttachVO> getAttachList(Long bno) {
log.info(" get Attach List by bno " + bno);
return attachMapper.findByBno(bno);
}
BoardServiceImpl의 register()는 트랜잭션 하에서 TBL_BOARD에 먼저 게시물을 등록하고, 각 첨부파일은 생성된 게시물 번호를 세팅한 후 TBL_ATTACH 테이블에 데이터를 추가한다. MyBatis 쪽에 문제가 없다면 데이터베이스의 TBL_ATTACH 테이블에 첨부파일이 여러 개 등록 되었을 때 아래와 같은 모습으로 출력되는 것을 볼 수 있다.
'⚙️ Backend > Spring Web Project' 카테고리의 다른 글
[33] 파일 업로드 처리 - 첨부파일의 다운로드 혹은 원본 보여주기 - 스프링 (1) | 2021.05.31 |
---|---|
[01] 실행 환경 구축하기 - 스프링 웹 프로젝트 (0) | 2021.05.31 |
댓글