그라가승훈
[SpringBoot] - 파일 업로드와 다운로드 본문
- 파일 첨부를 위한 기본 설정.
1. 라이브러리 추가
스프링 프레임워크에는 파일 업로드를 위한 MultipartResolver 인터페이스가 정의되어있다.
일반적으로 사용되는 구현체는 아래 두가지이다.
- 아파치의 Common Fileupload를 이용한 CommonMultipartResolver
- 서블릿 3.0 이상의 API를 이용한 StandardServletMultipartResolver
여기서는 아파치의 Common Fileupload 를 사용했다.
- build.gradle 에 라이브러리 추가
commons-fileupload 는 commons-io 를 이용하기 때문에 두개의 라이브러리를 추가한다.
작성일 기준 최신 버전을 사용했다.
// 아파치 Common Fileupload
implementation 'commons-io:commons-io:2.14.0'
implementation 'commons-fileupload:commons-fileupload:1.5'
2. 파일 처리를 위한 빈 설정
WebMvcConfiguration 클래스에 다음 코드를 추가.
@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {
@Bean
public CommonsMultipartResolver multipartResolver() {
CommonsMultipartResolver commonsMultipartResolver = new CommonsMultipartResolver();
commonsMultipartResolver.setDefaultEncoding("UTF-8"); // 파일 인코딩
commonsMultipartResolver.setMaxUploadSizePerFile(5 *1024 * 1024); // 5mb (바이트 단위로 업로드 되는 파일 크기)
return commonsMultipartResolver;
}
3. 파일 관련 자동구성 제거
스프링 부트의 특성 중 하나는 애플리케이션의 스프링 설정이 자동으로 구성된다는 것이다.
multipartResolver 를 등록했기 때문에 첨부파일 관련된 자동 구성을 제거해야 한다.
Application 클래스의 @SpringBootApllication 어노테이션에서 MultipartAutoConfiguration 을 제거한다.
// 스프링부트에서 자동으로 구성된 첨부파일 관련 MultipartAutoConfiguration 클래스 제외
@SpringBootApplication(exclude = {MultipartAutoConfiguration.class})
public class BoardApplication {
public static void main(String[] args) {
SpringApplication.run(BoardApplication.class, args);
}
}
- 파일 업로드
1. 파일을 업로드하고 파일의 정보 확인
- boardWrite.html
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>board</title>
<link rel="stylesheet" th:href="@{/css/style.css}"/>
</head>
<body>
<div class="container">
<h2>게시글 등록</h2>
<form id="frm" name="frm" method="post" action="/board/insertBoard.do" enctype="multipart/form-data">
<table class="board_detail">
<tr>
<td>제목</td>
<td><input type="text" id="title" name="title"/></td>
</tr>
<tr>
<td colspan="2">
<textarea id="contents" name="contents"></textarea>
</td>
</tr>
</table>
<input type="file" id="files" name="files" multiple="multiple">
<input type="submit" id="submit" value="저장" class="btn">
</form>
</div>
</body>
</html>
- BoardController
@RequestMapping("/board/insertBoard.do")
public String insertBoard(BoardDto dto, MultipartHttpServletRequest multipartHttpServletRequest) throws Exception{
boardService.insertBoard(dto, multipartHttpServletRequest);
return "redirect:/board/openBoardList.do";
}
MultipartHttpServletRequest 는 ServletRequest 를 상속받아 구현된 인터페이스로, 업로드된 파일을 처리하기 위한 여러가지 메서드가 제공된다.
- BoardService
@Override
@Transactional
public void insertBoard(BoardDto dto, MultipartHttpServletRequest multipartHttpServletRequest) throws Exception{
if (!ObjectUtils.isEmpty(multipartHttpServletRequest)) {
// input 태그에서 file 타입을 가진 태그의 name 들을 Iterator 로 받을 수 있다.
Iterator<String> iterator = multipartHttpServletRequest.getFileNames();
String name;
while (iterator.hasNext()) {
name = iterator.next();
log.debug("file tag name : {}", name);
List<MultipartFile> list = multipartHttpServletRequest.getFiles(name);
for (MultipartFile multipartFile : list) {
log.debug("start file information !!");
// ex) file name : 1.jpg, size : 249055, contentType : image/jpeg
log.debug("file name : {}, size : {}, contentType : {}", multipartFile.getOriginalFilename(), multipartFile.getSize(), multipartFile.getContentType());
log.debug("end file informantion !! \n");
}
}
}
// boardMapper.insertBoard(dto);
}
- 파일 정보 로그 확인

2. 서버에 업로드 파일 저장 및 DB 저장
- FileUtils 생성
파일 정보를 가공하고 지정된 위치에 파일을 저장한다. common 패키지에 생성.
@Component
public class FileUtils {
public List<BoardFileDto> parseFileInfo(int boardIdx, MultipartHttpServletRequest multipartHttpServletRequest) throws Exception{
if (ObjectUtils.isEmpty(multipartHttpServletRequest)) return null;
List<BoardFileDto> fileList = new ArrayList<>();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd");
ZonedDateTime current = ZonedDateTime.now(); // 자바 1.8 부터 사용 가능
String path ="images/" + current.format(formatter);
File file = new File(path);
// 해당 폴더가 없으면 생성.
if (!file.exists()) file.mkdirs();
String newFileName, originalFileExtension, contentType;
Iterator<String> iterator = multipartHttpServletRequest.getFileNames();
while (iterator.hasNext()) {
List<MultipartFile> list = multipartHttpServletRequest.getFiles(iterator.next());
// 파일의 contentType 확인 후 확장자 넣어줌.
for (MultipartFile multipartFile : list) {
if (!multipartFile.isEmpty()) {
contentType = multipartFile.getContentType();
if (ObjectUtils.isEmpty(contentType)) break;
else {
if (contentType.contains("image/jpeg")) {
originalFileExtension = ".jpg";
} else if (contentType.contains("image/png")) {
originalFileExtension = ".png";
} else if (contentType.contains("image/gif")) {
originalFileExtension = ".gif";
} else break;
}
// 나노타임을 이용해서 이름 중복나지 않게.. (밀리초로 할 경우 중복될 수 있음)
newFileName = System.nanoTime() + originalFileExtension;
BoardFileDto boardFileDto = new BoardFileDto();
boardFileDto.setBoardIdx(boardIdx);
boardFileDto.setFileSize(multipartFile.getSize());
boardFileDto.setOriginalFileName(multipartFile.getOriginalFilename());
boardFileDto.setStoredFilePath(path +"/" + newFileName);
fileList.add(boardFileDto);
// 파일 저장
file = new File(boardFileDto.getStoredFilePath());
multipartFile.transferTo(file);
}
}
}
return fileList;
}
}
- 파일이 업로드될 폴더는 업로드 날짜로 (yyyyMMdd) 형식의 폴더에 저장, 없으면 생성
- 서버에 저장될 이름은 중복되면 정상적으로 저장되지 않기 때문에 nanoTime 으로 이름 생성.
- Service 변경
@Override
@Transactional
public void insertBoard(BoardDto dto, MultipartHttpServletRequest multipartHttpServletRequest) throws Exception{
boardMapper.insertBoard(dto);
List<BoardFileDto> list = fileUtils.parseFileInfo(dto.getBoardIdx(), multipartHttpServletRequest);
if (!CollectionUtils.isEmpty(list)) boardMapper.insertBoardFileList(list);
}
- insertBoard 쿼리 동작 후 boardIndx 를 받아와야함.
- Mapper 변경
1. insertBoard
<insert id="insertBoard" parameterType="com.example.board.dto.BoardDto"
useGeneratedKeys="true" keyProperty="boardIdx"> <!-- 생성된 key 값을 반환한다. -->
<![CDATA[
INSERT INTO t_board
(
title,
contents,
created_datetime,
creator_id
)
VALUES
(
#{title},
#{contents},
NOW(),
'admin'
)
]]>
</insert>
- useGeneratedKeys 는 DBMS가 자동 생성 키 (MySql의 경우 Auto Increment)를 지원하면 사용 가능.
- keyProperty 로 해당 키를 반환받는다.
2. insertBoardFileList
<insert id="insertBoardFileList" parameterType="com.example.board.dto.BoardFileDto">
<![CDATA[
INSERT INTO t_file (
board_idx, original_file_name, stored_file_path,
file_size,creator_id, created_datetime
)
VALUES
]]>
<foreach collection="boardFileDtoList" item="item" separator=",">
(
#{item.boardIdx}, #{item.originalFileName}, #{item.storedFilePath},
#{item.fileSize}, 'admin', NOW()
)
</foreach>
</insert>
3. 파일 업로드 결과 확인하기
- 파일 테이블

- 파일 저장 경로

- 파일 다운로드
1. 파일 조회 쿼리
<select id="selectBoardFileList" parameterType="int" resultType="com.example.board.dto.BoardFileDto">
<![CDATA[
SELECT
idx, board_idx, original_file_name,
FORMAT(ROUND(file_size / 1024), 0) AS 'file_size'
FROM t_file
WHERE board_idx = #{boardIdx} AND deleted_yn = 'N'
]]>
</select>
- 바이트 단위로 저장된 파일의 크기를 소수점을 반올림한 킬로바이트로 변환
2. 조회 Service 변경
-Service
@Override
@Transactional
public BoardDto selectBoardDetail(int boardIdx) throws Exception{
BoardDto board = boardMapper.selectBoardDetail(boardIdx);
List<BoardFileDto> fileList = boardMapper.selectBoardFileList(boardIdx);
board.setBoardFileDtoList(fileList);
boardMapper.updateHitCount(boardIdx);
return board;
}
3. 뷰 파일 목록 추가
<div class="file_list">
<a th:each="list : ${board.boardFileDtoList}"
th:href="@{/board/downloadBoardFile.do(idx=${list.idx}, boardIdx=${list.boardIdx})}"
th:text="|${list.originalFileName} (${list.fileSize} kb)|"></a>
</div>
- Thymeleaf 에서 파라미터는 "," 로 구분,
- Thymeleaf 에서 "|" 로 변수와 문자열 혼합
4. 파일 정보 가져오는 쿼리 추가
- 매퍼
BoardFileDto selectBoardFileInformation(@Param("idx") int idx,@Param("boardIdx") int boardIdx) throws Exception;
- XML
<select id="selectBoardFileInformation" parameterType="map" resultType="com.example.board.dto.BoardFileDto">
<![CDATA[
SELECT
original_file_name,
stored_file_path,
file_size
FROM
t_file
WHERE
idx = #{idx} AND board_idx = #{boardIdx} AND deleted_yn = 'N'
]]>
</select>
- 매퍼에서 @Param 어노테이션을 사용하면 애매하기 DTO 를 만들지 않더라도 xml 에서 parameterType 을 "map" 으로 받아서 사용할 수 있다.
5. 컨트롤러 추가
@RequestMapping("/board/downloadBoardFile.do")
public void downloadBoardFile(@RequestParam int idx, @RequestParam int boardIdx, HttpServletResponse response) throws Exception{
BoardFileDto fileDto = boardService.selectBoardFileInformation(idx, boardIdx);
if (!ObjectUtils.isEmpty(fileDto)) {
String fileName = fileDto.getOriginalFileName();
// storedFilePath 로 파일을 가져온 후 byte[] 로 변환 (org.appache.commons.io 라이브러리)
byte[] files = FileUtils.readFileToByteArray(new File(fileDto.getStoredFilePath()));
// response 설정.
response.setContentType("application/octet-stream");
response.setContentLength(files.length);
// 파일 이름을 UTF-8 로 인코딩 해주지 않으면 이상한 문자로 다운됨.
response.setHeader("Content-Disposition", "attachment; fileName=\"" +
URLEncoder.encode(fileName,"UTF-8") + "\";");
response.setHeader("Content-Transfer-Encoding", "binary");
// 파일 정보의 바이트 배열 데이터를 response 에 작성
response.getOutputStream().write(files);
// response 버퍼를 정리하고 닫음.
response.getOutputStream().flush();
response.getOutputStream().close();
}
}
- HttpServletResponse 를 사용하면 response 를 원하는대로 내려줄 수 있다.
- DB에서 가져온 file 의 storedFilePath 로 파일을 찾은 후 byte[] 로 변환 후 response 에 작성해서 내려준다.
// 파일명 컨버팅
ContentDisposition attachment = ContentDisposition.builder("attachment")
.filename(result.getOriginalName(), StandardCharsets.UTF_8)
.build();
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, attachment.toString());
- 파일 UTF-8 인코딩은 위처럼 해줄 수 있다.
6. 결과 확인


'Spring' 카테고리의 다른 글
[SpringBoot] - Swagger 적용 (0) | 2023.10.22 |
---|---|
[SpringBoot] - Mybatis 설정 (0) | 2023.10.15 |
[SpringBoot] - @ControllerAdvice 를 이용한 전역 예외처리 (0) | 2023.10.15 |
[SpringBoot] - AOP (2) | 2023.10.15 |
[SpringBoot] - 인터셉터(Interceptor) (0) | 2023.10.15 |