폼 전송 방법
- HTML Form 전송 방식
application/x-www-form-urlencoded
multipart/form-data
-
application/x-www-form-urlencoded
- Form 데이터를 서버로 전송하는 가장 기본적인 방식
- form 태그(<form …>)에 별도의 enctype 설정이 없으면 웹 브라우저는 요청 HTTP 메시지 헤더에
Content-Type: application/x-www-form-urlencoded
를 추가함 - 그 후 폼에 입력된(전송할) 것들을 HTTP Body에 문자로
username=kim&age=20
와 같이 & 로 구분해서 전송함 - 그렇다면 문자가 아닌 바이너리로 구성된 첨부파일도 함께 전송하고 싶다면 어떻게 할까? →
multipart/form-data
-
multipart/form-data
- 다른 종류의 여러 파일과 폼의 내용 함께 전송 가능 (문자와 첨부파일 등)
- 각각의 항목을 구분해서, 한번에 전송하는 것
- form 태그(<form …>)의 enctype에 별도의 설정 필요 →
enctype="multipart/form-data"
지정 - Form의 입력 결과로 생성된 요청 HTTP 메시지를 보면 알 수 있듯이 각각의 전송 항목이 “————XXX” 로 구분(Part)되어 있으며 Content-Dispositon 이라는 항목별 헤더가 추가되어 있고 부가 정보 또한 포함되어 있음.
- 해당 예시에서는 uesrname, age, file1이 분리되어 있고 내용에는 문자, 파일의 경우는 이름과 Content-Type이 추가되고 바이너리 데이터가 전송됨
- 그렇다면 이렇게 구분된 데이터를 포함한 요청 HTTP 메시지를 어떻게 서버에서 사용할 수 있을까? → 스프링 없이 서블릿만으로 사용 ⇒ 스프링에서 파일 받기 의 순서로 알아보자잉
File Upload in Servlet (without Spring)
서블릿에서 Multipart 형식의 Form 데이터 받아오기
- Spring에서 제공하는 파일 업로드 기능없이 서블릿만으로 파일 업로드 구현
-
html
<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>
- multipart로 form을 받아올 때는 항상
enctype="multipart/form-data"
부분 추가! - multipart로 text, file 2가지 Part를 받아옴
- multipart로 form을 받아올 때는 항상
-
Controller
@PostMapping("/upload") public String saveFileV1(HttpServletRequest request) throws ServletException, IOException { String itemName = request.getParameter("itemName"); log.info("itemName={}", itemName); Collection<Part> parts = request.getParts(); log.info("parts={}", parts); return "upload-form"; }
request.getParameter
를 통해서 text의 값을 가져올 수는 있음. but file의 값은 가져올 수 없음! → file은 parameter형식으로 넘어오는게 아니니깐!request.getParts()
:multipart/form-data
전송 방식에서 각각 나누어진 부분을 받아서 확인 가능 →parts=[org.apache.catalina.core.ApplicationPart@13766755, org.apache.catalina.core.ApplicationPart@30602154]
-
application.properties 에서
logging.level.org.apache.coyote.http11=debug
설정을 통해 HTTP 요청 메시지를 확인해 보면 Part로 구분되어 전달되는 것을 확인할 수 있음
- Multipart 사용 옵션
- application.properties 에서
multipart
옵션 설정 가능 - 업로드 사이즈 제한
spring.servlet.multipart.max-file-size=1MB
: 파일 하나의 최대 사이즈(default = 1MB)spring.servlet.multipart.max-request-size=10MB
: 전체 파일(여러 파일의 합)의 최대 사이즈(default = 10MB)
- mutlipart 허용
spring.servlet.multipart.enabled=true
- defalut = true
- 이 옵션을 켜면 복잡한 멀티파트 요청을 처리해서 사용할 수 있게 제공
- 해당 옵션을 키면 기본으로 사용했던
HttpServletRequest
가MultipartResolver
를 통해서MultipartHttpServletRequest
로 변환됨 - MultipartHttpServletRequest
- HttpServletRequest 의 자식 인터페이스
- 멀티파트와 관련된 추가 기능을 제공
- 이것을 사용하면 멀티파트와 관련된 여러가지 처리를 편리하게 사용 가능
- BUT 우린 HttpServletRequest 가 변환된 MultipartHttpServletRequest를 사용하지 않고
MultipartFile
이라는 스프링에서 제공하는 유용한 기능을 사용할 것!
- application.properties 에서
서블릿에서 Multipart형식의 File 업로드
- 서블릿이 제공하는 Part를 이용해서 실제로 파일 업로드 구현 (Spring의 기능을 이용하지 않고 서블릿의 Part 기능만을 이용하여)
- 경로 설정
- application.properties 에
file.dir
속성에 업로드 경로 설정 (ex_/Users/park/servlet/file/
) - 해당 경로 값은
@Value
를 통해 가져와 사용할 예정
- application.properties 에
- ServletUploadController
-
경로 불러오기
@Value("${file.dir}") private String fileDir;
@Value
를 통해 application.propreties의 속성 값을 가져옴
-
파일 확인 및 업로드 (Servlet의 Part 이용)
@PostMapping("/upload") public String saveFileV1(HttpServletRequest request) throws ServletException, IOException { Collection<Part> parts = request.getParts(); // 각각의 부분(Part)들을 가져옴 for (Part part : parts) { Collection<String> headerNames = part.getHeaderNames(); // parts 도 header 와 body 로 구분됨! for (String headerName : headerNames) { log.info("header {}: {}", headerName, part.getHeader(headerName)); } //편의 메서드 //content-disposition 의 filename 따오기 log.info("submittedFilename={}", part.getSubmittedFileName()); // 자동 파싱해주는 것 log.info("size={}", part.getSize()); //part body size //데이터 읽기 InputStream inputStream = part.getInputStream(); // 실제 받아온 바이너리 값을 읽는 것 String body = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8); // 그 읽어 온 값을 String 으로 보기 편하게 변경 log.info("body={}", body); //파일에 저장하기 if (StringUtils.hasText(part.getSubmittedFileName())) { // 파일 제출인지 확인 String fullPath = fileDir + part.getSubmittedFileName(); log.info("파일 저장 fullPath={}", fullPath); part.write(fullPath); // 해당 설정된 경로를 통해 저장 } } return "upload-form"; }
- Servlet의 Part : Multipart의 각각의 부분들 중 File과 관련된 Part에 대해서 확인하거나 업로드할 때 사용
part.getInputStream()
: Part의 전송 데이터를 읽을 수 있음- 중요한 부분은 “파일에 저장하기” 부분
StringUtils.hasText(part.getSubmittedFileName())
을 통해 현재 part가 제출된 파일인지 확인 → (part.getSubmittedFileName()
: 클라이언트가 전달한 파일명. 파일이 없다면null
)part.write(fullPath)
: 지정된 경로에 해당 파일 저장
-
- 이렇게 서블릿이 제공하는 Part 로도 파일을 저장할 수 있지만, 추가적으로 HttpServletRequest 를 사용해야 하기도 하고. 추가적으로 파일인지 아닌지 확인 하고 구분해야되는 여러코드를 요구함. → Spring을 사용하면 훨씬 더 편리하게 사용 가능!!
File Upload in Spring
스프링과 파일 업로드 (MultipartFile)
- 스프링은
MultipartFile
이라는 인터페이스로 multipart의 file을 쉽게 다룰 수 있도록 지원 -
Controller
@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(); file.transferTo(new File(fullPath)); } return "upload-form"; }
@RequestParam MultipartFile file
: 제출된 파일을 MultipartFile로 받아옴 → 서블릿의 복잡한 과정이 어노테이션과 Spring에서 지원하는 인터페이스로 한번에 해결되는 것 (HTML Form의 name에 맞추어@RequestParam
을 적용.@ModelAttribute
도 가능!)file.getOriginalFilename()
: 업로드 파일 명file.transferTo(new File(fullPath));
: 해당 경로로 파일 저장
파일 업로드 및 확인, 다운로드 (실전)
- 실제 파일,이미지 업로드 및 확인, 다운로드에서는 고려해야할 점들이 있음. 그런 사항들을 확인
- 상품 객체 및 DTO
- 상품 이름, 첨부파일 하나, 이미지 파일 여러개(multiple)
-
업로드 파일 정보 보관
@Data @AllArgsConstructor public class UploadFile { private String uploadFileName; private String storeFileName; }
uploadFileName
은 파일의 원본명storeFileName
은 Repository에 저장되는 파일의 이름 → UUID 사용 (똑같은 이름으로 저장되면 안됨! 충돌 예방)
-
Repository에 저장되는 상품 객체
@Data public class Item { private Long id; private String itemName; private UploadFile attachFile; // 단일 파일 private List<UploadFile> imageFiles; // 파일 여러개 }
-
Item 객체의 DTO
@Data public class ItemForm { private Long itemId; private String itemName; private MultipartFile attachFile; private List<MultipartFile> imageFiles; }
- Form 태그로 받아올 때 사용되는 Item DTO (상품 저장용 폼)
- Form 에서 받아올 때의 단일 파일은 MultipartFile로, **여러 파일은 List
**로 받아올 수 있음
- 상품은 업로드 및 확인, 다운로드 가능
- 업로드
- Repository는 이전과 동일하므로 생략 (이전 파트 참고)
-
업로드 Form
@GetMapping("/items/new") public String newItem(@ModelAttribute ItemForm form) { return "item-form"; }
- 빈 itemDTO를 보내주어 form의 object를 맞춰 줌
-
업로드 진행
<form th:action method="post" enctype="multipart/form-data"> <ul> <li>상품명 <input type="text" name="itemName"></li> <li>첨부파일<input type="file" name="attachFile" ></li> <li>이미지 파일들<input type="file" multiple="multiple" name="imageFiles" ></li> </ul> <input type="submit"/> </form>
- 단일 파일은 그냥 file을 통해서 받아와 주면 됨 → Controller에서
MultipartFile
로 받음 - 여러 파일의 input은 multiple 설정을 통해 받아와야 됨! → Controller에서
List<MultipartFile>
로 받음
@PostMapping("/items/new") public String saveItem(@ModelAttribute ItemForm form, RedirectAttributes redirectAttributes) throws IOException { UploadFile attachFile = fileStore.storeFile(form.getAttachFile()); List<UploadFile> storeImageFiles = fileStore.storeFiles(form.getImageFiles()); // DTO → Item Item item = new Item(); item.setItemName(form.getItemName()); item.setAttachFile(attachFile); item.setImageFiles(storeImageFiles); itemRepository.save(item); //데이터베이스에 저장 redirectAttributes.addAttribute("itemId", item.getId()); return "redirect:/items/{itemId}"; }
@ModelAttribute
를 통해서 Form의 입력 값들(text, file, multiple files)을 받아옴- DTO → Item : 받아온 MutlipartFile 을 UploadFile로 변경함으로써 실제 파일이 아닌 원본 파일 명과 파일 저장 경로만을 Item에 저장
- PRG : Post 후 Redircet를 통해 Get 요청 실행
- 단일 파일은 그냥 file을 통해서 받아와 주면 됨 → Controller에서
- 조회 및 다운로드
-
Item 조회
@GetMapping("/items/{id}") public String items(@PathVariable Long id, Model model) { Item item = itemRepository.findById(id); model.addAttribute("item", item); return "item-view"; }
-
Item의 첨부파일, 이미지 파일 조회 및 다운로드
상품명: <span th:text="${item.itemName}">상품명</span><br/> 첨부파일: <a th:if="${item.attachFile}" th:href="|/attach/${item.id}|" th:text="${item.getAttachFile().getUploadFileName()}" /><br/> <img th:each="imageFile : ${item.imageFiles}" th:src="|/images/${imageFile.getStoreFileName()}|" width="300" height="300"/>
-
이미지 조회 (각 이미지들을 보여주기 위함) →
th:src="|/images/${imageFile.getStoreFileName()}|"
@ResponseBody @GetMapping("/images/{filename}") public Resource imgViewer(@PathVariable String filename) throws MalformedURLException { return new UrlResource("file:" + fileStore.getFullPath(filename)); // 직접 경로를 따라 가서 파일을 찾아와서 아웃풋스트림으로 반환 }
@ResponseBody
: 반환된 아웃풋 스트림 그대로 HTTP Body에 담기위함 (View Resolver가 아닌 HttpMessageConverter 동작) → img 태그가 이를 받아서 이미지를 띄워주는 것- Resource, UrlResource : 해당 경로에 대한 파일을 아웃풋스트림으로 변환하여 전달
-
첨부파일 다운로드 (클릭 시 다운로드) →
th:href="|/attach/${item.id}|"
@GetMapping("/attach/{itemId}") public ResponseEntity<Resource> downloadAttach(@PathVariable Long itemId) throws MalformedURLException { Item item = itemRepository.findById(itemId); String storeFileName = item.getAttachFile().getStoreFileName(); String uploadFileName = item.getAttachFile().getUploadFileName(); UrlResource resource = new UrlResource("file:" + fileStore.getFullPath(storeFileName)); String encodedUploadFileName = UriUtils.encode(uploadFileName, StandardCharsets.UTF_8); // 인코딩 (한글을 위함) String contentDisposition = "attachment; filename=\"" + encodedUploadFileName + "\""; // 다운로드하기 위한 규약 return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition) // 다운로드를 위한 헤더 .body(resource); }
- 일반적인
@ResponseBody
로 UrlResource를 반환하면 그냥 조회만 됨. - 다운로드를 위해선 response Http Header 값을 변경해줘야 됨 → ResponseEntity 사용
- body에는 조회의 방식 그대로 UrlResource로 채워줌 (아웃풋스트림)
ResponseEntity.ok().header(...).body(resource)
ResponseEntity.ok()
:ResponseEntity
의 status를 200으로 설정.header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition)
: 다운르도를 위해 header의 content-disposition 이라는 항목을 따로 설정해줌(한글을 포함한 제대로된 경로를 위해 인코딩 설정 필수) → 다운로드 규약!
- 일반적인
-