스터디/2023_스프링부트

[study] 스프링MVC2 - 10. 파일 업로드

Hotsan 2023. 8. 14. 00:17

김영한님의 스프링 mvc2편 - 벡엔트 웹 개발 활용 기술 을 듣고 정리한 내용입니다.

 

HTML 폼 전송 방식

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

 

 

Content-Type: application/x-www-form-urlencoded 방식으로 전송할 경우
문자열과 함께 첨부파일을 함께 전송해야 되는 경우 문제가 있다.

따라서 문자와 바이너리를 동시에 전송할 땐 multipart/form-data 방식을 사용한다

 

 

폼의 입력 결과로 생성된 HTTP 메시지를 보면 각각의 전송 항목이 구분이 되어있다.

Content-Disposition 이라는 항목별 헤더가 추가되어 있고 부가정보가 존재한다.

 

 

서블릿과 파일 업로드 

@Slf4j
@Controller
@RequestMapping("/servlet/v1")
public class ServletUploadControllerV1 {

    @GetMapping("/upload")
    public String newFile() {
        return "upload-form";
    }

    @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";
    }
}

request.getParts() : multipart/form-data 전송 방식에서 각각 나누어진 부분을 받아서 확인할 수 있다.

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8">
</head>
<body>

<div class="container">

    <div class="py-5 text-center">
        <h2>상품 등록 폼</h2>
    </div>

    <h4 class="mb-3">상품 입력</h4>

    <form th:action method="post" enctype="multipart/form-data">
        <ul>
            <li>상품명 <input type="text" name="itemName"></li>
            <li>파일<input type="file" name="file" ></li>
        </ul>
        <input type="submit"/>
    </form>

</div> <!-- /container -->
</body>
</html>

 

결과 로그

Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryIkmRCsDjBoqIZhWc
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Whale/3.21.192.18 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: http://localhost:8080/servlet/v1/upload
Accept-Encoding: gzip, deflate, br
Accept-Language: ko,en-US;q=0.9,en;q=0.8,ko-KR;q=0.7,ja;q=0.6,de;q=0.5

------WebKitFormBoundaryIkmRCsDjBoqIZhWc
Content-Disposition: form-data; name="itemName"

test
------WebKitFormBoundaryIkmRCsDjBoqIZhWc
Content-Disposition: form-data; name="file"; filename="KakaoTalk_20230805_173811541.jpg"
Content-Type: image/jpeg

 

 

멀티파트 사용 옵션

spring.servlet.multipart.max-file-size=1MB
spring.servlet.multipart.max-request-size=10MB

spring.servlet.multipart.enabled=true (기본 true)

 

spring.servlet.multipart.enabled 옵션을 켜면
스프링의 DispatcherServlet 에서 멀티파트 리졸버( MultipartResolver )를 실행한다.

 

@Slf4j
@Controller
@RequestMapping("/servlet/v2")
public class ServletUploadControllerV2 {

    @Value("${file.dir}")
    private String fileDir;

    @GetMapping("/upload")
    public String newFile() {
        return "upload-form";
    }

    @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 주요 메서드

part.getSubmittedFileName() : 클라이언트가 전달한 파일명
part.getInputStream(): Part의 전송 데이터를 읽을 수 있다.
part.write(...): Part를 통해 전송된 데이터를 저장할 수 있다.

 

결과로그

2023-08-15 21:04:31.263  INFO 292 --- [nio-8080-exec-2] h.u.c.ServletUploadControllerV2          : ==== PART ====
2023-08-15 21:04:31.263  INFO 292 --- [nio-8080-exec-2] h.u.c.ServletUploadControllerV2          : name=itemName
2023-08-15 21:04:31.263  INFO 292 --- [nio-8080-exec-2] h.u.c.ServletUploadControllerV2          : header content-disposition: form-data; name="itemName"
2023-08-15 21:04:31.263  INFO 292 --- [nio-8080-exec-2] h.u.c.ServletUploadControllerV2          : submittedFilename=null
2023-08-15 21:04:31.263  INFO 292 --- [nio-8080-exec-2] h.u.c.ServletUploadControllerV2          : size=4
2023-08-15 21:04:31.264  INFO 292 --- [nio-8080-exec-2] h.u.c.ServletUploadControllerV2          : body=test
2023-08-15 21:04:31.264  INFO 292 --- [nio-8080-exec-2] h.u.c.ServletUploadControllerV2          : ==== PART ====
2023-08-15 21:04:31.264  INFO 292 --- [nio-8080-exec-2] h.u.c.ServletUploadControllerV2          : name=file
2023-08-15 21:04:31.264  INFO 292 --- [nio-8080-exec-2] h.u.c.ServletUploadControllerV2          : header content-disposition: form-data; name="file"; filename="KakaoTalk_20230805_173811541.jpg"
2023-08-15 21:04:31.264  INFO 292 --- [nio-8080-exec-2] h.u.c.ServletUploadControllerV2          : header content-type: image/jpeg
2023-08-15 21:04:31.264  INFO 292 --- [nio-8080-exec-2] h.u.c.ServletUploadControllerV2          : submittedFilename=KakaoTalk_20230805_173811541.jpg
2023-08-15 21:04:31.264  INFO 292 --- [nio-8080-exec-2] h.u.c.ServletUploadControllerV2          : size=318310
Y  �  
[        XYZ       ��     �-mluc          enUS        G o o g l e   I n c .   2 0 1 6�� C 	

 

 

스프링은 MultipartFile 이라는 인터페이스로 멀티파트 파일을 매우 편리하게 지원한다.

@Slf4j
@Controller
@RequestMapping("/spring")
public class SpringUploadController {

    @Value("${file.dir}")
    private String fileDir;

    @GetMapping("/upload")
    public String newFile() {
        return "upload-form";
    }

    @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";
    }
}

@RequestParam MultipartFile file 업로드하는 HTML Form의 name에 맞추어 @RequestParam 을 적용하면 된다.

추가로 @ModelAttribute 에서도 MultipartFile 을 동일하게 사용할 수 있다.

 

MultipartFile 주요 메서드

  • file.getOriginalFilename() : 업로드 파일 명
  • file.transferTo(...) : 파일 저장

 

 

 

 

 

 


https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2/dashboard

 

스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 - 인프런 | 강의

웹 애플리케이션 개발에 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. MVC 2편에서는 MVC 1편의 핵심 원리와 구조 위에 실무 웹 개발에 필요한 모든 활용 기술들을 학습할 수 있

www.inflearn.com