Notice
Recent Posts
Recent Comments
Link
«   2025/08   »
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
Archives
Today
Total
관리 메뉴

그라가승훈

[SpringBoot] - 파일 업로드와 다운로드 본문

Spring

[SpringBoot] - 파일 업로드와 다운로드

그라가승훈 2023. 10. 16. 23:02

- 파일 첨부를 위한 기본 설정.

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. 결과 확인

파일 다운로드
response

'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
Comments