본문 바로가기

Spring/MVC

[Spring MVC] 파일 업로드

이번 장에서는 서블릿과 스프링의 파일 업로드 기능에 대해서 알아본다.
모든 코드는 깃허브(링크) 에 올려두었다.


파일 업로드

HTML Form 전송 방식은 아래와 같은 두 가지 방식이 있다.

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

application/x-www-form-urlencoded

application/x-www-form-urlencoded은 가장 기본적으로 HTML 폼 데이터를 서버로 전송하기 위해 사용되는 방법이다.
Form 태그에 별도의 enctype 옵션이 없으면 브라우저는 요청 HTTP 메시지의 헤더에 컨텐츠 타입(Content-Type: application/x-www-form-urlencoded)을 추가한다.
HTML 폼에 입력되어 전송될 항목을 HTTP Body에 문자로 username=roy&age=30과 같이 &으로 구분하여 전송한다.

하지만 파일을 업로드하기 위해서는 일반 문자열이 아닌 바이너리(binary) 데이터로 전송해야 한다.
문자열을 전송하기 위한 x-www-form-urlencoded와 같은 방식으로는 파일을 전송하기 쉽지 않으며 폼으로 데이터를 전송할 때 파일만 전송되는 것이 아니라 추가 데이터까지 전송되기 때문에 파일 업로드에 무리가 있다.
결국 일반적으로 사용되는 문자열과 바이너리를 동시에 전송하기 위해서 HTTPmultipart/form-data라는 전송 방식을 사용한다.


multipart/form-data

multipart/form-data(이하 multipart)를 사용하기 위해서는 Form 태그에 enctype="multipart/form-data"로 지정해야 한다.
multipart의 경우 이름에서 알 수 있듯이 종류가 다른 여러가지의 파일과 폼 데이터를 전송할 수 있다.
HTTP 메시지를 확인해보면 각각의 전송 항목이 구분되어 있으며 Content-Disposition이라는 헤더와 함께 부가 정보가 있다.
폼의 일반적인 데이터는 문자로 전송되며 파일의 경우에는 파일명과 함께 Content-Type이 추가되어 바이너리 데이터가 전송된다.
multipart의 경우 application/x-www-form-urlencoded와 비교해서 구조가 복잡하고 각각의 부분(part)으로 나누어져 있다.

결국 multipart는 각각의 항목을 구분해서 한번에 전송하는 기능이다.


서블릿 파일 업로드

스프링의 파일 업로드 기능을 알아보기 이전에 서블릿의 파일 업로드 기능을 알아본다.

옵션

파일 업로드 관련된 옵션에 대해서 알아본다.

application.properties

logging.level.org.apache.coyote.http11=debug
spring.servlet.multipart.max-file-size=100MB
spring.servlet.multipart.max-request-size=1000MB

spring.servlet.multipart.enabled=false
file.dir=업로드 경로
  • logging.level.org.apache.coyote.http11: HTTP 요청 메시지를 확인하기 위해서 사용되는 옵션이다.
    상당히 많은 양의 로그가 생성되므로 개발 시점에 디버깅 및 학습용도로만 사용하고 프로덕션 환경에서는 사용하지 않아야 한다.
  • spring.servlet.multipart.max-file-size: 업로드하는 파일 하나의 최대 사이즈 크기를 제한한다.
    제한된 사이즈를 초과하는 경우 SizeLimitExceededException이 발생한다.
  • spring.servlet.multipart.max-request-size: 요청 한 번에 최대로 전송할 수 있는 파일 용량의 합을 의미한다.
    제한된 사이즈를 초과하는 경우 SizeLimitExceededException이 발생한다.
  • spring.servlet.multipart.enabled: 멀티파트 데이터 처리 유무를 의미한다.
    기본값은 true이며 해당 옵션이 false가 되는 경우 스프링은 서블릿 컨테이너에게 멀티파트 데이터를 처리하지 않도록 설정하기 때문에 서블릿은 멀티파트 관련 처리를 할 수 없다.
  • file.dir: 업로드되는 파일이 저장될 위치를 의미하며 모든 디렉토리는 파일이 저장되기 이전에 생성되어 있어야 한다.

