웹페이지에서 파일업로드하고, 첨부파일 다운로드를 어떻게 하는지, 그 원리와 방법을 정리해보자.
HTML form을 통한 파일업로드를 이해하려면, 먼저 폼에서 전송하는 2가지 방식을 알아야 한다.
application/x-www-form-urlencoded 방식은 우리가 익히 아는, HTML 폼 데이터를 서버로 전송하는 가장 기본적인 방법이다.
form 태그에 별도의 enctype 옵션
이 없다면, 웹 브라우저는 요청 HTTP 헤더의 다음의 내용을 추가한다.
Content-Type: application/x-www-form-urlencoded
그리고 폼에 입력한 항목들을 HTTP Body에 문자
로 username=kim&age=20
와 같이 &로 구분해서 전송한다.
그런데, 파일을 업로드하려면 문자가 아닌 바이너리 데이터를 전송해야 한다. 또한 보통 폼을 전송할 때는 파일만 전송하는 것이 아니다.
다음의 예시를 보자.
- 이름
- 나이
- 첨부파일
이때 문자(이름,나이)와 바이너리(파일)을 동시에 전송해야 하는 상황이다..
이 문제를 해결하기 위해 HTTP는 multipart/form-data
라는 전송방식을 제공했다 !!
이 방식을 사용하려면 앞선 방식과 달리,
form 태그에 별도의 enctype="multipart/form-data"
을 지정해야 한다.
multipart/form-data는 다른 종류의 여러 파일과 폼의 내용을 같이 전송할 수 있다. (그래서 이름이 multipart임)
폼의 입력결과로 생성된 HTTP 요청메시지를 보면, 각각의 전송항목들이 구분되어 있다.
Content-Disposition
이라는 항목별 헤더가 추가되어 있고, 여기에 부가정보가 있다.
예제에서는 username, age, file1이 각각 분리되어 있고, 폼의 일반데이터는 각 항목별로 '문자'가 전송되며, 파일의 경우 파일이름과 Content-Type이 추가되고, '바이너리 데이터가 전송'된다.
multipart/form-data는 이렇게 각각의 항목을 구분해서, 한번에 전송하는 것이다.
# application.properties에서
-- HTTP 요청 메세지 확인가능
logging.level.org.apache.coyote.http11=trace
-- 업로드 사이즈 제한
spring.servlet.multipart.max-file-size=1MB
spring.servlet.multipart.max-request-size=10MB
max-file-size
: 파일 하나의 최대 사이즈, 기본 1MB
max-request-size
: 멀티파트 요청 하나에 여러 파일을 업로드할 수 있는데, 그 전체의 함. 기본 10MB
서블릿이 제공하는 Part에 대해서 알아보고, 실제 파일도 서버에 업로드해보자.
파일업로드를 하려면, 실제파일저장경로가 필요하다.
-- application.properties
file.dir=파일업로드경로설정
ex) file.dir=/Users/minji/Desktop/spring/study/mvc2/upload/file/
-- 주의사항
1. 해당경로에 실제 폴더를 미리 만들어둘 것
2. 경로설정 적을때, 마지막에 / (슬래시)가 포함된 것
<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>
enctype으로 multipart/form-data를 설정해줬고, input type="file"로 파일1개를 업로드할 수 있게 했다.
@Slf4j
@Controller
@RequestMapping("/servlet/v2")
public class ServletUploadControllerV2 {
@Value("${file.dir}") // application.properties에서 설정한 속성값을 가져오자
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);
for (Part part : parts) {
log.info("==== PART =====");
// PART name 출력
log.info("name : {}", part.getName());
// PART의 각각의 헤더name - 헤더value 출력
Collection<String> headerNames = part.getHeaderNames();
for (String headerName : headerNames) {
log.info("header {} : {}", headerName,
part.getHeader(headerName));
}
// 편의메서드
// content-Disposition ; filename
log.info("submittedFileName : {}", part.getSubmittedFileName()); // -- cat.jpg (업로드한 파일명)
log.info("size : {}", part.getSize()); // part body size
// 데이터 읽기 -- 파일 body 내용 읽어오기
InputStream inputStream = part.getInputStream();
// 파일 body부분이 바이너리 내용이니까, 바이너리 -> String으로 형변환
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
)로 나누어서 전송한다.
서블릿이 제공하는 Part는 멀티파트 형식을 편리하게 읽을 수 있는 다양한 메서드를 제공했다.
실행
다음의 내용을 전송했다.
결과로그
==== PART ====
name=itemName
header content-disposition: form-data; name="itemName"
submittedFileName=null
size=7
body=상품A
==== PART ====
name=file
header content-disposition: form-data; name="file"; filename="스크린샷.png"
header content-type: image/png
submittedFileName=스크린샷.png
size=112384
body=qwlkjek2ljlese...
파일 저장 fullPath=/Users/kimyounghan/study/file/스크린샷.png
이때, content-disposition 헤더의 name="itemName"이나 name="file"은 upload-file.html 폼에서 input태그에 작성한 name과 일치한다.
실행결과, 파일저장경로에 가보면 실제 파일이 저장된 것을 알 수 있다. 만일 저장이 안되었다면, 파일저장경로를 다시 한번 확인해보자.
서블릿이 제공하는 Part는 편하기는 하지만, HttpServletRequest를 사용해야 하고, 추가로 파일부분을 구분하려면 여러가지 코드를 추가해야 한다.
이번에는, 스프링이 해당부분을 얼마나 편리하게 제공하는지 확인해보자.
스프링은 MultipartFile
이라는 인터페이스로, 멀티파트 파일을 매우 편리하게 지원해준다.
@Controller
@Slf4j
@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(); // -- 업로드 파일명
log.info("파일 저장 fullPath : {}", fullPath);
file.transferTo(new File(fullPath)); // -- 파일 저장
}
return "upload-form";
}
}
업로드하는 HTML form의 name에 맞추어, @RequestParam을 적용해주면 된다.
request=org.springframework.web.multipart.support.StandardMultipartHttpServletRequest@5c022dc6
itemName=상품A
multipartFile=org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile@274ba730
파일 저장 fullPath=/Users/kimyounghan/study/file/스크린샷.png
예제의 파일이 많으므로, 각 파일의 흐름과 역할을 정리해보았으니, 실제 코드에서 확인해보자.
ㄴmain.java.hello.upload
ㄴdomain
ㄴ Item - 데이터베이스 도메인 !!
(DB에는 파일자체 저장 X, 파일관련정보 저장O)
ㄴ ItemRepository
ㄴ UploadFile - File DTO !!
(서버에서 관리할 파일명, 사용자가 올린 파일명 저장_
ㄴ file
ㄴ FileStore - 파일업로드,다운로드 관련 비즈니스로직
ㄴ controller
ㄴ ItemForm - 상품저장용 폼 DTO!! (Item엔디티랑 다름)
ㄴ ItemController - 상품업로드폼,상품업로드, 첨부파일 다운로드