Spring MVC : 파일 업로드 및 다운로드 구현해보기

    이 포스팅은 인프런 김영한님의 강의를 복습하며 정리한 내용입니다.

     

    HTML FORM 전송 방식


    • application/x-www-urlencoded
    • multipart/form-data

    HTML FORM 전송 방식은 위와 같이 두 가지(https://ojt90902.tistory.com/651)가 있다. 이름에서부터 유추가 가능하겠지만 multipart/form-data는 뭔가 여러 형태의 데이터를 전송해 줄 것이라는 것이 유추된다. 그와 대조적으로 application/x-www-urlencoded는 한 형태의 데이터를 전송해줄 것이라는 것이 유추된다.

     

    application/x-www-urlencoded 전송 방식

    • application/x-www-urlencoded는 HTML 폼 데이터를 전송하는 가장 기본적인 방법이다.
    • 전송하고자 하는 데이터를 String 형태로 전송해준다. 
    • FORM 태그에 enctype 옵션이 없으면, 웹 브라우저는 자동으로 application/x-www-urlencoded를 헤더에 추가한다.
    • 전송할 데이터를 쿼리 파라미터 형식으로 HTML Body에 넣어 전송해준다.
      • username=kim&age=20

    위의 이미지에서 볼 수 있듯이 application/x-www-urlencoded는 input-type = "text"에서 볼 수 있듯이 문자를 전송하는 방식이다. 이 방식으로는 파일을 전송할 수는 없다. 

     

    multipart/form-data 전송 방식

    • multipart/form-data는 여러 형태의 자료(문자 / 바이너리)를 전송하기 위한 전송형태다. 여러 형태를 전송하기 때문에 multipart라는 이름을 가진다. 
    • multipart/form-data를 사용하기 위해서는 Form 태그에 enctype="multipart/form-data"를 입력해줘야한다. 

    multipart/form-data가 생성한 HTTP 메세지를 보면 application/x-www-urlencoded와 다르다는 것을 알 수 있다. 먼저 Content-type에 boundary라는 값이 보인다. 데이터는 이렇게 boundary로 나누어져서 전송되는 것을 알 수 있다.

    전송되는 데이터들은 Boundary로 나누어지고, 이것을 'Part'라고 한다. 각 Part는 각 Part에 맞는 Header 정보와 Body값을 가진 채로 클라이언트에서 서버로 전송된다. 파일을 업로드하기 위해서는 'Part' 단위로 전송되는 이 값을 처리해주어야 한다. 

     

    MultiPart/form-data 사용 시, 서블릿은 바뀐다.


    multipart/form-data 사용 O → Standard
    multipart/form-data 사용 X → Facade

    multipart/form-data를 사용하지 않으면 이 때 HttpServletRequest는 RequestFacade 형태로 사용이 된다. 그렇지만 multipart/form-data를 사용하게 되면 RequestFacade는 standardMultipartHttpServletRequest가 전달된다. 이것은 스프링의 Dispatcher Servlet이 중간에서 처리를 해주고 있는 것이다.

    multipart/form-data = true인 경우, 스프링은 Dispatcher Servlet에서 MultiPartResolver를 실행한다. MultiPartResolver는 멀티파트 요청인 경우, 서블릿 컨테이너가 전달하는 일반전인 HttpServletRequest인 RequestFacade를 MultiPartHttpservletRequest 형태로 변환해서 넘겨준다. 

     

     

    서버에서 Part 값 받기(파일 업로드)


    • 서블릿 기술 : HttpServletRequest에서 getPart로 받기
    • 스프링 기술 : MultiPartFile로 받기

    Part값을 받는 방법은 위에서 볼 수 있듯이 크게 두 가지 방법이 있다. 가장 원초적인 방법으로는 HttpServletRequest에서 getPart로 전송된 Part를 Collection 형태로 받을 수 있다. 또 한 가지 방법은 Parameter를 Binding하는 시점에 스프링이 지원하는 MultiPartFile로 받는 방법이다. 

    MultiPartFile로 받기 위해서는 @ModelAttibute를 이용하면 된다. HTML FORM 데이터는 모든 데이터를 요청 파라미터 형식으로 전달하기 때문이다. 이런 이유 때문에 MultiPartFile을 매개변수에서 받을 때는 @ModelAttribute도 생략할 수 있다. Spring은 @RequestBody가 아닌 경우, 자동적으로 기본형 자료에는 @RequestParam가, 나머지 자료에는 @ModelAttribute가 적용되기 때문이다. 

     

    파일 업로드와 관련있는 스프링의 설정 정보


    spring.servlet.multipart.max-file-size=1MB
    spring.servlet.multipart.max-request-size=10MB
    spring.servlet.multipart.enabled=true
    • max-file-size : 파일 하나 당 올릴 수 있는 최대 파일 용량
    • max-request-size : 한번에 올릴 수 있는 전체 최대 파일 용량 
    • multipart.enabled : multipart/form-data를 사용할 수 있는지를 정한다.

    application.properties를 통해서 파일 업로드와 관련된 스프링 설정을 처리할 수 있다. 위에 간략히 내용을 정리했다. 

     

    업로드 파일 저장 시 주의사항


    업로드 된 파일을 저장할 때 주의할 사항이 있다. 서버는 하나지만, 클라이언트는 여러 군데에서 값을 올려준다. 서로 다른 클라이언트가 2.jpg라는 값을 각각 올려주면 별다른 조치를 하지 않으면 값은 소실될 수 밖에 없다. 이것을 해결하기 위해서 UUID를 이용해 파일을 저장하는 것을 추천한다. 

    서버에 저장될 파일 이름을 UUID로 만들어준다. 그리고 그 UUID로 만든 파일명이 업로드 파일명을 맵핑해준다. 위처럼 객체 형태로 파일명을 바인딩해서, 바인딩 된 파일명만 DB에 넣어주는 방식도 있다. 그리고 파일이 필요할 때는, 해당 객체를 DB에서 찾아와서 여기서 이름을 참조해서 값을 내려주는 것이다. 

    public UploadFile saveFile(MultipartFile file) throws IOException {
        
        // 예외체크
        if (!StringUtils.hasText(file.getOriginalFilename())) {
            return null;
        }
    
        // 성공 로직
        String originalFilename = file.getOriginalFilename();
        String storeFileName = createStoreFileName(originalFilename);
    
        String fullPath = findFinalPath(storeFileName);
        file.transferTo(new File(fullPath));
    
        return new UploadFile(storeFileName, originalFilename);
    
    }
    
    public String findFinalPath(String storeFileName) {
        return fileDir + storeFileName;
    }
    
    
    public String createStoreFileName(String originalFilename) {
        String uuid = UUID.randomUUID().toString();
        String ext = extractExt(originalFilename);
        return uuid + "." + ext;
    }
    
    public String extractExt(String originalFilename) {
        int posi = originalFilename.lastIndexOf(".");
        return originalFilename.substring(posi + 1);
    }
    
    @Data
    @AllArgsConstructor
    public class UploadFile {
    
        private String storeFileName;
        private String uploadFileName;
    
    }

    위는 업로드된 파일을 UUID 형태를 가진 파일명으로 치환하고, 그 이름으로 서버에 저장해준다. 그리고 서버에 저장한 후의 동작 결과물로 UploadFile이라는 기존 파일명, 저장된 파일명이 바인딩 된 객체를 전달해주는 형태를 가지는 식으로 동작한다. 

     

    파일 저장하기


    • 서블릿 기술 : part.getWrite()로 저장
    • 스프링 기술 : MultiPartfile.transferTo()로 저장

    다른 형태의 기술이기 때문에 당연히 업로드 된 파일을 저장하는 것도 다르다. 서블릿 기술은 경로를 잡고 part를 getWrite()를 해주면 된다. MultiPartfile은 transferTo()로 저장을 할 수 있게 된다. 

     

    파일 다운로드(웹 랜더링 / PC 다운로드)


    파일 다운로드는 크게 두 가지로 나누어진다. 하나는 실제로 클라이언트가 요청해서 파일을 PC로 받을 수 있는 형태다. 또 다른 하나는 우리가 웹에 랜더링 된 이미지를 볼 수 있는 것처럼, 단순히 웹에서만 볼 수 있는 방법이 있다. 

    기본적으로 자바 + 스프링에서 파일을 내려주려면 Resource 형태의 값을 내려주면 된다. 이 때, 주로 사용하는 방법은 UrlResource, ClassPathResource이다. UrlResource는 Prefix로 프로토콜을 명시해주고 해당 리소스의 위치를 알려주는 URL 방식을 통해 리소스를 내려준다. 

    Content-Disposition : attachment; filename="1.png"

    파일을 실제로 PC에 다운받으려면 응답에 특정 Header를 포함시켜야한다.  만약, PC에서도 파일을 직접 다운 받을 수 있게 하고 싶다면 Content-Disposition : attachment; filename="파일명" 형식의 헤더를 반드시 응답에 포함시켜서 내려줘야한다. 여기서 지정된 파일명은 실제 PC에 다운로드 받아질 때의 파일명이 된다. 웹이 이미지를 렌더링하고 싶다면, 위 헤더를 포함하지 않고 응답을 내려주기만 하면 된다. 

     

    코드 실습


     HttpServletRequest를 이용해 업로드 해보기

    @PostMapping("/upload")
    public String saveFileV2(HttpServletRequest request) throws ServletException, IOException {
        String itemName = request.getParameter("itemName");
    
        Collection<Part> parts = request.getParts();
        for (Part part : parts) {
            log.info("==== PART ====");
            log.info("name = {}", part.getName());
    
            Collection<String> headerNames = part.getHeaderNames();
            for (String headerName : headerNames) {
                log.info("header {}: {}", headerName, part.getHeader(headerName));
            }
    
            log.info("submittedFilename ={}", part.getSubmittedFileName());
            log.info("size = {}", part.getSize()); // body 사이즈 읽기
    
            // 파일을 String으로 인코딩해서 로그 출력
            InputStream inputStream = part.getInputStream();
            String body = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
            log.info("body = {}", body); 
    
            // 파일에 저장하기
            if (StringUtils.hasText(part.getSubmittedFileName())) {
                String fullPath = fileDir + part.getSubmittedFileName(); 
                log.info("파일 저장 fullPath = {}", fullPath);
    
            }
        }
    • HttpServletRequest 형식으로 Part 데이터를 전달받는다.
    • 이 때, Servlet은 MultipartHttpServletRequest다. 
    • MultiPartServletRequest는 getPart()를 통해 part를 제공한다. 그리고 part와 관련된 다양한 편의 메서드를 제공한다.
    • 파일을 저장하는 것은 part.write()를 이용해서 저장한다. 이 때, 저장될 위치를 표기해주면 된다. 

     

    Spring의 MultiPartFile로 저장해보기

    @PostMapping("/upload") 
    public String saveFile(@RequestParam String itemName,
                           @RequestParam MultipartFile file,
                           HttpServletRequest request) throws IOException {
    
        if (!file.isEmpty()) {
            String fullPath = fileDir + file.getOriginalFilename();
            file.transferTo(new File(fullPath)); // 이렇게 하면 파일을 fullpath 경로에다가 저장을 해준다.
        }
    
    
        return "upload-form";
    }
    • 스프링은 파라미터 바인딩 시점에 MultiPartFile로 File을 바인딩해준다.
    • @RequestParam, @ModelAttribute를 직접 작성해도 되고 생략해도 된다. 
    • MultiPartFile은 transferTo(...) 메서드를 이용해 저장한다. 

     

    MultiPartFile의 다운로드 (웹 이미지 렌더링)

    @ResponseBody
    @GetMapping("/images/{storeFileName}")
    public Resource imageDownLoad(@PathVariable String storeFileName) throws MalformedURLException {
        return new UrlResource("file:" + fileStore.findFinalPath(storeFileName));
    }
    • 스프링은 Resource 형식의 데이터를 내려주는 형식으로 파일의 다운로드를 실행해준다.
    • HTML Body에 Resource 데이터를 넣어서 내려주면 된다.
    • UrlResource는 "file : 절대경로" 형식으로 파일의 위치를 지정해서 파일이 내려올 수 있게 해준다. 

     

    MultiPartFile의 다운로드 (PC에 다운로드)

    @GetMapping("/attach/{itemId}")
    public ResponseEntity<Resource> attachFileDownload(@PathVariable Long itemId, HttpServletResponse response) throws MalformedURLException {
        log.info("headfdfdf");
    
        Item item = itemRepository.findById(itemId);
        UploadFile attachFile = item.getAttachFile();
        String storeFileName = attachFile.getStoreFileName();
        log.info("storeFileName : {}", storeFileName);
    
        UrlResource urlResource = new UrlResource("file:" + fileStore.findFinalPath(storeFileName));
    
    
        String uploadFileName = attachFile.getUploadFileName();
        String encode = UriUtils.encode(uploadFileName, StandardCharsets.UTF_8);
    
        String contentDispostion = "attachment; filename=\"" + encode + "\"";
    
        return ResponseEntity.ok()
                .header(HttpHeaders.CONTENT_DISPOSITION, contentDispostion)
                .body(urlResource);
    }
    • HTML Body에 동일하게 Resource를 내려준다.
    • Resource는 UrlResource 형식으로 내려줬고, file: 절대경로 형식으로 설정해서 내려주었다. 
    • 이 때, CONTENT-DISPOSTION : attachment; filename = "파일명" 헤더를 넣어주어야 한다. 이 때, 파일명은 다운로드 될 때의 파일이름이 된다.

    댓글

    Designed by JB FACTORY