728x90
1. 파일 업로드 소개
- form 전송 방식 2가지
- application/x-www-form-urlencoded
- multipart/form-data
- application/x-www-form-urlencoded
- 가장 기본적인 방법
- Form tag에 별도의 enctype 옵션이 없는 경우 web browser 역할
- content-type: application/x-www-form-urlencoded를 추가
- form에 입력한 전송할 항목을 HTTP Body에 문자로 & 로 구분해서 전송
- multipart/form-data
- file 업로드시 application/x-www-form-urlencoded의 방식의 문제점
- binary 데이터 전송이 필요한데 문자 전송 방식이기 때문에 부적합
- 실제로 파일만 전송하는 경우가 드물고 문자와 바이너리 파일을 복합적으로 보내는 경우가 대다수
- 문자와 binary file을 동시에 전송하기 위해서 multipart/form-data 전송 방식을 제공
- file 업로드시 application/x-www-form-urlencoded의 방식의 문제점
- multipart/form-data 방식
- form tag에 enctype="multipart/form-data" 작성
- 실제 전송되는 http 메시지 생김새
- boundary=------random값 으로 구분
- 각 boundary마다 해당 값에 맞는 헤더 타입과 바디값을 가진다.
- 전송 HTTP message의 핵심은 Part 라는 부분으로 나누어져서 해당 타입에 맞게 전송한다는 것
2. 서블릿과 파일 업로드1 - 서버로 파일 보내기
- 파일 보낼 form과 controller 생성
@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();// 중요. parts가 httpMessage에 각각의 부분을 의미하는 것
log.info("parts={}", parts);
return "upload-form";
}
}
- form 형식을 encype = "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>
- console 창에 request 찍어주기
# applications.properties
# http message를 console 창에서 볼 수 있다.
logging.level.org.apache.coyote.http11=debug
- 결과
- 2개의 parts가 나온 것을 확인 할 수 있다.
- request를 받는 class가 StandardMultipartHttpServletRequest로 바뀐 것을 확인 할 수 있다.
- 원래는 RequestFacade 가 기본으로 나왔었다.
- itemName과 같은 단순 text는 직접 가져올 수 있다.
- file은 binary 파일이기에 못가져오지만 parts=[]에 binary file이 들어온 것을 확인 할 수 있다.
# applications.properties
spring.servlet.multipart.enabled=false # default가 true
- 멀티파트와 관련된 처리를 못하게 막아서 StandardMultipartHttpServletRequest가 사용되지 못하고 기본 request 받는 RequestFacade를 사용한다.
- 하지만 itemName=null 과 parts=[] 는 이렇게 값 자체를 받지를 못하게 된다.
- 작동 방식 * 깊이 알 필요가 없다. 더 좋은 MultipartFile가 존재
- 스프링이 DispatcherServlet에서 멀티파트 리졸버(Multipartresolver)를 실행
- 멀티파트 요청인 경우 일반적인 HttpServletRequest(RequestFacade)에서 MultipartHttpServletRequest로 변환해서 반환
* MultipartHttpServletRequest은 HttpServletRequest의 자식 인터페이스이고 멀티파트와 관련된 추가 기능을 제공한다. - MultipartHttpServletRequest를 주입받는데 이를 구현한 StandardMultipartHttpServletRequest를 반환한다.
3. 서블릿과 파일 업로드2 - 서버로 파일 받기
파일 업로드를 위해 실제 파일이 저장되는 경로 필요
directory 생성 후 application.properties 저장하기
file.dir=/Users/유저명/~~~/
- 주요 메서드
- part.getSubmittedFileName() : client가 전달한 파일명
- part.getInputStream() : Part의 전송 데이터를 읽을 수 있다.
- part.write(directory경로 + 파일명) : Part를 통해 전송된 데이터를 저장할 수 있다.
package hello.upload.controller;
@Slf4j
@Controller
@RequestMapping("/servlet/v2")
public class ServletUploadControllerV2 {
// application.properties에 있는 속성 그대로 들고 올 수 있다.
@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();// 중요. parts가 httpMessage에 각각의 부분을 의미하는 것
log.info("parts={}", parts);
for (Part part : parts) {
// 헤더와 헤더 값 찍기
log.info("==== PART ====");
log.info("name={}", part.getName()); // form의 name칸의 name
Collection<String> headerNames = part.getHeaderNames(); // request header name, string 같은 경우는 1가지지만, file 같은 것은 2가지가 나온다.
for (String headerName : headerNames) {
log.info("header {} : {}", headerName, part.getHeader(headerName)); // headerName: 헤더 tag 명, part.getHeader(headerName) : 해당 tag명의 실제 값
}
// 편의 메서드
// content-disposition; filename
// part body size
log.info("submittedFileName={}", part.getSubmittedFileName()); // 실제 파일 이름
log.info("size={}", part.getSize()); // 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";
}
}
4. 스프링과 파일 업로드
- MultipartFile 이라는 인터페이스로 멀티파트 파일을 편리하게 지원
- 제공 기능
- file.getOriginalFilename() : 업로드 파일 명
- file.tramsferTo(...) : 파일 저장
- 제공 기능
@Slf4j
@Controller
@RequestMapping("/spring")
public class SpringUploadController {
@Value("${file.dir}") // application.properties에 있는 속성 그대로 들고 올 수 있다.
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(); // spring 제공하는 기능
log.info("파일 저장 fullPath={}", fullPath);
file.transferTo(new File(fullPath));
}
return "upload-form";
}
}
5. 예제로 구현하는 파일 업로드, 다운로드
- 지금 해봤자 까먹을꺼 같애서 나중에 필요할 때 강의 듣기