WEB/Spring MVC 2

Spring MVC 2편 파일 업로드

Tony Lim 2021. 11. 6. 10:04
728x90

html 폼 전송 방식

application/x-www-form-urlencoded

가장 기본적인 방법 , form 에 입력한 전송할 항목을 HTTP Body 에 문자로 username=kim&age20 처럼 & 으로 구분한다.

파일을 업로드 하려면 파일은 문자가 아니라 바이너리 데이터를 전송해야 한다. 문자를 전송하는 이 방식으로 파일을 전송하기는 어렵다. 그리고 또 한가지 문제가 더 있는데 , 보통 폼을 전송할 때 파일만 전송하는 것이 아니라는 점이다.

문자와 바이너리를 동시에 전송해야 하는 상황이 발생한다.

 

mutipart/form-data

form tag에 별도로 enctype에 지정을 해줘야한다.

이런 방식으로 헤더 , 바디 가 계속 반복되고 여러개의 다른 스타일의 데이터를 한번에 보낼 수 있게 해준다. 

폼의 일반 데이터는 각 항목별로 문자가 전송되고 , 파일의 경우 파일 이름과 Content-Type 이 추가되고 바이너리 데이터가 전송된다.

 


 

    @PostMapping("/upload")
    public String saveFileV1(HttpServletRequest request) throws ServletException, IOException {
        log.info("request={}", request);

        String itemName = request.getParameter("itemName");
        log.info("itemName={}", itemName);

        Collection<Part> parts = request.getParts();
        log.info("parts={}", parts);

        return "upload-form";
    }

spring.servlet.mutipart.enabled=false 기본이 true이긴한데 false로하면 multipart 지원안하겠다는 뜻이다.

false로 하게되면 tomcat의 request 기본 구현체인 RequestFacade 가 나오게 된다. 

true인 경우에는 DispatcherServlet에서 MutipartResolver를 실행한다.
MutipartResolver는 멀티파트 요청인 경우 서블릿 컨테이너가 전달하는 일반적인 HttpServletRequest 를 MultipartHttpServletRequest 로 변환해서 반환한다.

MutipartHttpServletRequest 는 HttpServletRequest 의 자식 인터페이스이고 멀티파트와 관련된 추가 기능을 제공한다.


 

    @PostMapping("/upload")
    public String saveFileV1(HttpServletRequest request) throws ServletException, IOException {
        log.info("request={}", request);

        String itemName = request.getParameter("itemName");
        log.info("itemName={}", itemName);

        Collection<Part> parts = request.getParts();
        log.info("parts={}", parts);

        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));
            }
            //편의 메서드
            //content-disposition; filename
            log.info("submittedFilename={}", part.getSubmittedFileName());
            log.info("size={}", part.getSize()); //part body size

            //데이터 읽기
            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);
                part.write(fullPath);
            }
        }

        return "upload-form";
    }

Part 클래스에서 알아서 다른 종류데이터 나오면 파싱해서 return 해주는 메소드가 존재한다. 쓰기도 편하다.


스프링은 MutipartFile 이라는 인터페이스로 업로드를 편하게 지원한다.

    @PostMapping("/upload")
    public String saveFile(@RequestParam String itemName,
                           @RequestParam MultipartFile file, HttpServletRequest request) throws IOException {

        log.info("request={}", request);
        log.info("itemName={}", itemName);
        log.info("multipartFile={}", file);

        if (!file.isEmpty()) {
            String fullPath = fileDir + file.getOriginalFilename();
            log.info("파일 저장 fullPath={}", fullPath);
            file.transferTo(new File(fullPath));
        }

        return "upload-form";
    }

내부적으로는 서블릿의 기술들을 쓰는 것이다.

 


파일들을 저장

@PostMapping("/items/new")
public String saveItem(@ModelAttribute ItemForm form, RedirectAttributes redirectAttributes) throws IOException {
    UploadFile attachFile = fileStore.storeFile(form.getAttachFile());
    List<UploadFile> storeImageFiles = fileStore.storeFiles(form.getImageFiles());

    //데이터베이스에 저장
    Item item = new Item();
    item.setItemName(form.getItemName());
    item.setAttachFile(attachFile);
    item.setImageFiles(storeImageFiles);
    itemRepository.save(item);

    redirectAttributes.addAttribute("itemId", item.getId());

    return "redirect:/items/{itemId}";
}

데이터베이스에 저장을 할시에 실제 file, image 자체를 저장하지 않는다. 이것들은 S3같은 별도의 저장소에 보관을 하고 db에는 해당 파일들의 이름및 확장자를 저장하여 S3를 참조 할 수 있도록 한다.

 

파일및 이미지 조회

@GetMapping("/items/{id}")
public String items(@PathVariable Long id, Model model) {
    Item item = itemRepository.findById(id);
    model.addAttribute("item", item);
    return "item-view";
}

현재 a.txt 도 다운로드 안되고 이미지들도 다 액박으로 뜬다.

localhost:8080/images/{UUID.png} 여기에다 get 요청을 했지만 아무것도 없기에 액박으로 뜨는것이다 여기에 해당하는 Controller를 만들어줘야한다.

 

파일및 이미지 다운로드

@ResponseBody
@GetMapping("/images/{filename}")
public Resource downloadImage(@PathVariable String filename) throws MalformedURLException {
    return new UrlResource("file:" + fileStore.getFullPath(filename));
}

url 경로에 접근해서 stream으로 반환을 한다.

  @GetMapping("/attach/{itemId}")
    public ResponseEntity<Resource> downloadAttach(@PathVariable Long itemId) throws MalformedURLException {
        Item item = itemRepository.findById(itemId);
        String storeFileName = item.getAttachFile().getStoreFileName();
        String uploadFileName = item.getAttachFile().getUploadFileName();

        UrlResource resource = new UrlResource("file:" + fileStore.getFullPath(storeFileName));

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

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

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

다운로드를 하게 하려면 꼭 header에 contentDisposition 을 넣어줘야한다. 안그러면 해당 url에 담겨있는 정보를 그냥 브라우저에 뿌리게 된다.

encodedUploadFileName = 한글 같은것들은 깨질수 있기 때문에 해주는 작업이다. 브라우저 마다 다를 수 있다.

uploadFileName = 고객이 업로드 한 파일명 (같을 수 있음)

storeFileName = 겹치지 않게 하기위해서 존재함

 

728x90