- 일반적으로 사용하는 HTML form을 통한 파일 업로드 방식은 두가지다.
- 이 방식은 HTML Form 데이터를 서버로 전송 하는 가장 기본적인 방식이다.
- 아래와 같이 데이터를 보내면 application/x-www-form-urlencoded 방식으로 데이터를 보내게 된다.
<form action="/save" method="post">
<input type="text" name="username" />
<input type="text" name="age" />
<button type="submit"> 전송 </button>
</form>
- 위의 경우 전송 버튼을 클릭 하면 웹 브라우저는 HTTP메시지의 헤더에 아래와 같은 내용을 추가한다.
// 전송방식, 요청 url, 프로토콜 정보
POST /save HTTP/1.1
// 보내는 곳의 ip
Host: localhost:8080
// body의 데이터 형식
ContentType : application/x-www-form-urlencoded
// body 데이터들을 &로 구분하여 전송한다.
username=hdh&age=30
- 하지만 파일을 업로드 하려면 문자가 아니라 바이너리 데이터를 전송해야 한다. 따라서 문자를 전송하는 이 방식으로 파일을 전송하기는 어렵다.
- 또한 보통 Form 데이터를 전송할 때 파일만 전송하는 것이 아니라 부가적인 정보 즉, 이름/나이 등등도 전달할 수 있기 때문에 위의 방식으로는 문자와 바이너리 데이터를 동시에 전송할 수 없다.
- 이 문제를 해결 하기 위해 multipart/form-data 이라는 전송 방식을 제공 한다.
- 이 방식을 사용하려면 Form 태그에 별도로 enctype="multipart/form-data"라는 속성을 지정해야 한다.
- multipart/form-data 방식은 다른 종류의 여러 파일과 폼의 내용 함께 전송할 수 있다. (그래서 이름이 multipart 이다. )
<form action="/save" method="post" enctype="multipart/form-data">
<input type="text" name="username" />
<input type="text" name="age" />
<input type="file" name="file1 />
<button type="submit">전송 </button>
</form>
- 위와 같이 요청을 하면 HTTP 요청 메세지는 아래와 같이 구성 된다.
// 전송방식, 요청 url, 프로토콜 정보
POST /save HTTP/1.1
// 보내는 곳의 ip
Host: localhost:8080
Content-Type : multipart/form-data, boundary=-----X
Content-Length: 10457
// 🥊아래와 같이 각각의 전송 항목을 구분한다.
// 🥊각각의 항목의 헤더에는 Content-Disposition이라는 항목이 들어 가고
// 🥊여기에 부가 정보가 들어 간다.
// 🥊이렇게 항목별로 구분하여 다른 형식의 데이터를 한번에 전송하는 것이다.
-----XXX
Content-Disposition: form-data; name="username"
hdh
-----XXX
Content-Disposition: form-data; name="age"
30
-----XXX
Content-Disposition: form-data; name="file1"; filename="hello.png"
Content-Type : image/png
321321653216546asdf65w432e1651
-----XXX--
30
- 그렇다면 이렇게 복잡한 HTTP메시지를 서버에서 어떻게 사용할 수 있을까?
- multipart로 보낸 데이터를 받아 보기 위해 먼저 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 {
//🥊 전달 받은 request를 출력
log.info("request={}", request);
//🥊 입력 폼에서 전달한 itemName을 가져 온다.
String itemName = request.getParameter("itemName");
log.info("itemName={}", itemName);
//🥊 getParts가 multipart로 구성된 http 메세지에서 -----X로 구분된 항목들을 의미한다.
Collection<Part> parts = request.getParts();
log.info("parts={}", parts);
return "upload-form";
}
}
<!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>
//🥊 multipart로 데이터 전송
<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>
</body>
</html>
- 파일 이름과 파일을 선택하여 제출하기를 누르면 이제 로그가 찍힐 것이다.
request=org.springframework.web.multipart.support.StandardMultipartHttpServletRequest@5aea357d
itemName=��ǰA
//🥊 두개의 part가 등록 된 거 확인
parts=[org.apache.catalina.core.ApplicationPart@726ffff8, org.apache.catalina.core.ApplicationPart@37a4e696]
multipart 사용 옵션
- 먼저 multipart에는 사이즈 제한 옵션이 있다. 큰 파일을 무제한으로 업로드하게 둘 수는 없기 때문이다.
// 파일 하나의 최대 사이즈, 기본 1MB
max-file-size
// 멀티파트 요청 하나에 여러개의 파일이 들어갈 텐 데 그 전체 합, 기본 10MB
max-request-size
서버에 업로드
- 먼저 실제 파일을 서버에 업로드 할려면 파일이 저장되는 경로가 필요하다.
- application.properties에 가서 파일 경로를 입력해줘야 한다.
// application.properties에 아래와 같이 입력
// 끝에 '/'를 붙여 줘야 한다.
file.dir=D:/localUpload/
@Slf4j
@Controller
@RequestMapping("/servlet/v2")
public class ServletUploadControllerV2 {
// 🥊 application.properties에 입력한 파일 저장 장소의 경로를 가져 온다.
// 🥊 @Value(${})를 사용하면 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();
log.info("parts={}", parts);
// 🥊 http 메서드의 각각의 part의 이름를 가져오기 위해 반복문을 사용
for (Part part : parts) {
log.info("==== PART ====");
log.info("name={}", part.getName());
// 🥊 http전체 메세지의 헤더가 아닌 각각의 part도 헤더와 바디로 구성되어 있음
// 🥊 그 정보를 가져옴
Collection<String> headerNames = part.getHeaderNames();
for (String headerName : headerNames) {
log.info("header {}: {}", headerName,
part.getHeader(headerName));
}
// 🥊 편의 메서드
// 1. 제출한 파일 이름과 content-body의 크기 가져오기
//요청한 http메시지를 보면 입력한 상품 이름과 파일
// 각각 content-disposition을 가지고 있고, 파일에는 filename과 같은
// 좀 더 자세한 정보가 있다. 그 정보들을 가져 오는 것
log.info("submittedFileName={}", part.getSubmittedFileName());
log.info("size={}", part.getSize()); //part body size
// 🥊 2. 데이터 읽기
// spring에서는 inputStream을 쉽게 읽을 수 있도록 StreamsUtils라는 메서드를
// 제공한다. 해당 메서드를 통해 inputStream값을 string으로 바꾸고, 언어세트를 지정해 준것
InputStream inputStream = part.getInputStream();
String body = StreamUtils.copyToString(inputStream,
StandardCharsets.UTF_8);
log.info("body={}", body);
// 🥊 파일에 저장하기
if (StringUtils.hasText(part.getSubmittedFileName())) {
// 저장될 파일의 경로를 입력한다.
// application.properties에 지정한 경로에 파일의 이름을 붙인 것 (그게 그 파일의 경로이기 때문)
String fullPath = fileDir + part.getSubmittedFileName();
log.info("파일 저장 fullPath={}", fullPath);
part.write(fullPath);
}
}
return "upload-form";
}
}
- 여기까지 서블릿을 만들어 파일을 업로드 해보았는데, 사용했던 Part는 편리하기는 하지만 HttpServletRequest를 사용해야 하고, 파일 부분만 따로 구분 하려면 여러가지 코드를 추가해야 했다.
- 이러한 불편함을 스프링을 사용한다면 해소 할 수 있다.
💡스프링을 통한 파일 업로드
- 스프링은 MultipartFile이라는 인터페이스로 멀티파트 파일을 매우 편리하게 사용하도록 지원한다.
파일 업로드 다운로드
요구사항
- 상품을 관리 ( 상품 이름, 첨부파일 하나, 이미지 파일 여러개 )
- 첨부파일을 업로드 하고 다운로드 할 수 있다.
- 업로드한 이미지를 웹 브라우저에서 확인할 수 있다.