본문 바로가기
Spring Boot

스프링 부트(Spring Boot) 게시판 - 첨부파일 다운로드하기 (다중 파일 업로드 & 다운로드 구현하기 3/3) [Thymeleaf, MariaDB, IntelliJ, Gradle, MyBatis]

by 도뎡 2023. 6. 12.
반응형
  • 본 게시판 프로젝트는 단계별(step by step)로 진행되니, 이전 단계를 진행하시는 것을 권장드립니다.
  • DBMS 툴은 DBeaver를 이용하며, DB는 MariaDB를 이용합니다. (MariaDB 설치하기)
  • 화면 처리는 HTML5 기반의 자바 템플릿 엔진인 타임리프(Thymeleaf)를 사용합니다.

이전 글에서는 게시글에 등록된 첨부파일을 상세 페이지에서 조회하는 방법과, 게시글을 수정할 때 기존에 등록된 첨부파일을 유지하는 방법, 그리고 첨부파일을 추가/변경/삭제하는 방법을 알아보았습니다.

이번에는 다중 첨부파일 업로드/다운로드 처리 중 마지막 단계인, 첨부파일을 다운로드하는 방법을 알아볼 건데요. 파일 다운로드 기능은 업로드 처리에 비해 비교적 로직이 간단합니다.

 

1. FileMapper 인터페이스 - 첨부파일 상세정보 조회 메서드 추가하기

첨부파일 다운로드는 게시글 상세 페이지에서 파일명을 클릭했을 때 실행됩니다. 이때 파일의 id(PK)를 컨트롤러로 전달해서, 다운로드할 첨부파일의 상세정보를 DB에서 조회해야 합니다.

게시글 상세 페이지 - 첨부파일 영역

 

DB에서 첨부파일 상세정보를 조회할 수 있도록 FileMapper 인터페이스에 아래 메서드를 추가해 주세요.

    /**
     * 파일 상세정보 조회
     * @param id - PK
     * @return 파일 상세정보
     */
    FileResponse findById(Long id);

 

2. FileMapper XML - 첨부파일 상세정보 조회 SQL 쿼리 작성하기

다음은 FileMapper.xml입니다. 앞에서 추가한 메서드와 연결할, 파일 상세정보 조회 쿼리를 작성해 주세요. 심플한 쿼리이니 설명은 생략하도록 하겠습니다.

    <!-- 파일 상세정보 조회 -->
    <select id="findById" parameterType="long" resultType="com.study.domain.file.FileResponse">
        SELECT
            <include refid="fileColumns" />
        FROM
            tb_file
        WHERE
            delete_yn = 0
            AND id = #{value}
    </select>

 

3. FileService 클래스 - 첨부파일 상세정보 조회 메서드 추가하기

서비스에서도 특별한 로직 없이 단순히 쿼리 실행 결과만 리턴합니다.

    /**
     * 파일 상세정보 조회
     * @param id - PK
     * @return 파일 상세정보
     */
    public FileResponse findFileById(final Long id) {
        return fileMapper.findById(id);
    }

 

4. FileUtils 클래스 - 첨부파일(리소스) 조회 메서드 추가하기

스프링은 Resource라는 인터페이스를 제공해 주는데요. Resource는 InputStreamSource를 상속하며, 리소스(자원)에 대한 접근을 추상화하기 위해 사용됩니다. 우리는 이 Resource를 이용해서 파일 다운로드를 처리합니다.


KEMON 님의 블로그에 Resource에 대한 설명이 잘 정리되어 있으니, 자세히 알아보고 싶으시다면 한 번쯤은 읽어보시는 걸 권장드립니다.


