서블릿 Part와 MultipartFile로 파일 업로드 처리하기
서블릿 Part로 multipart 요청을 직접 처리해 보고, 스프링 MultipartFile이 이를 얼마나 단순하게 만드는지 비교한다.
시리즈 · Spring9 / 14
- SOLID 원칙과 OCP·DIP를 지키는 방법
- 의존성 주입(DI)과 스프링 컨테이너
- 쓰레드 풀과 싱글톤 서블릿의 동시성 문제
- FrontController 패턴과 핸들러 어댑터로 스프링 MVC 구조 이해하기
- @ModelAttribute와 PRG 패턴으로 폼 요청 처리하기
- System.out 대신 SLF4J로 로그 남기기
- 세션 직접 만들어 보기와 HttpSession
- 스프링 인터셉터와 예외 발생 시 오류 페이지 흐름
- 서블릿 Part와 MultipartFile로 파일 업로드 처리하기
- JDBC로 CRUD 구현하기와 커넥션 풀, DataSource
- JDBC 트랜잭션에서 스프링 트랜잭션 동기화와 트랜잭션 AOP까지
- JPA 변경 감지(dirty checking)와 병합(merge)
- 서블릿 컨테이너 초기화와 스프링 컨테이너 등록 과정
- 싱글톤 빈의 동시성 문제와 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이 있으면 그 이름이 저장 파일명으로 미리 채워진다.
참고
- 김영한, 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 (인프런)
- Content-Disposition - MDN