파일을 업로드하려면 바이너리 데이터를 전송해야 하는데 폼을 전송할 때는 파일만 전송하는 것이 아니라 문자와 바이너리 데이터를 동시에 전송하는 경우가 많다.
HTTP는 multipart/form-data 전송 방식으로 다른 종류의 여러 파일와 폼의 내용을 함께 전송할 수 있는데 서블릿 컨테이너는 멀티파트 데이터를 request 객체에 각각 나누어 담아 전송하고 request.getParts()로 받을 수 있다.
업로드 사이즈는 다음과 같이 제한할 수 있다.
spring.servlet.multipart.max-file-size=1MB
spring.servlet.multipart.max-request-size=10MB
// 옵션을 끄면 서블릿 컨테이너는 멀티파트 처리를 하지 않고 request.getParts()는 비어서 전송된다.
// Default true
spring.servlet.multipart.enabled=false
스프링의 DispatcherServlet에서 MultipartResolver가 실행이 되고 서블릿 컨테이너가 전달하는 HttpSerlvetRequest를 자식 인터페이스인 MultipartHttpServletRequest로 변환해서 반환해서 멀티파트와 관련된 추가 기능을 할 수 있게 한다.
서블릿이 제공하는 Parts는 멀티파트 형식을 편리하게 읽을 수 있는 다양한 메서드를 제공하지만 HttpServletRequest를 사용해야 하고 스프링이 제공하는 MultipartFile 인터페이스가 훨씬 편리하기 때문에 잘 사용하지 않는다.
스프링 MultipartFile
@RequestParam MultipartFile file, @ModelAttribute MultipartFile file 처럼 컨트롤러 파라미터에 입력만 해주면 사용할 수 있다.
- getOriginalFilename() : 업로드 파일 명
- transferTo() : 파일 저장
1. 업로드 파일 정보를 보관할 클래스
@Data
public class UploadFile {
// 클라이언트에서 업로드한 파일명
private String uploadFileName;
// 서버에서 관리할 파일명 (중복되지 않을 이름으로 보관해야한다.)
private String storeFileName;
public UploadFile(String uploadFileName, String storeFileName) {
this.uploadFileName = uploadFileName;
this.storeFileName = storeFileName;
}
}
2. 파일 저장 등을 처리하는 클래스
@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()) {
storeFileResult.add(storeFile(multipartFile));
}
}
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;
}
private String extractExt(String originalFilename) {
int pos = originalFilename.lastIndexOf(".");
return originalFilename.substring(pos + 1);
}
}
설정 값 분리의 필요성 (@Value)
DB 연결 정보나 외부 API 주소, 파일 경로 같은 설정 값들을 클래스에 입력하는 대신 properties, yml 등으로 분리해서 관리하면 환경에 따라 유연하게 값을 설정해줄 수 있고 불필요한 컴파일, 직접 클래스 코드를 하나 하나 수정하는 등의 문제를 해결할 수 있다.
@Value
1. 프로퍼티 치환법
properties에 작성된 키 값을 ${}안에 넣어주면 Spring이 PropertyPlaceHolderConfigurer를 통해 초기화 작업 중에 해당 값을 실제 값으로 치환한다. PropertyPlaceHolderConfigurer는 팩토리 후처리기로써 빈 설정 메타정보가 모두 준비됐을 때 빈 메타정보 자체를 조작하는 역할을 한다. 하지만 대체할 위치를 치환자로 지정해두고 후처리기가 치환해주기를 기다리는 수동적인 방법으로 빈에서 직접 값을 꺼내는 SpEL 방법이 권장 된다.
2. SpEL (Spring Expression Language)
SpEL은 기본적으로 #{} 안에 표현식을 넣도록 되어있는데, user.name이라는 표현식은 이름이 user인 빈의 name 프로퍼티를 의미한다. SpEL은 다른 빈의 프로퍼티에 접근가능할 뿐만 아니라 메소드 호출도 가능하고 다양한 연산도 지원하며 클래스 정보에도 접근할 수 있다. 심지어 생성자를 호출해서 객체를 생성할 수도 있다.
- UploadFile : 클라이언트에서 업로드한 파일명과 서버에 저장할 파일명을 가지고 있는 클래스
- List<UploadFile> storeFiles : multipartFile 을 하나씩 storeFile()을 통해 서버에 저장하고 List에 UploadFile을 add
- UploadFile storeFile : UUID 랜덤값을 생성하고 확장자를 추출해서 서버 경로에 파일을 저장하고 UploadFile을 반환
3. 폼 전송 객체
@Data
public class ItemForm {
private Long itemId;
private String itemName;
private List<MultipartFile> imageFiles;
private MultipartFile attachFile;
}
4. 컨트롤러
@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());
//데이터베이스에 저장
Item item = new Item();
...
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 {
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));
String encodedUploadFileName = UriUtils.encode(uploadFileName, StandardCharsets.UTF_8);
String contentDisposition = "attachment; filename=\"" + encodedUploadFileName + "\"";
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition)
.body(resource);
}
}
- Resource downloadImage : <img> 태그로 이미지를 조회할 때, UrlResource로 이미지 파일을 읽어서 이미지 바이너리를 반환
- ResponseEntity<Resource> dounloadAttach : 파일을 다운로드 할 때 클라이언트가 업로드했던 파일명으로 생성
5. View
<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 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>
[참고] 인프런 김영한님 강의를 공부한 내용입니다.
'Spring' 카테고리의 다른 글
[Spring] 스프링 시큐리티(Spring Security) + JWT 토큰 구현(1) (0) | 2022.09.19 |
---|---|
[Spring Security] 세션(Session) 기반의 로그인 인증 (0) | 2022.07.16 |
[스프링 MVC2] 스프링 타입 컨버터 (0) | 2022.06.17 |
[스프링 MVC2] API 예외 처리 (0) | 2022.06.17 |
[스프링 MVC2] 예외 처리와 오류 페이지 (0) | 2022.06.17 |