개발환경
IDE - Intelli j
Build Tool - maven
FrameWork - springboot 2.4.0
OS - mac
Java11

오늘 공부할 주제는 파일 업로드 & 다운로드 이다.
다양한 서비스에서 파일을 업로드 할 수 있는 기능을 제공해주는데,
당장 Velog 에서도 이미지 파일을 포스팅에 삽입할 수 있다.
어떤 원리로 파일이 업로드 되는지 알아보고 나아가서 다운로드 하는 방법도 다뤄보도록 하자.

간단하게, 파일을 업로드 할 수 있는 view 를 먼저 만들었다.
(참고 thymeleaf 사용)

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>FileUploadForm</title>
</head>
<body>
<div th:if="${message}">
    <h2 th:text="${message}">message</h2>
</div>
<form action="#" enctype="multipart/form-data" th:action="@{/fileUpload}" method="post">
    File : <input type="file" name="file">
    <input type="submit" value="upload">
</form>
</body>
</html>

위 코드에서 우선 주목해야할 부분은 폼 데이터로 업로드할 파일을 전송하는 부분이다

<form action="#" enctype="multipart/form-data" th:action="@{/fileUpload}" method="post">
    File : <input type="file" name="file">
    <input type="submit" value="upload">
</form>

바로 이부분인데 코드를 조금 뜯어보자
form 태그의 enctype 속성은 폼 데이터(form data)가 서버로 제출될 때 해당 데이터가 인코딩되는 방법을 의미한다.

인코딩 방법은 다음 3개의 방법이 존재한다
1. application/x-www-form-urlencoded (the default)
2. multipart/form-data
3. text/plain

그 중 multipart/form-data 방식으로 데이터를 인코딩해 전송하면 파트를 나누어 다양한 타입의 데이터들을 보낼 수 있게된다. 디폴트로 사용되는 1번 방식은 문자열로만 데이터를 보내기 때문에, 파일을 보낼 수 없지만 multipart/form-data 를 사용하면 바이너리 데이터를 보낼 수 있기 때문에 파일을 전송할 수 있는 것이다.


다음으로 해당 뷰를 보여주는 핸들러와 폼데이터를 받아서 처리할 핸들러를 구현한 코드를 살펴보자

우선, 간단하게 뷰를 보여주는 핸들러부터 살펴보자

    @GetMapping("/fileUploadForm")
    public String showFileUploadForm(){
        return "fileUploadForm";
    }

컨트롤러 안에 @GetMapping 을 사용하여 해당 url 로 요청을 하면 아까 작성한 뷰를 넘겨주는 핸들러를 만들었다.


다음으로 post요청을 처리할 핸들러 이다.

  @PostMapping("/fileUpload")
    public String fileUpload(@RequestParam MultipartFile file, RedirectAttributes redirectAttributes) throws IOException {

        // file upload to system
        File converFile = new File("파일을저장할 폴더 path" + file.getOriginalFilename());
        file.transferTo(converFile);

        // todo : file meta data should be saved in DB
        
        String message = file.getOriginalFilename()+ " is saved in server db";
        redirectAttributes.addFlashAttribute("message", message);
        return "redirect:/fileUploadForm";
    }

@RequestParam 을 활용하여 multipartfile 을 받고 transferTo() 를 이용해 파일로 변환하여 시스템에 저장하는 형식으로 파일 업로드를 구현하였다.
일반적으로, DB에 파일을 직접 저장하게되면 파일을 CRUD 할 때 과도한 부하가 걸리기 때문에 파일을 따로 관리하되 DB에는 파일에 대한 메타데이터만 저장하는 식으로 운영된다고 한다.

그리고, RedirectAttributes 의 FlashAttribute 을 활용해 업로드 메세지를 보여주도록 구현하였는데 FlashAttribute 의 경우 세션을 활용해 리다이렉트시 필요한 데이터를 넘기고 리다이렉트가 되어 데이터를 쓰고나면 세션에서 날려준다. 이 작업을 통해 파일을 업로드 하고 나면

<div th:if="${message}">
    <h2 th:text="${message}">message</h2>
</div>

위에서 작성한 타임리프 코드에 의해 업로드 완료 메세지를 사용자에게 보여줄 수 있다.


