• spring
  • spring-mvc
  • servlet

서블릿 Part와 MultipartFile로 파일 업로드 처리하기

서블릿 Part로 multipart 요청을 직접 처리해 보고, 스프링 MultipartFile이 이를 얼마나 단순하게 만드는지 비교한다.

시리즈 · Spring9 / 14
  1. SOLID 원칙과 OCP·DIP를 지키는 방법
  2. 의존성 주입(DI)과 스프링 컨테이너
  3. 쓰레드 풀과 싱글톤 서블릿의 동시성 문제
  4. FrontController 패턴과 핸들러 어댑터로 스프링 MVC 구조 이해하기
  5. @ModelAttribute와 PRG 패턴으로 폼 요청 처리하기
  6. System.out 대신 SLF4J로 로그 남기기
  7. 세션 직접 만들어 보기와 HttpSession
  8. 스프링 인터셉터와 예외 발생 시 오류 페이지 흐름
  9. 서블릿 Part와 MultipartFile로 파일 업로드 처리하기
  10. JDBC로 CRUD 구현하기와 커넥션 풀, DataSource
  11. JDBC 트랜잭션에서 스프링 트랜잭션 동기화와 트랜잭션 AOP까지
  12. JPA 변경 감지(dirty checking)와 병합(merge)
  13. 서블릿 컨테이너 초기화와 스프링 컨테이너 등록 과정
  14. 싱글톤 빈의 동시성 문제와 ThreadLocal

파일 업로드는 HTTP multipart/form-data 요청으로 전송된다. 서블릿이 제공하는 Part로 직접 처리하는 방법과 스프링의 MultipartFile을 비교한다.

서블릿 Part로 처리

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

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

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

        Collection<Part> parts = request.getParts();

        for (Part part : parts) {
            log.info("name={}", part.getName());
            for (String headerName : part.getHeaderNames()) {
                log.info("header {}: {}", headerName, part.getHeader(headerName));
            }

            // 편의 메서드: Content-Disposition의 filename
            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);

            // 파일 저장: 파일이 아닌 part는 getSubmittedFileName()이 null이다.
            if (StringUtils.hasText(part.getSubmittedFileName())) {
                String fullPath = fileDir + part.getSubmittedFileName();
                log.info("파일 저장 fullPath={}", fullPath);
                part.write(fullPath);
            }
        }

        return "upload-form";
    }
}

multipart 요청은 폼 필드와 파일이 각각 하나의 part로 나뉘어 전송된다. 서블릿 방식에서는 HttpServletRequest에서 part를 모두 꺼내 순회하면서, 파일인지 아닌지 직접 구분하고 InputStream으로 데이터를 읽어야 한다.

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) throws IOException {
        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.getOriginalFilename(): 업로드한 파일명
  • file.transferTo(): 파일 저장

관련 설정

# 파일 하나의 최대 크기
spring.servlet.multipart.max-file-size=1MB
# 요청 하나에 포함된 전체 파일 크기의 합
spring.servlet.multipart.max-request-size=10MB
# multipart 처리 비활성화 (기본값 true)
spring.servlet.multipart.enabled=false

Content-Disposition

업로드한 파일을 다시 내려줄 때는 응답의 Content-Disposition 헤더로 브라우저의 동작을 정한다.

Content-Disposition: inline
Content-Disposition: attachment
Content-Disposition: attachment; filename="filename.jpg"
  • inline: 브라우저 안에서 내용을 바로 표시한다.
  • attachment: 다운로드하도록 저장 창을 띄운다. filename이 있으면 그 이름이 저장 파일명으로 미리 채워진다.

참고