HTML 폼 전송 방식
1. application/x-www-form-urlencoded
- HTML 폼 데이터를 서버로 전송하는 가장 기본적인 방법
- Form 태그에 별도의 enctype 옵션이 없으면 웹 브라우저는 요청 HTTP 메시지의 헤더에 Content-Type : application/x-www-form-urlencoded를 추가
- 폼에 입력한 전송할 항목을 HTTP Body에 문자로 &로 구분해서 전송
- 파일을 업로드 하려면 문자가 아닌 바이너리 데이터를 전송해야 하는데, 해당 방식으로는 어려움
2. multipart/form-data
- 문자와 바이너리를 동시에 전송하는 방법
- Form 태그에 별도의 enctype="multipart/form-data를 지정
- 다른 종류의 여러 파일과 폼의 내용을 함께 전송 가능
- Content-Dispositon 이라는 항목별 헤더 추가
- username, age, file1이 각각 분리
- 폼의 일반 데이터는 각 항목별로 문자가 전송
- 파일의 경우 파일 이름 + Content-Type이 추가되고 바이너리 데이터 전송
스프링 파일 업로드
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
@Slf4j
@Controller
@RequestMapping("/spring")
public class SpringUploadController {
@Value("${file.dir}") // 파일이 저장될 경로
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";
}
}
- 스프링이 제공하는 MultipartFile 인터페이스 사용
- 단일, 다중 업로드 가능
- HTTP Method가 POST인 경우는 컨트롤러의 매개변수에 대부분 @RequestBody를 넣어 처리하지만 파일의 경우는 다름
- Content-Type = multipart/form-data로 전달되어올 때는 Exception을 발생시키기 때문에, @RequestBody가 아닌 @RequestParam, @RequestPart 사용
파일 업로드, 다운로드 구현
업로드 파일 정보 보관
import lombok.Data;
@Data
public class UploadFile {
private String uploadFileName;
private String storeFileName;
public UploadFile(String uploadFileName, String storeFileName) {
this.uploadFileName = uploadFileName; // 업로드한 파일명
this.storeFileName = storeFileName; // 서버 내부에서 관리하는 파일명
}
}
파일 저장과 관련된 업무 처리
import hello.upload.domain.UploadFile;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@Component
public class FileStore {
@Value("${file.dir}")
private String fileDir;
public String getFullPath(String filename){
return fileDir + filename;
}
// 다중 업로드
public List<UploadFile> storeFiles(List<MultipartFile> multipartFiles) throws IOException{
List<UploadFile> storeFileResult = new ArrayList<>();
for (MultipartFile multipartFile : multipartFiles) {
if(!multipartFile.isEmpty()){
UploadFile uploadFile = storeFile(multipartFile);
storeFileResult.add(uploadFile);
}
}
return storeFileResult;
}
public UploadFile storeFile(MultipartFile multipartFile) throws IOException {
if(multipartFile.isEmpty()){
return null;
}
String originalFilename = multipartFile.getOriginalFilename();
String storeFileName = createStoreFileName(originalFilename); // 서버에 저장하는 파일명
multipartFile.transferTo(new File(getFullPath(storeFileName)));
return new UploadFile(originalFilename, storeFileName);
}
private String createStoreFileName(String originalFilename) {
String ext = extractExt(originalFilename);
String uuid = UUID.randomUUID().toString();
return uuid + "." + ext;
}
// 확장자 추출(jpg, png 등)
private String extractExt(String originalFilename) {
int pos = originalFilename.lastIndexOf(".");
return originalFilename.substring(pos+1);
}
}
- 같은 파일이름을 서로 다른 사람이 업로드 하는 경우 기존 파일 이름과 충돌이 날 수 있음
- 그래서 UUID를 이용하여 서버 내부에서 관리하는 별도의 파일명 생성
컨트롤러
import hello.upload.domain.Item;
import hello.upload.domain.ItemRepository;
import hello.upload.domain.UploadFile;
import hello.upload.file.FileStore;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import org.springframework.web.util.UriUtils;
import java.io.IOException;
import java.net.MalformedURLException;
import java.nio.charset.StandardCharsets;
import java.util.List;
@Slf4j
@Controller
@RequiredArgsConstructor
public class ItemController {
private final ItemRepository itemRepository;
private final FileStore fileStore;
@GetMapping("/items/new")
public String newItem(@ModelAttribute ItemForm form){
return "item-form";
}
@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());
// DB에 저장
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}";
}
@GetMapping("items/{id}")
public String items(@PathVariable Long id, Model model){
Item item = itemRepository.findById(id);
model.addAttribute("item", item);
return "item-view";
}
@ResponseBody
@GetMapping("/images/{filename}")
public Resource downloadImage(@PathVariable String filename) throws MalformedURLException {
// file:/User/.../0c2c1b0-02f9-42cb-9488-b0be86bc8aea.png
return new UrlResource("file:" + fileStore.getFullPath(filename));
}
@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));
log.info("uploadFileName={}", uploadFileName);
String encodeUploadFileName = UriUtils.encode(uploadFileName, StandardCharsets.UTF_8);
String contentDisposition = "attachment; filename=\"" + encodeUploadFileName + "\"";
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition)
.body(resource);
}
}
- @GetMapping("/items/new") : 등록 폼을 보여줌
- @PostMapping("/items/new") : 폼의 데이터를 저장하고 보여주는 화면으로 리다이렉트
- @GetMapping("/items/{id}") : 상품을 보여줌
- @GetMapping("images/{filename}") : <img> 태그로 이미지를 조회할 때 사용
- UrlResource로 이미지 파일을 읽어서 @ResponseBody로 이미지 바이너리 반환
- @GetMapping("/attach/{itemId}") : 파일 다운로드
- Content-Disposition 헤더에 attachment; filename="업로드 파일명" 값을 주면 됨
등록 폼 View
<!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>
<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>
</div> <!-- /container -->
</body>
</html>
조회 View
<!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>
상품명: <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"/>
</div> <!-- /container -->
</body>
</html>
'Spring > MVC' 카테고리의 다른 글
스프링 API 예외 처리 (0) | 2024.07.21 |
---|---|
API 예외 처리 (0) | 2024.07.21 |
HttpEntity (0) | 2024.07.21 |
서블릿 예외 처리 (0) | 2024.07.20 |
스프링 인터셉터 (0) | 2024.07.11 |