- 본 게시판 프로젝트는 단계별(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 객체에 담기는 정보입니다.
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번 게시글에 등록된 첨부파일은 총 네 개가 있습니다.
파일명을 클릭해 보니, 네 개 파일 모두 정상적으로 다운로드되고 있습니다. 이미 다운로드 폴더에 있는 파일은 뒤에 숫자가 붙게 됩니다.
다운로드한 파일 모두 다 정상적으로 열리고 있습니다. 아래 이미지는 첫 번째로 다운로드한 이미지 파일입니다.
마치며
이렇게 해서 모든 다중 첨부파일 업로드/다운로드 기능의 구현이 완료되었습니다. 이번 글을 마지막으로 게시판에 필요한 기능의 구현이 모두 끝이 났습니다. 게시글, 댓글, 회원, 첨부파일까지 적다면 적고, 많다면 많은 기능들을 만들어 왔습니다.
부족한 게 많았던 게시판이지만, 꾸준히 글을 써나가면서 기존 글들도 보완을 해볼까 합니다. 계속해서 가다듬다 보면, 언젠가는 여러분께 좋은 내용의 글들만 공유할 수 있는 날이 오지 않을까 싶습니다.
다음 글부터는 MyBatis보다 훨씬 더 새롭고 흥미로운 기술을 적용해 볼 건데요. 흔히 ORM(객체-관계-매핑)이라고 하죠. 자바 진영에서 ORM 기술의 표준이라고 하는 JPA를 공부해 보고, JPA에 익숙해졌다 싶을 때 Querydsl이라는 프레임워크를 이용해서, 자바 코드로 SQL 쿼리를 작성하는 방법을 공유해 드리겠습니다.
오늘도 방문해 주신 여러분께 진심으로 감사의 말씀을 전합니다. 행복한 한 주 보내세요 :)
댓글