어리바리 신입 개발자의 얼렁뚱땅 개발 기록 ✨
[ Java / SpringBoot / MVC / RESTful] RESTful한 게시판 만들기5 / 첨부파일 업로드 구현하기(MultipartResolver) 본문
REST API
[ Java / SpringBoot / MVC / RESTful] RESTful한 게시판 만들기5 / 첨부파일 업로드 구현하기(MultipartResolver)
낫쏘링 2023. 9. 11. 15:11728x90
원래는 게시글 등록할 때 첨부파일 업로드도 같이 구현해줬어햐 하는데...
첨부파일 구현 경험이 없어서 일단 CRUD 부터 다 끝내고 첨부파일 구현 시작!
[ MultipartResolver]
클라이언트로부터 데이터를 전송 받을 때 폼 데이터만 전송 받는 경우도 있지만,
폼 데이터와 함께 파일 같은 데이터도 같이 전송 받는 경우가 있다. 이런 다중 부분 데이터를 처리하기 위해 스프링에서 제공하는 인터페이스다.
주로 파일 업로드와 다운로드에 사용된다.
스프링부트에는 이미 기본 빈으로 등록되어 있기 때문에 따로 처리해줄 필요가 없다.
1. CommonsMultipartResolver (Apache Commons FileUpload를 사용)
2. StandardServletMultipartResolver (Servlet 3.0+의 멀티파트 해결 방법을 사용)
[ 파일 업로드 구현하기 ]
[ 1. application.properties와 html 작성 ]
# 첨부파일 처리 위한 설정 # 최대 파일 크기 설정 - 기본 값 1MB spring.servlet.multipart.max-file-size=1MB # 여러 파일 업로드 시 전체 파일 크기 - 기본 값 10MB spring.servlet.multipart.max-request-size=10MB # 파일 저장 경로 설정 # file.path는 내가 임의로 정한 이름 다른 걸로 해도되고 여러 개 설정해도 된다. file.path=C:/Users/abc/Documents/project/restApi/board/src/main/resources/static/boardFile<!-- enctype="multipart/form-data"를 추가해주는 것이 포인트 --> <form id="frm" name="frm" method="post" action="/board/write" enctype="multipart/form-data"> <!-- 파일 하나만 업로드 가능하게 할 때 --> <input type="file" id="file" name="file"> <!-- 여러 파일 업로드 가능하게 할 때 --> <input type="file" id="files" name="files" multiple="multiple"> <input type="submit" id="submit" value="등록" class="btn"> </form>
[ 2. BoardFileDto 생성 ]
- dto 패키지 내에 BoardFileDto 클래스 생성
@Getter @Setter @ToString public class BoardFileDto { private int idx; private int boardIdx; private String originalFileName; private String storedFilePath; private long fileSize; }
[ 3. BoardController 수정 ]
// 기존 게시글 등록 처리 @PostMapping("/board") public String insertBoard(BoardDto board) throws Exception{ boardService.insertBoard(board); return "redirect:/board"; }// 수정 후 게시글 등록 처리 // @RequestParam("files") List<MultipartFile> files 추가 @PostMapping("/board") public String insertBoard(BoardDto board, @RequestParam("files") List<MultipartFile> files) throws Exception{ boardService.insertBoard(board, files); return "redirect:/board"; }
[ 4. BoardFileService 생성 ]
- BoardFileService에서 따로 파일 처리를 위한 기능을 만들어 줄 것
/** * 프로퍼티 파일, 환경 변수, 시스템 속성 등의 외부 설정 값을 Spring 빈에 주입한다. * application.properties(프로퍼티 파일)에 작성해 둔 file.path를 사용하기 위해 썼다. 즉, 프로퍼티 파일에 * 작성해둔 file.path라는 값을 필드의 filePath에 주입하겠다는 뜻 */ @Value("${file.path}") private String filePath; private final BoardMapper boardMapper; /** * 첨부파일 서버 컴퓨터에 저장 & 파일 정보 리턴 * */ public List<BoardFileDto> uploadFile(int boardIdx, List<MultipartFile> files) throws Exception { // 업로드한 파일이 없으면 null 반환 // 만약 files.isEmpty() 하면 false 떠서 return이 안된다. // files는 데이터 타입이 List<MultipartFile> 이기 때문에 일단 List 자체에는 비어있는 MultipartFile이 존재하기 때문 // 실제로 files.size()는 1이고 files.get(0).getSize()는 0이 나온다. // 참고로 size()는 List 같은 컬렉션에서 제공하는 메서드고 // getSize()는 MultipartFile에서 제공하는 메서드라는 점에서 차이를 알 수 있다. // files.isEmpty()는 files가 String이거나 컬렉션 등 특정 타입에서만 사용 가능하다. // 다양한 타입에서 사용할 경우에는 ObjectUtils.isEmpty(files) 사용 // 하지만, MultiparFile 자체에서 isEmpty()를 제공하기 때문에 files.isEmpty()를 사용 if(files.get(0).isEmpty()) { return null; } // 파일을 저장할 경로 지정하고 폴더 생성하기 // 날짜를 20230911 형식으로 가져오도록 미리 형식 선언 DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("yyyMMdd"); // 파일 저장 당시 날짜 가져오기 ZonedDateTime currentDate = ZonedDateTime.now(); // 파일 저장 경로 설정해주기 (날짜를 미리 선언해둔 형식으로 바꿔줘라) String path = filePath + currentDate.format(dateFormat); // File 객체는 파일 또는 디렉토리의 경로 정보를 저장하고 있다. 그래서 생성할 때 path 넣어준다. // File 객체가 제공하는 메서드 exists,isFile,isDirectory,mkdir,mkdirs... File file =new File(path); // 만약 지정된 경로에 해당 폴더가 존재하지 않을 경우 폴더를 만든다. // exists() : 파일 및 디렉토리 존재여부 확인 (파일 : isFile() / 디렉토리 : isDirectory()) if(file.isDirectory() == false) { // mkdir() : 경로의 가장 마지막 폴더만 생성 / mkdirs() : 없는 폴더 전부 생성 // 예를 들어 경로가 /a/b/c 인데 a라는 폴더만 있을 경우 mkdir()는 c 폴더만 / mkdirs()는 b,c 폴더 생성 file.mkdirs(); } List<BoardFileDto> fileList = new ArrayList<>(); for (MultipartFile oneFile : files) { // 원래 파일 이름 추출 MultipartFile에서 제공하는 메서드 (확장자 포함) String originalFileName = oneFile.getOriginalFilename(); // 확장자 추출 // lastIndexOf() : 문자열의 끝에서 부터 해당 문자열을 찾아 인덱스를 반환 (확장자명은 .으로 구분되어 있기 때문) // substring(숫자) : 해당 인덱스부터 끝까지 반환 // 만약 파일 이름이 img.gif 였다면 substring(3) -> .gif 반환 String fileExtension = originalFileName.substring(originalFileName.lastIndexOf(".")); // 새로운 파일 이름 (확장자 포함) // 새로운 파일 이름은 유일성을 보장해야한다. (중복 x) // UUID라는 객체는 따로 생성자 생성 없이 사용 가능 // UUID.randomUUID()는 유일성을 보장하는 랜덤 객체 반환 // 즉 UUID 객체를 반환하기 때문에 Strign으로 형변환 String uniqueObject = UUID.randomUUID().toString(); // 문자열로 변환한 UUID 객체와 확장자 결합 String newFileName = uniqueObject + fileExtension; BoardFileDto boardOneFile = new BoardFileDto(); boardOneFile.setBoardIdx(boardIdx); boardOneFile.setFileSize(oneFile.getSize()); boardOneFile.setOriginalFileName(originalFileName); boardOneFile.setStoredFilePath(path + "/" + newFileName); fileList.add(boardOneFile); // File 객체에 파일 저장 (파일 확장자 포함한 파일경로) file = new File(path + "/" + newFileName); // onFile을 file 경로로 전송(저장)한다. oneFile.transferTo(file); } return fileList; } /** 첨부파일 등록 */ public void insertFile(BoardDto board, List<MultipartFile> files) throws Exception{ // 파일을 서버 컴퓨터에 저장 후 파일 정보들 DTO에 받아주기 // useGeneratedKeys로 받아온 새로 생성된 boardIdx를 매개변수로 넣어준다. List<BoardFileDto> fileList = uploadFile(board.getBoardIdx(), files); if(fileList == null) { return; } // 데이터베이스에 파일 정보 저장 boardMapper.insertFileList(fileList); }
[ 5. BoardService 수정 ]
// 기존 게시글 등록 로직 public void insertBoard(BoardDto board) throws Exception{ boardMapper.insertBoard(board); }// 수정 후 게시글 등록 public void insertBoard(BoardDto board, List<MultipartFile> files) throws Exception{ // form 데이터 등록 // dao에서 등록된 pk(boardIdx) 받아오도록 작성함 - useGeneratedKeys boardMapper.insertBoard(board); // 첨부파일 등록 boardFileService.insertFile(board, files); }
[ 6. BoardMapper 작성 ]
// 데이터베이스에 업로드 파일 정보 저장 void insertFileList(List<BoardFileDto> fileList) throws Exception;
[ 7. DAO(SQL) 수정 및 작성 ]
<insert id="insertBoard" parameterType="BoardDto"> /* 기존 게시글 등록 */ <![CDATA[ INSERT INTO t_board ( title, contents, created_datetime, creator_id ) VALUES ( #{title}, #{contents}, NOW(), 'admin' ) ]]> </insert><!-- useGeneratedKey : insert 문에서 insert된 auto-increment 또는 sequence로 생성된 기본 키의 값을 가져온다. --> <!-- keyProperty : 가져온 값을 담아주는 변수 --> <insert id="insertBoard" parameterType="BoardDto" useGeneratedKeys="true" keyProperty="boardIdx"> /* 게시글 등록 */ <![CDATA[ INSERT INTO t_board ( title, contents, created_datetime, creator_id ) VALUES ( #{title}, #{contents}, NOW(), 'admin' ) ]]> </insert>
<insert id="insertFileList" parameterType="BoardFileDto"> /* 데이터베이스에 업로드 파일 정보 저장 */ <![CDATA[ INSERT INTO t_file ( board_idx, original_file_name, stored_file_path, file_size, creator_id, created_datetime ) VALUES ]]> /* 받아온 데이터(collection) : List 일 때 반복해서 쿼리 생성*/ <foreach collection="list" item="item" separator=","> ( #{item.boardIdx}, #{item.originalFileName}, #{item.storedFilePath}, #{item.fileSize}, 'admin', Now() ) </foreach> </insert>
[ 게시글 클릭했을 때 첨부파일 정보 같이 보기 ]
[ 1. DAO(SQL) 작성 ]
<!-- 바이트 크기로 저장되어 있기 때문에 킬로바이트 크기로 변환한다. --> <select id="selectFileList" parameterType="int" resultType="BoardFileDto"> /* 해당 첨부파일 조회 */ <![CDATA[ SELECT idx, board_idx, original_file_name, stored_file_path, FORMAT(ROUND(file_size / 1024), 0) AS file_size FROM t_file WHERE board_idx = #{boardIdx} AND deleted_yn = 'N' ]]> </select>
[ 2. BoardDto 추가 ]
- BoardDto와 BoardFileDto는 1 : N 관계다.
- boardIdx 하나에 파일 정보 여러 개가 매핑 될 수 있다는 뜻
- 그래서 BoardDto에 BoardFileDto 타입으로 List 필드를 하나 추가해 준다.
private List<BoardFileDto> fileList;
[ 3. BoardService 수정 ]
// 게시글 상세 내용 조회 public BoardDto selectDetail(int boardIdx) throws Exception{ // 상세 내용 조회 -> 조회수 증가 boardMapper.updateHitCount(boardIdx); // 상세 내용 가져오기 BoardDto boardDetail = boardMapper.selectDetail(boardIdx); // 파일들 정보 가져오기 List<BoardFileDto> fileList = boardMapper.selectFileList(boardIdx); boardDetail.setFileList(fileList); return boardDetail; }
[ 4. BoardMapper 추가 ]
List<BoardFileDto> selectFileList(int boardIdx) throws Exception;

728x90