파일 업로드 및 다운로드 예제
상품 관리
상품 이름
첨부 파일 한 개
이미지 파일 여러 개
첨부 파일을 업로드, 다운로드 할 수 있다.
업로드 한 이미지를 웹 브라우저에서 확인할 수 있다.
Item, UploadFile
@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
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);
}
}
UploadFile은 파일 이름을 두 가지로 구분한다.
uploadFileName
고객이 업로드 한 파일명
storeFileName
서버 내부에서 관리하는 파일명
여러 고객이 같은 이름을 쓰면 충돌이 나기 때문에 별도로 관리한다.
FileStore
@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;
}
// multipart 파일을 uploadFile로 변환한다.
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);
}
// UUID를 이용해 파일 이름을 생성한다. 단, 어떤 파일인지 알기 위해 확장자는 남겨둔다.
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);
}
}
multipart 파일을 서버에 저장한다.
createStoreFileName()
서버에 저장할 파일 이름은 UUID를 써서 충돌을 방지한다.
extractExt()
확장자를 따로 빼서 서버에 저장할 파일명에 붙여준다.
ex. 51041c62-86e4-4274-801d-614a7d994edb.png
ItemForm
@Data
public class ItemForm {
private Long itemId;
private String itemName;
// 요구 사항에서 이미지는 여러 개를 첨부할 수 있으므로 MultipartFile를 리스트로 받는다.
private List<MultipartFile> imageFiles;
private MultipartFile attachFile;
}
상품 저장을 요청하는 객체를 생성한다.
Item 도메인 객체와는 달리 UploadFile 대신 MultipartFile로 받는다.
MultipartFile은 @ModelAttribute를 통해 사용할 수 있다.
ItemController
@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());
// 사실 파일은 DB가 아니라 스토어 서비스에 저장한다. DB에 저장하는 건 파일을 저장한 곳의 상대 경로다.
item.setAttachFile(attachFile);
item.setImageFiles(storeImageFiles);
itemRepository.save(item);
redirectAttributes.addAttribute("itemId", item.getId());
return "redirect:/items/{itemId}";
}
}
등록과 저장 화면을 위한 컨트롤러를 정의한다.
저장한 아이템에 대한 페이지로 리다이렉트 한다.
@Slf4j
@Controller
@RequiredArgsConstructor
public class ItemController {
...
// 고객이 업로드 한 파일 리스트를 보여준다.
@GetMapping("/items/{id}")
public String items(@PathVariable Long id, Model model) {
Item item = itemRepository.findById(id);
model.addAttribute("item", item);
return "item-view";
}
// 파일 리스트에서 이미지를 보여줄 때 HTML img 태그에 넣을 이미지 주소를 반환한다.
@ResponseBody
@GetMapping("/images/{filename}")
public Resource downloadImage(@PathVariable String filename) throws MalformedURLException {
// UrlResource로 파일을 읽어서 @ResponseBody로 이미지 바이너리를 반환한다.
// fullPath로 전체 경로 /Users/... 를 가져온다.
return new UrlResource("file:" + fileStore.getFullPath(filename));
}
// 파일을 다운로드 할 때 사용한다.
@GetMapping("/attach/{itemId}")
public ResponseEntity<Resource> downloadAttach(@PathVariable Long itemId) throws MalformedURLException {
// itemId로 요청을 받으면 접근 권한을 체크하는 등의 로직을 추가할 수 있다.
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 + "\"";
// content-disposition 헤더를 추가하지 않으면 다운로드 하려고 파일명을 누르면
// 다운로드가 되지 않고 열기로 작동해 파일 내용이 화면에 노출된다.
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition)
.body(resource);
}
}
고객이 업로드 한 파일과 이미지를 보여준다.
@GetMapping("/images/{filename}")
태그로 이미지를 조회할 때 사용한다.
UrlResource
저장했던 이미지 파일을 읽는다.
@ResponseBody
이미지를 바이너리로 반환한다.
@GetMapping("/attach/{itemId}"
파일을 다운로드 한다.
고객이 업로드 했던 이름으로 화면에 노출한다.
Content-Disposition 헤더를 적용해야 다운로드가 정상적으로 실행된다.
Last updated