본문 바로가기

Spring/파일 업로드, 다운로드

[Spring] 스프링으로 파일 업로드, 다운로드 구현하기 (3) - DB까지 사용하여, 실제로 파일 업로드와 다운로드 해보기

지금까지는 업로드에 대해서 알아보았다. 실제 DB에도 저장하지 않았다.

실제로 파일을 업로드, 다운로드 하기 위해서는, 파일의 내용을 저장하는 DB까지 필요하다.

 

이제, 실제 예시를 통해 파일 업로드와 파일 다운로드 방법을 알아보자.

 

[개념]

파일 업로드란?

파일 업로드라는 것이 파일을 바이너리 데이터로 만들어서 DB에 넣어둔다는 뜻이 아니다. 비싼 저장장치인 DB를 이미지 저장소로 사용하지는 않는다. 우리는 DB에 파일의 정보(파일 이름, 파일이 서버에 저장되었을 때의 이름, 컨텐트 타입, 등록 날짜, 수정 날짜 등등)만을 저장할 것이다. 파일이라는 데이터 그 자체는, 서버에 올라가게 된다(현재는 작업을 로컬에서 진행하므로, 내 로컬 저장소(하드디스크)가 그 역할을 하게 된다)

 

정리하자면 파일 업로드란,

  • 파일 그 자체 -> 서버에 업로드(현재는 로컬에서 실행하므로 내 맥북의 저장소를 말함)
  • 파일 관련 정보 -> DB에 insert

를 의미하는 것이다.

 

그렇다면, 파일 다운로드란 뭘까?

"DB에 있는 파일 정보를 가지고 서버에서 알맞은 파일 데이터를 찾아 다운로드 하는 것"이다.

 

 

[실제 예시]

파일 업로드

 

1. application.properties

아래와 같이, 파일을 저장할 로컬 저장소의 path를 명시해두어야 한다.

여기서 주의할 점은, 마지막에 / 가 들어가야 한다. 그렇지 않으면, 나중에 fullpath를 만들기 위해 path 와 filename을 합치는 과정에서 내가 원하는 path로 인식하지 않고, 마지막 단어는 filename으로 들어가게 된다.

(아래와 같은 상황에서는 /Users/yoon/ 하위 폴더로 저장된다는 뜻이다. 마지막에 /가 없으면filepractice가 filename이랑 합쳐져버리기 때문이다)

upload.path = /Users/yoon/filepractice/

 

2. controller

다른거 다 무시하고 @RequestPart 부분만 보면 된다. 파라미터로 MultipartFile 타입을 작성해주면 된다.

@PostMapping("/api/comments")
public CommentRegisterResponse register(@AuthenticationPrincipal CustomUserDetails customUserDetails,
                                        @RequestParam (value = "reservationInfoId") int reservationInfoId,
                                        @RequestParam (value = "score") int score,
                                        @RequestParam (value = "comment") String comment,
                                        @RequestPart (value = "multipartFile") MultipartFile multipartFile) throws IOException {

    int productId = commentService.register(customUserDetails, reservationInfoId, score, comment, multipartFile);

    // response 만들기
    return CommentRegisterResponse.builder()
            .result(success)
            .productId(productId)
            .build();
}

 

3. service

controller에서 multipartFile을 받았으면, 그것으로 "서버에 파일 저장 & DB에 파일 정보 저장" 작업을 하면 된다.

private 메서드 몇 개를 선언하여 깔끔한 코드로 작성하였다.

// 2. 서버에 파일 저장 & DB에 파일 정보(fileinfo) 저장
// - 동일 파일명을 피하기 위해 random값 사용
String originalFilename = multipartFile.getOriginalFilename();
String saveFileName = createSaveFileName(originalFilename);

// 2-1.서버에 파일 저장
multipartFile.transferTo(new File(getFullPath(saveFileName)));

// 2-2. DB에 정보 저장
String contentType = multipartFile.getContentType();

FileInfoRegister fileInfoRegister = FileInfoRegister.builder()
        .fileName(originalFilename)
        .saveFileName(saveFileName)
        .contentType(contentType)
        .deleteFlag(notDeleted).build();

int fileInfoId = fileInfoDao.insert(fileInfoRegister);

///////////////////////////////////////////////////////////////////////////////

// 파일 저장 이름 만들기
// - 사용자들이 올리는 파일 이름이 같을 수 있으므로, 자체적으로 랜덤 이름을 만들어 사용한다
private String createSaveFileName(String originalFilename) {
    String ext = extractExt(originalFilename);
    String uuid = UUID.randomUUID().toString();
    return uuid + "." + ext;
}

// 확장자명 구하기
private String extractExt(String originalFilename) {
    int pos = originalFilename.lastIndexOf(".");
    return originalFilename.substring(pos + 1);
}

// fullPath 만들기
private String getFullPath(String filename) {
    return uploadPath + filename;
}

 

아래와 같이 랜덤 이름으로 서버의 uploadPath에 파일이 잘 저장된 모습을 볼 수 있다.

 

 

 

파일 다운로드

@GetMapping("/api/file/{fileId}")
@ResponseBody
public ResponseEntity<Resource> download(@PathVariable("fileId") int fileId) throws MalformedURLException {
    FileInfo fileInfo = fileInfoService.get(fileId);

    String saveFileName = fileInfo.getSaveFileName();
    String originalFileName = fileInfo.getFileName();

//        UrlResource resource = new UrlResource("file:" + getFullPath(saveFileName));
    UrlResource resource = new UrlResource("file:" + "/Users/yoon/filepractice/" + saveFileName);

    log.info("saveFileName={}", saveFileName);

    String encodedOriginalFileName = UriUtils.encode(originalFileName, StandardCharsets.UTF_8);
    String contentDisposition = "attachment; filename=\"" + encodedOriginalFileName + "\"";

    return ResponseEntity.ok()
            .header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition)
            .body(resource);
}

UrlResource : "file:" 과 함께 파일의 절대경로를 주면, 해당 파일을 찾아서 반환해준다

 

Content-Disposition : 이 헤더의 값을 지정해줌으로서, 다운로드 받는 파일 명을 지정할 수 있다.

업로드한 파일 명은 랜덤하게 만들었지만, 다운로드 시에는 오리지널 파일 명으로 저장해주는 것이 가독성에 좋다. 이를 위해 Content-Disposition 헤더 값을 지정해준다.

 

UrlUtils.encode : 한글이나 특수문자가 들어간 파일 명은 깨질 수 있으므로, StandardCharsets.UTF_8 로 인코딩한 파일 이름을 사용하기 위해 사용한다. UrlUtils는 다양한 인코딩 방식을 제공하는 클래스임을 기억하자.

 

 

위와 같은 코드로 실행해보았는데, swagger에서는 아래와 같이 응답 결과가 반환되지 않았다.

swagger의 응답 결과 모습

이를 해결하기 위해서는 CORS 설정을 해주어야 한다고 한다. 해당 포스팅은 아래의 포스트를 통해 진행하도록 하고, 다운로드는 크롬 브라우저를 이용해 테스트 해보았다.

https://yoons-development-space.tistory.com/88

 

 

 

swagger 대신 크롬 브라우저로 GET 요청을 날리니, 아래와 같이 잘 다운로드 되었다.

크롬 브라우저로 다운로드 된 모습

(파일을 저장해두는 서버와 다운로드 받는 저장소가 같아서, (2)라는 추가적인 이름이 들어간 것일 뿐이다)