Framework/Spring

Spring 첨부파일 다운로드 코드 리팩토링

MINGYUM 2022. 3. 28. 17:15

 

2022.03.05 - [Web Development/스프링\JPA] - Spring 첨부파일 다운로드 구현하기

 

Spring 첨부파일 다운로드 구현하기

첨부파일이 이미지인 경우, 클릭했을 때 화면에 크게 원본 파일을 넘겨줘야 하고, 일반 첨부파일인 경우에는 다운로드를 기본으로 실행해야 한다. MIME 타입이란? https://developer.mozilla.org/ko/docs/Web/

mingyum119.tistory.com

위의 게시물의 코드를 리팩토링한 과정을 기록해보겠다!

 

참고한 사이트 : 

todyDev :: Spring 개발 - 게시판 만들기 #첨부파일 다운로드 (tistory.com)

 

Spring 개발 - 게시판 만들기 #첨부파일 다운로드

이전 게시글에서 파일을 업로드했다면, 이번에는 업로드된 파일을 다운로드하는 기능을 만들 것이다. 생각보다 복잡하지 않아서 개발하기 좋았다. 아래는 파일 다운로드가 진행될 순서이다. 참

to-dy.tistory.com

 

업로드 상태인 첨부파일을 다운로드하는 로직은 다음과 같다. 

1. 클라이언트에서 서버에 파일 Idx로 파일 다운로드를 요청
2. 서버에서 다운로드할 파일의 정보를 DB에 요청
3. DB에서 Idx로 파일을 조회해 파일 정보를 서버에 전달
4. DB로부터 전달받은 경로를 이용해, 서버에서 파일 저장 경로에 있는 파일을 가져옴.
5. 서버에서 가져온 파일 데이터를 클라이언트로 전송하여 다운로드 진행.

 

위 링크에서는 JPA를 사용하지 않고 XML에서 직접 쿼리문을 사용해 DB와 통신하고 있다. 

이 방식을 기존에 쓰던 레이어를 적용해 JPA를 사용한 방식으로 바꿔보겠다.

 

링크에 있는 블로그에서는 코드 내에서 response에 크게 총 두 가지 작업을 하고 있다. 

1. Header 세팅
- Content-Disposition (HTTP Respose Body에 오는 컨텐츠의 기질/성향을 알려주는 속성.), "attachment; fileName=\""를 사용해서 다운로드 되는 파일 이름을 originalFileName으로 지정해준다. 
- Content-Transfer-Encoding(전송 데이터의 Body를 인코딩하는 방법을 표시) 의 값을 binary로 지정 (Base64와 Quoted-Printable이 명시되어있지 않아, 인코딩을 하지 않는 것으로 확인된다. 확실하지는 않음) 

2. 출력 스트림을 사용해 클라이언트로 Buffer 전송 
: Java.IO에서 사용하는 OutputStream을 사용해서 외부로 데이터를 출력한다. (https://bamdule.tistory.com/179)
- write() : 출력스트림에 값을 쓴다. 
- flush() : 버퍼를 지원하는 경우 버퍼에 존재하는 데이터를 내보낸다. 
- close() : OutputStream을 종료한다. 

다른 책에서는 fileName을 파라미터로 받고,  아래 FileSystemResource 생성자를 이용해서

fileName을 인자로 하여 Resource 객체를 만들어 saveFileName, originalFileName 등의 정보를 얻고 있다. 

 

 

내가 구현할 로직의 경우,

ServerFileController에서는 로직과 관련된 매개변수는 파일의 Idx에 대한 정보만 받는다.

그래야 나중에 게시판과 매핑되기 위해서 BoardFile 테이블에서, 특정 게시글을 조회했을 때

그 게시글 번호와 매핑되어 있는 File의 Idx를 조회하고 File 테이블에서 꺼내올 수 있게 된다. 

 

따라서 Integer로 받아온 FIle Id를 기준으로 DB(Repository 레이어)에서 file 객체를 가져온 뒤

file의 데이터를 가져와 reponse에 담아서 출력하면 된다!

 

1. ServerFileController

참고한 사이트에서는 Controller 단에서 CommandMap이라는 클래스를 사용했다. 

JSP에서 입력값을 받기 위해 주로 사용하며, https://beecomci.tistory.com/42

위 링크에서 사용자가 정의한 클래스를 참고해보면, 컨트롤러의 파라미터가 Map형식이면 HandlerMethodArgumentResolver가 동작하지 않아 문제가 발생한다. 

따라서 Wrapper 클래스인 CommandMap을 생성해 Map 타입을 사용하는 것이다. 

 

나는 위에서 언급했듯이 Integer을 파라미터로 받아 파일을 DB에서 조회해 사용할 것이다.

    @GetMapping(value = "/downloadFile", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
    public ResponseEntity<Resource> downloadFile(@RequestHeader("User-Agent") String userAgent, Integer fileId) {
        HttpHeaders headers = new HttpHeaders();
        return new ResponseEntity<Resource>(fileService.downloadFile(userAgent, fileId, headers), headers, HttpStatus.OK;
    }

 

2. ServerFileService

public Resource downloadFile(String userAgent, Integer fileId, HttpHeaders headers) {

        ServerFile downloadFile = serverFileRepository.findById(fileId)
                .orElseThrow(() -> new EntityNotFoundException("파일을 찾을 수 없습니다."));
        String originalFileName = downloadFile.getOriginalFileName();

        try {
            String downloadName = null;
            if (userAgent.contains("Trident")) {
                log.info("IE browser");
                downloadName = URLEncoder.encode(originalFileName, "UTF-8").replaceAll("\\+", " ");

            } else if (userAgent.contains("Edge")) {
                log.info("Edge browser");
                downloadName = URLEncoder.encode(originalFileName, "UTF-8");
            } else {
                log.info("Chrome browser");
                downloadName = new String(originalFileName.getBytes("UTF-8"), "ISO-8859-1");
            }
            headers.add("Content-Disposition", "attachment; filename=" + downloadName);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return new FileSystemResource(downloadFile.getPath());
    }

먼저 다운로드할 파일을 DB에서 조회, 파일의 순수한 이름을 가져와 담는다. 

이 파일 이름으로, Header에서 받은 User-Agent를 사용해 브라우저 별로 나누어 다운로드 로직을 처리해준다.

 서버에서 파일 이름을 지정해 다운로드할 때는, UUID 가 붙은 부분을 제거하고, 브라우저 별로 오류 없이 정상적으로 다운로드 처리하기 위해서 가공한 downloadName 변수를 사용한다. 

 

그리고 Resource 객체를 리턴해 Controller단에서 Resource를 포함한 ResponseEnitty를 반환할 수 있도록 한다. 

 

3. ServerFileRepository

package com.inhabas.api.repository;

import com.inhabas.api.domain.ServerFile;
import org.springframework.data.jpa.repository.JpaRepository;

public interface ServerFileRepository extends JpaRepository<ServerFile, Integer> {
}

 

3. ServerFile

@Entity
@Data
public class ServerFile {

    @Id @GeneratedValue
    private Integer id;

    private String uuid;

    private String originalFileName;

    private String savedFileName; // UUID 포함

    private String path; // 업로드 된 디렉터리의 절대경로

    private boolean isImage;
}

임시로 구현한 것이므로 변경의 여지가 있다!