≣ 목차
파일 업로드는 현대 웹 애플리케이션에서 필수적인 기능 중 하나입니다. 사용자들은 다양한 파일을 업로드하여 정보를 공유하고, 데이터를 저장하며, 협업을 원활하게 진행할 수 있습니다. 특히, 클라우드 서비스와 같은 플랫폼에서는 파일 업로드 기능이 더욱 중요합니다. 파일 업로드 시, 일반적인 데이터 전송 방식과는 달리 바이너리 데이터를 전송해야 한다는 점에서 특별한 처리가 필요합니다. 그리고 일반적으로 HTML 폼을 통해 첨부 파일을 전송할 때는 파일 자체와 함께 다른 데이터(예: 사용자 입력 정보)도 함께 전송되기 때문에, 파일은 바이너리 데이터로, 다른 데이터는 문자로 전송해야 하는 요구사항이 발생합니다. 이를 해결하기 위해 HTTP는multipart/form-data라는 전송 방식을 제공합니다.
해당 포스팅에서는 multipart/form-data를 중심적으로 알아보겠습니다.
1. 파일 업로드의 기본 개념
1-1 파일 크기 제한
파일 업로드 시 고려해야 할 중요한 요소 중 하나는 파일 크기 입니다. 용량이 큰 파일을 업로드할 수 있다면 서버 성능에 직접적인 영향을 미칠 수 있으므로 업로드 크기를 제한할 수 있습니다.
spring.servlet.multipart.max-file-size=1MB
spring.servlet.multipart.max-request-size=10MB
- max-file-size: 파일 하나의 최대 크기 #기본 1MB
- max-request-size : 멀티파트 요청 하나에 여러 파일을 업로드 할 수 있는데, 그 전체 합 #기본 10MB
지정한 용량을 넘으면 예외(SizeLimitExceededException)가 발생합니다.
1-2 서버와 클라이언트 간의 데이터 전송 방식
파일 업로드는 서버와 클라이언트 간의 데이터 전송 방식을 통해 이루어집니다. 일반적으로 HTTP/HTTPS 프로토콜을 사용하며, 다음과 같은 방식이 있습니다.
1. Form 기반 업로드
- HTML Form을 사용하여 파일을 업로드합니다.
- 예: <form enctype="multipart/form-data">`
2. AJAX 업로드
- 페이지를 새로 고침하지 않고 파일을 업로드할 수 있습니다.
- 예: XMLHttpRequest 객체를 사용한 비동기 업로드
2. 간단한 파일 업로드 구현
2-1 Servlet를 이용한 구현
환경 설정 파일(properties) # 실제 파일이 저장되는 폴더
file.dir=C:/study/images/
- 해당 경로에 폴더가 존재해야 합니다.
- 경로를 설정할 때 마지막에 /(슬래시)가 무조건 존재해야 합니다.
Controller
@Slf4j
@Controller
@RequestMapping("/servlet")
public class ServletUploadController {
// properties파일에서 설정한 값 주입
@Value("${file.dir}")
private String fileDir;
@GetMapping("/upload")
public String newFile() {
return "upload-form";
}
@PostMapping("/upload")
public String saveFile(HttpServletRequest request) throws ServletException, IOException {
log.info("request={}", request);
String itemName = request.getParameter("itemName");
log.info("itemName={}", itemName);
Collection<Part> parts = request.getParts();
log.info("parts={}", parts);
for (Part part : parts) {
log.info("========PART=======");
log.info("name={}", part.getName());
Collection<String> headerNames = part.getHeaderNames();
for (String headerName : headerNames) {
log.info("header {}: {}", headerName, part.getHeader(headerName));
}
// 파일 이름, 크기 읽기
log.info("submittedFilename={}", part.getSubmittedFileName()); // 클라이언트가 입력한 파일 명
log.info("size={}", part.getSize());
// 데이터 읽기
InputStream inputStream = part.getInputStream();
String body = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
log.info("body={}", body);
// 파일 저장하기
if (StringUtils.hasText(part.getSubmittedFileName())) {
String fullPath = fileDir + part.getSubmittedFileName();
log.info("파일 저장 주소={}", fullPath);
part.write(fullPath); // 전송한 데이터를 저장
}
}
return "upload-form";
}
multipart/form-data 형식은 전송 데이터를 각각 part로 나눠서 전송합니다. parts에는 이렇게 나누어진 데이터를 모아둔 집합입니다.
2-2 Spring를 이용한 구현
@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("multipart={}", file);
if (!file.isEmpty()) {
String fullPath = fileDir + file.getOriginalFilename(); // 파일 명
log.info("파일 저장 fullPath={}", fullPath);
file.transferTo(new File(fullPath)); // 파일 저장
}
return "upload-form";
}
}
스프링이 제공하는 @RequestParam, @ModelAttribute는 multipart/form-data name에 맞춰 바인딩 해줍니다.
자세한 설명은 아래 블로그 참고해주세요~~~!!!
https://codinghejow.tistory.com/370
[Spring] @ModelAttribute
🌞 들어가기 앞서 스프링에서 요청한 파라미터를 바인딩하는 방법은 여러 가지 있다. 오늘은 그중에서 @ModelAttribute에 대해서 이야기해 볼 생각이다. 사용은 쉽지만 주의할 점이 있기 때문에 어
codinghejow.tistory.com
3. 파일 업로드, 다운로드 예제
해당 코드는 데이터 관리를 위해 데이터 베이스를 사용하지 않습니다. 대신 Java에서 직접 데이터를 처리합니다.
요구사항
- 이미지 파일 여러 개를 업로드 할 수 있어야 된다.
- 첨부 파일을 업로드 및 다운로드 할 수 있어야 된다.
- 업로드 한 이미지를 웹 브라우저에서 확인할 수 있어야 된다.
데이터 저장소
@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); // storeFilename 파일 명
multipartFile.transferTo(new File(getFullPath(storeFileName)));
return new UploadFile(originalFilename, storeFileName);
}
// 내부에서 관리할 파일 명을 UUID를 이용해서 생성
private String createStoreFileName(String originalFilename) {
String uuid = UUID.randomUUID().toString();
String ext = extractExt(originalFilename);
return uuid + "." + ext;
}
// 확장자 추출
private String extractExt(String originalFilename) {
int pos = originalFilename.lastIndexOf(".");
return originalFilename.substring(pos + 1);
}
}
FileStore클래스는 파일을 저장하는 역할을 수행하면서, 확장자 추출, UUID를 이용한 파일 명 생성 메소드를 제공합니다. 이미지 파일이 여러 개 업로드 할 수 있으므로 mutipartFile이 한 개인 경우랑 여러 개인 경우를 구별해서 메소드를 생성했습니다.
Controller
@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}";
}
@GetMapping("/items/{id}")
public String items(@PathVariable Long id, Model model
) {
Item item = itemRepository.findById(id);
model.addAttribute("item", item);
return "item-view";
}
@GetMapping("/images/{filename}")
public ResponseEntity<Resource> displayImage(@PathVariable String filename) throws IOException {
String fullPath = fileStore.getFullPath(filename);
MediaType mediaType = MediaType.parseMediaType(Files.probeContentType(Paths.get(fullPath)));
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_TYPE, mediaType.toString())
.body(new UrlResource("file:" + fullPath));
}
@GetMapping("/attach/{itemId}")
public ResponseEntity<Resource> downloadAttach(@PathVariable Long itemId) throws MalformedURLException {
Item item = itemRepository.findById(itemId);
String uploadFileName = item.getAttachFile().getUploadFileName();
String storeFileName = item.getAttachFile().getStoreFileName();
UrlResource urlResource = 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.status(HttpStatus.OK)
.header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition)
.body(urlResource);
}
}
파일 업로드 및 다운로드를 처리하는 컨트롤러로써 각 각 메소드의 기능은 아래와 같습니다
- newItem: 새 아이템을 추가하기 위한 등록 페이지를 반환합니다.
- saveItem: 상품을 등록하면 저장소에(FileStore) 사진 및 첨부파일을 저장하며 item 정보를 저장합니다.
- items: 특정 아이템을 조회하여 뷰에 전달합니다.
- displayImage: 이미지 파일을 조회하여 웹에서 표시합니다.
- downloadAttach: 아이템의 첨부 파일을 클릭하면 다운로드할 수 있도록 합니다.
'Spring' 카테고리의 다른 글
[Spring]PathPattern 공식 문서 내용에 대한 예시 (0) | 2024.08.01 |
---|---|
[Spring] Filter(필터) VS Interceptor(인터셉터) 정리 및 차이 (1) | 2024.08.01 |
[Spring+Thymeleaf] Spring Converter 및 Formatter 정리 (0) | 2024.07.11 |
[Spring] 스프링이 제공하는 ExceptionResolver에 대하여 (0) | 2024.07.08 |
[Spring] 스프링이 제공하는 HandlerExceptionResolver에 대해서(ExceptionHandlerExceptionResolver) (0) | 2024.07.04 |