컨트롤러

@Slf4j
@Controller
@RequestMapping("/servlet")
public class ServletUploadController {
    @Value("${file.dir}")
    private String fileDir;

    @PostMapping("/upload")
    public String saveFile(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);

        int index = 0;
        for (Part part : parts) {
            index++;
            log.info("===== PART - {} =====", index);
            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());

            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.getSubmittedFileName(): 클라이언트가 전달한 파일명을 의미한다.
  • part.getInputStream(): Part를 통해 전송된 데이터를 읽을 때 사용한다.
  • part.write(): Part를 통해 전송된 데이터를 저장할 때 사용한다.

파일을 업로드하면서 출력된 결과는 아래와 같다.

request = org.springframework.web.multipart.support.StandardMultipartHttpServletRequest@119d4fb
itemName = aaa
parts = [org.apache.catalina.core.ApplicationPart@20c72688, org.apache.catalina.core.ApplicationPart@4a61f377]
===== PART - 1 =====
name = itemName
header content-disposition: form-data; name="itemName"
submittedFileName = null
size = 3
body = aaa
===== PART - 2 =====
name = file
header content-disposition: form-data; name="file"; filename="logo.png"
header content-type: image/png
submittedFileName = logo.png
size = 4780
body = �PNG...

스프링의 파일 업로드

스프링의 MultipartFile 기능을 사용하면 서블릿을 통한 파일 업로드보다 편하게 개발할 수 있다.
물론 스프링의 MultipartFile도 자체적으로 파일을 업로드 하는 것이 아니라 서블릿 기능을 사용하기 쉽게 해주어 개발 생산성을 높인 것이다.

@Slf4j
@Controller
@RequestMapping("/spring")
public class SpringUploadController {
    @Value("${file.dir}")
    private String fileDir;
    // ...
    @PostMapping("/upload")
    public String saveFile(@RequestParam String itemName, 
                           @RequestParam MultipartFile file, 
                           HttpServletRequest request) throws IOException {
        if (!file.isEmpty()) {
            String fullPath = String.format("%s%s", fileDir, file.getOriginalFilename());
            log.info("fullPath = {}", fullPath);
            file.transferTo(new File(fullPath));
        }
        return "upload-form";
    }
}

서블릿의 파일 업로드 기능과 비교해보면 코드가 상당히 간략해진 것을 알 수 있다.


파일 업로드 참고

실무에서 파일 업로드를 구현하기 위해서 참고할만한 사항들을 정리해본다.

  • 클라이언트가 전달한 파일명(이하 A)과 서버에서 생성한 파일명(이하 B) 모두 서버에 저장하고 있어야 한다.
    A는 추후 파일을 클라이언트에게 제공할 때 사용해야 하며 B는 중복되는 파일명이 발생하지 않게 하기 위해서 서버에서 관리해야 한다.
    일반적으로 중복되지 않는 파일명을 만들기 위해 UUID를 사용한다.
  • MultipartFile(멀티파트)는 @ModelAttribute에서 사용할 수 있다.
  • 클라이언트가 브라우저를 통해 파일에 접근하면 일반적으로 파일이 다운로드 되는 것이 아니라 브라우저를 통해 열리게 된다.
    만약 다운로드 되도록 구현하고 싶다면 Content-Disposition 헤더에 attachment; filename="업로드 파일명"값을 추가해야 한다.
  • 다중 파일을 업로드 하기 위해서는 input 태그에 multiple="multiple" 속성을 활성화 해야 한다.

참고한 강의:

참고한 문서:

'Spring > MVC' 카테고리의 다른 글

[Spring MVC] Formatter  (0) 2022.06.03
[Spring MVC] Type Converter  (0) 2022.06.03
[Spring MVC] API 예외처리 - 2  (0) 2022.06.02
[Spring MVC] API 예외처리 - 1  (0) 2022.06.02
[Spring MVC] 스프링 부트 - 오류 페이지  (0) 2022.06.02