1~3번은 DB에서 첨부파일 정보를 조회하는 기능이었고, 지금부터는 DB에서 조회한 첨부파일 정보를 이용해서 디스크상에 업로드된 물리적 파일 객체를 읽어 들이는 기능이 필요합니다. 우선 FileUtils에 아래 메서드를 추가해 주세요.

    /**
     * 다운로드할 첨부파일(리소스) 조회 (as Resource)
     * @param file - 첨부파일 상세정보
     * @return 첨부파일(리소스)
     */
    public Resource readFileAsResource(final FileResponse file) {
        String uploadedDate = file.getCreatedDate().toLocalDate().format(DateTimeFormatter.ofPattern("yyMMdd"));
        String filename = file.getSaveName();
        Path filePath = Paths.get(uploadPath, uploadedDate, filename);
        try {
            Resource resource = new UrlResource(filePath.toUri());
            if (resource.exists() == false || resource.isFile() == false) {
                throw new RuntimeException("file not found : " + filePath.toString());
            }
            return resource;
        } catch (MalformedURLException e) {
            throw new RuntimeException("file not found : " + filePath.toString());
        }
    }

 

구성 요소 설명
file DB에서 조회한 첨부파일의 상세정보를 의미합니다. 이를 통해 업로드된 첨부파일의 경로를 찾아낼 수 있습니다.
uploadedDate 첨부파일이 업로드된 날짜를 의미합니다. file 객체에 담긴 첨부파일의 생성일시(createdDate)를 연월일('yyMMdd') 형태로 포맷하면, 첨부파일이 업로드된 날짜를 추적할 수 있습니다.
filename 디스크에 업로드된 파일명을 의미합니다.
filePath 디스크에 업로드된 파일의 전체 경로를 의미합니다. (파일 경로 + 파일명)
resource 본 메서드에서 핵심이 되는 객체입니다. Resource의 구현체인 UrlResource의 생성자 파라미터로 filePath에 담긴 첨부파일의 경로를 전달해서 Resource 객체를 생성합니다.

만약 리소스가 없거나, 리소스의 타입이 파일이 아닌 경우에는 예외를 던져 로직을 종료합니다.

 

아래 이미지는 제가 기존에 업로드한 첨부파일을 다운로드했을 때 Resource 객체에 담기는 정보입니다.

resource 객체의 멤버

 

5. FileApiController 클래스 - 첨부파일 다운로드 메서드 추가하기

마지막으로 FileApiController에 FileUtils를 멤버로 선언하고, downloadFile( ) 메서드를 추가해 주세요.

package com.study.domain.file;

import com.study.common.file.FileUtils;
import lombok.RequiredArgsConstructor;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.List;

@RestController
@RequiredArgsConstructor
public class FileApiController {

    private final FileService fileService;
    private final FileUtils fileUtils;

    // 파일 리스트 조회
    @GetMapping("/posts/{postId}/files")
    public List<FileResponse> findAllFileByPostId(@PathVariable final Long postId) {
        return fileService.findAllFileByPostId(postId);
    }

    // 첨부파일 다운로드
    @GetMapping("/posts/{postId}/files/{fileId}/download")
    public ResponseEntity<Resource> downloadFile(@PathVariable final Long postId, @PathVariable final Long fileId) {
        FileResponse file = fileService.findFileById(fileId);
        Resource resource = fileUtils.readFileAsResource(file);
        try {
            String filename = URLEncoder.encode(file.getOriginalName(), "UTF-8");
            return ResponseEntity.ok()
                    .contentType(MediaType.APPLICATION_OCTET_STREAM)
                    .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; fileName=\"" + filename + "\";")
                    .header(HttpHeaders.CONTENT_LENGTH, file.getSize() + "")
                    .body(resource);

        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException("filename encoding failed : " + file.getOriginalName());
        }
    }

}

 

구성 요소 설명
postId 게시글 번호(PK)를 의미합니다.
fileId 첨부파일 번호(PK)를 의미합니다.
file DB에서 조회한 첨부파일 상세정보를 의미합니다.
resource FileUtils의 readFileAsResource( )로 첨부파일 상세정보를 전달해서 다운로드할 첨부파일을 리소스 타입으로 조회합니다.
filename 다운로드할 첨부파일의 이름을 의미합니다. 리소스를 읽어들일 땐 실제로 디스크에 저장된 파일명(saveName)을 통해 접근했지만, 다운로드되는 첨부파일의 이름은 원본 파일명(originalName)이 되어야 합니다.

