-
Spring MVC - 파일 업로드와 다운로드 구현해보기Spring/Spring MVC 2022. 2. 18. 15:56728x90반응형
파일 업로드와 다운로드 구현해보기
파일 업로드와 다운로드 구현 시 주의할 사항들이 있다. 직접 구현을 해보면서 주의 사항들을 살펴보자.
파일 업로드
도메인 객체 생성
우선 데이터 베이스에 저장할 도메인 객체를 생성해준다.
@Data public class Item { private Long id; private String itemName; private UploadFile attachFile; private List<UploadFile> imageFiles; }
파일 관리 클래스
실제 파일명으로 서버 내부에 파일을 저장하면 안 된다. 그 이유는 기존에 서버에 저장되어있던 파일명으로 업로드 요청을 받게 되면 파일명이 같기 때문에 기존 파일명에 새로 들어온 파일명으로 덮어씌워 지게 되는 불상사가 일어난다. 즉, 기존 파일의 손실로 심각한 문제를 발생하기 때문에 서버에서는 저장할 파일명이 겹치지 않도록 내부에서 별도로 관리하는 파일명을 사용해야 한다.
따라서 실제 파일명과 서버 내부에서 관리하는 파일명의 정보를 보관하기 위한 클래스를 구현해준다.
@Data public class UploadFile { private String uploadFileName; private String storeFileName; public UploadFile(String uploadFileName, String storeFileName) { this.uploadFileName = uploadFileName; this.storeFileName = storeFileName; } }
상품 Repository 클래스
간단한 파일 업로드와 다운로드를 구현하기 때문에 데이터베이스를 사용하지 않고 Map을 이용하여 구현하자.
@Repository public class ItemRepository { private final Map<Long, Item> store = new HashMap<>(); private long sequence = 0L; public Item save(Item item) { item.setId(++sequence); store.put(item.getId(), item); return item; } public Item findById(Long id) { return store.get(id); } }
파일 저장과 관련된 클래스 - FileStore
HTTP Form 멀티파트 전송으로 넘어온 파일을 서버에 저장하는 역할을 담당하는 클래스가 필요하다. 위에서 설명했듯이 서버 내부에서 관리하는 파일명으로 서버에 파일을 저장해야 하므로 파일 저장과 관련된 클래스를 구현해야 한다.
구현할 FileStore 클래스에서 파일 저장 경로를 사용하기 위해 경로 설정 값을 넣어준다.
file.dir=C:/tools/spring/Spring_Lab/uploadex/file/
▶ 저장할 경로를 사용하기 위해 application.properties에 원하는 경로를 넣어준다.
이제 파일 저장과 관련된 처리를 하는 클래스를 구현해보자.
@Value("${file.dir}") private String fileDir;
▶ 위에서 설정한 경로 값을 @Value를 통해 가져온다.
public String getFullPath(String filename) { return fileDir + filename; }
▶ 파일을 저장할 전체 경로를 구하는 메서드 구현
public UploadFile storeFile(MultipartFile multipartFile) throws IOException { if (multipartFile.isEmpty()) { return null; } String originalFileName = multipartFile.getOriginalFilename(); int pos = originalFileName.lastIndexOf("."); String ext = originalFileName.substring(pos + 1); String uuid = UUID.randomUUID().toString(); String storeFileName = uuid + "." + ext; multipartFile.transferTo(new File(getFullPath(storeFileName))); return new UploadFile(originalFileName, storeFileName); }
▶ 실제 파일명으로 서버에서 관리하는 파일명을 구해 파일을 저장하는 메서드
- ex) qwd-wqdj-zxcq-123df-qwd.png 형식으로 파일이 저장▶ orginalFileName.lastIndexOf(".")
- 서버에서 관리하는 파일명 뒤에 해당 확장자를 붙여주기 위해 확장자를 추출▶ UUID.randomUUID().toString()
- UUID를 이용해 서버에서 관리하는 파일명 생성▶ uuid + "." + ext
- 서버에 저장하기 위해 서버에서 관리하는 파일명과 확장자를 붙여준다.▶ multipartFile.transferTo(new File(getFullPath(storeFileName)))
- getFullPath 메서드를 이용해 지정 경로로 서버에서 부여한 파일명으로 파일 저장
위에서 구현한 메서드를 보면 너무 많은 책임이 있다. 책임을 나눠주고 가독성이 좋게 파일 확장자를 구하는 메서드와 서버에서 관리하는 파일명을 구하는 메서드 구현을 해보자.
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); }
위에 파일 저장 메서드는 1개의 파일이 넘어올 때만 적용이 되므로 파일 다중 업로드를 처리할 수 있는 메서드를 구현해보자.
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; }
▶ List<UploadFile> storeFileResult = new ArrayList<>();
- 실제 파일명과 서버에서 관리하는 파일명을 저장할 List 컬렉션▶ for - each로 위에서 작성한 메서드를 이용하면 multipartFiles 리스트에
저장되어있는 파일들을 저장할 수 있다.
멀티 파트 전송으로 넘어온 데이터 저장 Form 클래스 생성
이제 html에서 HTTP Form 멀티파트 전송으로 넘어온 데이터들을 저장할 Form 클래스를 만들어 준다.
@Data public class ItemForm { private Long itemId; private String itemName; private List<MultipartFile> imageFiles; private MultipartFile attachFile; }
컨트롤러 구현
컨트롤러는 파라미터로 바인딩된 ItemForm으로 위에서 구현한 메서드들을 이용하면 된다.
@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(); item.setItemName(form.getItemName()); item.setAttachFile(attachFile); item.setImageFiles(storeImageFiles); itemRepository.save(item); redirectAttributes.addAttribute("itemId", item.getId()); return "redirect:/items/{itemId}"; } }
등록 폼 HTML
- multiple="multiple"
- 파일 다중 업로드에 사용
<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> 상품명: <span th:text="${item.itemName}">상품명</span><br/> <img th:each="imageFile : ${item.imageFiles}" th:src="|/images/${imageFile.getStoreFileName()}|" width="300" height="300"/> </div> <!-- /container -->
▶ th:src="|/images/${imageFile.getStoreFileName()}|"
- 해당 URL로 서버에 요청하여 이미지를 가져온다.아직 업로드된 이미지를 서버에서 가져오는 처리를 하지 않았기 때문에 아무것도 출력이 안된 것을 볼 수 있다.
컨트롤러
뷰 템플릿에서 요청한 이미지의 URL 요청 처리하기 위해 컨트롤러에 이미지를 @Responsebody에 담아 응답해준다. 반환 객체인 UrlResource는 파라미터로 전달받은 path를 가지고 이미지 파일을 읽어서 @ResponseBody로 이미지 바이너리를 반환해준다.(정확히는 ResourceHttpMessageConverter 리소스 컨버터가 해준다)
<img>태그에서는 응답으로 전달받은 이미지 바이너리를 읽어서 이미지로 변환하게 해 준다.
@ResponseBody @GetMapping("/images/{filename}") public Resource downloadImage(@PathVariable String filename) throws MalformedURLException { //"file:지정경로/서버에서 관리하는 파일명" return new UrlResource("file:" + fileStore.getFullPath(filename)); }
▶ UrlResource(String path)
- URL path를 가지고 UrlResource를 생성이제 서버를 가동하고 이미지를 업로드하면 업로드한 이미지가 웹페이지에 잘 출력된 모습을 볼 수 있다.
업로드된 파일 뷰에서 다운로드하기
이제 업로드된 파일을 웹페이지에서 다운로드하는 방법을 알아보자.
뷰 템플릿 생성
위 뷰 템플릿에 코드를 추가해줬다.
</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>
▶ th:href="|/attach/${item.id}|"
- 서버에 요청하여 해당 경로의 파일을 가져온다.
▶ th:text="${item.getAttachFile().getUploadFileName()}"
- 화면에 어떤 파일명인지 출력
아직 <a> 태그를 클릭했을 때 해당 경로로 파일을 가져오는 작업을 안 해줬기 때문에 오류가 난다.
컨트롤러
파일 다운로드하는 작업은 처리할 부분이 많다. 요청 메시지에 Content-Disposition 헤더를 추가해주고 한글의 경우 인코딩이 잘못되어 깨지는 경우가 있기 때문에 인코딩 처리까지 해주는 게 좋다.
@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 encodedUploadFileName = UriUtils.encode(uploadFileName, StandardCharsets.UTF_8); String contentDisposition = "attachment; filename=\"" + encodedUploadFileName + "\""; return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition) .body(resource); }
▶ UrlResource resource = new UrlResource("file:" + fileStore.getFullPath(storeFileName));
- 응답 바디에 파일을 파이너리를 담기 위해 UrlResource 객체 생성
▶ String encodedUploadFileName = UriUtils.encode(uploadFileName, StandardCharsets.UTF_8);
- 한글이 깨질 수 있기 때문에 인코딩 처리
▶ String contentDisposition = "attachment; filename=\"" + encodedUploadFileName + "\"";
- 다운로드가 되도록 설정
- attachment; filename="업로드 파일명"
▶ return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition)
.body(resource);- ok() : 상태 코드 200 설정
- headr() : 메시지 헤더에 헤더 추가
- body() : 파일이 바이트코드로 인코딩되어 바디에 넣어준다.
이제 서버를 가동하고 이미지를 업로드하면 업로드한 이미지가 웹페이지에서 다운로드가 잘 되는 것을 볼 수 있다.
참고
만약 응답 헤더에 Content-Disposition을 추가하지 않으면
아래 이미지와 같이 다운로드가 되지 않고 해당 파일을 출력해준다.728x90반응형'Spring > Spring MVC' 카테고리의 다른 글
- multiple="multiple"