다음으로 파일 다운로드는 어떻게 이루어지는지 알아보자
간단한 실습을 위해 url 패턴에 파일 이름을 주고 해당 파일을 다운받을 수 있는 핸들러를 작성하였다

 @GetMapping("/fileDownload/{filename}")
    public ResponseEntity<Resource> fileDownload(@PathVariable String filename) throws IOException {
        Resource resource = resourceLoader.getResource("classpath:/uploads/" + filename);
        File file = resource.getFile();

        Tika tika = new Tika();
        String mediaType = tika.detect(file);

        return ResponseEntity.ok()
                .header(HttpHeaders.CONTENT_DISPOSITION, "attachement; filename=\"" + resource.getFilename() +"\"")
                .header(HttpHeaders.CONTENT_TYPE, mediaType)
                .header(HttpHeaders.CONTENT_LENGTH, file.length()+"")
                .body(resource);
    }

ResponsEntiy 에 관해 간단히 설명하자면 응답상태코드와 응답header 정보 그리고 응답body 를 같이 리턴할 수 있는 타입이다.
코드를 살짝 뜯어 보자

@GetMapping("/fileDownload/{filename}")
    public ResponseEntity<Resource> fileDownload(@PathVariable String filename) throws IOException {
        Resource resource = resourceLoader.getResource("classpath:/uploads/" + filename);
        File file = resource.getFile();

@PathVariable 을 통해 원하는 파일의 이름을 받아 스프링이 제공하는 resourceLoader 를 활용하여 클래스패스기준으로/uploads/원하는파일이름 리소스를 받아와 파일로 변환하였다.
org.springframework.core.io 가 제공하는 Resource 와 ResourceLoader 에 대해서는 추후에 포스팅을 해보도록 하겠다.

Tika tika = new Tika();
String mediaType = tika.detect(file);

그리고 나는 Tika 라는 라이브러리를 사용하여 해당 파일은 mediaType을 찾도록 구현했다.

 return ResponseEntity.ok()
                .header(HttpHeaders.CONTENT_DISPOSITION, "attachement; filename=\"" + resource.getFilename() +"\"")
                .header(HttpHeaders.CONTENT_TYPE, mediaType)
                .header(HttpHeaders.CONTENT_LENGTH, file.length()+"")
                .body(resource);

그리고 응답 데이터를 다음과 같이 작성하였는데
HttpHeaders.CONTENT_DISPOSITION, "attachement; ~~ " 이부분에 대해서 조금 설명하자면
content-disposition 은 응답본문에 오는 컨테츠의 기질/성향을 알려주는 속성이다. attachement 와 함께 filename 을 주면 응답본문에 오는 파일을 다운로드 하게 된다.


처음 스프링을 접하는 분들도 쉽게 이해할 수 있도록 작성하려 했는데 선수지식이 필요한 부분이 많아서 모든 걸 설명하긴 쉽지 않은 것 같다. 그래도 공부하기 쉽도록 키워드를 몇 개 정리해보도록 하겠다


HTTP 관련 키워드

1. MultipartFile

2. content-disposition


Spring 관련 키워드

1. Resource (org.springframework.core.io)

2. ResourceLoader (org.springframework.core.io)

3. ResponseEntity

4. RedirectAttribute

5. FlashAttribute

6. @RequestMapping [@GetMapping, @PostMapping...]

7. @PathVariable


전체코드

import org.apache.tika.Tika;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import java.io.File;
import java.io.IOException;

@Controller
public class fileController {

    @Autowired
    ResourceLoader resourceLoader;

    @GetMapping("/fileUploadForm")
    public String showFileUploadForm(){
        return "fileUploadForm";
    }

    @PostMapping("/fileUpload")
    public String fileUpload(@RequestParam MultipartFile file, RedirectAttributes redirectAttributes) throws IOException {

        // file upload to system
        File converFile = new File("파일을저장할 폴더 path/" + file.getOriginalFilename());
        file.transferTo(converFile);

        // todo : file meta data should be saved in DB
        String message = file.getOriginalFilename()+ " is saved in server db";
        redirectAttributes.addFlashAttribute("message", message);
        return "redirect:/fileUploadForm";
    }

    @GetMapping("/fileDownload/{filename}")
    public ResponseEntity<Resource> fileDownload(@PathVariable String filename) throws IOException {
        Resource resource = resourceLoader.getResource("classpath:/uploads/" + filename);
        File file = resource.getFile();

        Tika tika = new Tika();
        String mediaType = tika.detect(file);

        return ResponseEntity.ok()
                .header(HttpHeaders.CONTENT_DISPOSITION, "attachement; filename=\"" + resource.getFilename() +"\"")
                .header(HttpHeaders.CONTENT_TYPE, mediaType)
                .header(HttpHeaders.CONTENT_LENGTH, file.length()+"")
                .body(resource);
    }
}

0개의 댓글