추가적으로 URLEncoder.encode( )를 이용해서 다운로드한 첨부파일의 이름이 깨지는 걸 방지합니다.
ResponseEntity Rest API 방식에서 사용되는 클래스로, 이 클래스를 이용하면 사용자의 HTTP 요청(Request)에 대한 응답(Response) 데이터를 개발자가 직접 제어할 수 있습니다.

ResponseEntity에는 응답에 대한 상태코드, 헤더, 본문 데이터 등을 설정할 수 있는데요. 여기서는 파일의 MIME 타입, 파일명, 파일 크기, 마지막으로 resource를 응답 본문에 담아 파일을 다운로드 처리합니다.

 

 

6. view.html - findAllFile( ) 함수에 파일 다운로드 링크 추가하기

아래 코드는 게시글에 등록된 모든 첨부파일을 조회하는 findAllFile( )의 일부입니다. 게시글 상세 페이지에서 첨부파일의 이름을 클릭했을 때 파일 다운로드가 실행되도록, 3번의 forEach( ) 안에서 실행되는 코드를 아래와 같이 변경해 주세요.

    // 전체 파일 조회
    function findAllFile() {

        ...
        ...
        ...

        // 3. 파일 영역 추가
        let fileHtml = '<div class="file_down"><div class="cont">';
        response.forEach(row => {
            fileHtml += `<a href="/posts/${postId}/files/${row.id}/download"><span class="icons"><i class="fas fa-folder-open"></i></span>${row.originalName}</a>`;
        })
        fileHtml += '</div></div>';

        // 4. 파일 HTML 렌더링
        document.getElementById('files').innerHTML = fileHtml;
    }

 

7. 첨부파일 다운로드 해보기

앞에서 보여드렸듯이, 제 환경에서 32357번 게시글에 등록된 첨부파일은 총 네 개가 있습니다.

게시글 상세 페이지 - 첨부파일 영역

 

파일명을 클릭해 보니, 네 개 파일 모두 정상적으로 다운로드되고 있습니다. 이미 다운로드 폴더에 있는 파일은 뒤에 숫자가 붙게 됩니다.

게시글 상세 페이지 - 첨부파일 다운로드 완료

 

다운로드한 파일 모두 다 정상적으로 열리고 있습니다. 아래 이미지는 첫 번째로 다운로드한 이미지 파일입니다.

테스트(1).jpg 실행(열기) 결과

 

마치며

이렇게 해서 모든 다중 첨부파일 업로드/다운로드 기능의 구현이 완료되었습니다. 이번 글을 마지막으로 게시판에 필요한 기능의 구현이 모두 끝이 났습니다. 게시글, 댓글, 회원, 첨부파일까지 적다면 적고, 많다면 많은 기능들을 만들어 왔습니다.

부족한 게 많았던 게시판이지만, 꾸준히 글을 써나가면서 기존 글들도 보완을 해볼까 합니다. 계속해서 가다듬다 보면, 언젠가는 여러분께 좋은 내용의 글들만 공유할 수 있는 날이 오지 않을까 싶습니다.

다음 글부터는 MyBatis보다 훨씬 더 새롭고 흥미로운 기술을 적용해 볼 건데요. 흔히 ORM(객체-관계-매핑)이라고 하죠. 자바 진영에서 ORM 기술의 표준이라고 하는 JPA를 공부해 보고, JPA에 익숙해졌다 싶을 때 Querydsl이라는 프레임워크를 이용해서, 자바 코드로 SQL 쿼리를 작성하는 방법을 공유해 드리겠습니다.

오늘도 방문해 주신 여러분께 진심으로 감사의 말씀을 전합니다. 행복한 한 주 보내세요 :)

Board.zip
0.93MB

반응형

댓글