파일 업로드

이동건 (불꽃냥펀치)·2025년 2월 11일
0

파일 업로드란?
HTML 폼 전송 방식

  • application/x-www-form-urllencoded
  • multipart/form-data

application/x-www-form-urllencoded 방식은 HTML 폼 데이터를 서버로 전송하는 가장 기본적인 방법이다. Form 태크에 별도의 enctype이 없으면 웹 브라우저는 요청 HTTP 헤더에 다음 내용을 추가한다.

Content-Type: application/x-www-form-urlencoded

그리고 폼에 입력한 전송할 항목을 HTTP Body에 문자로 username=kim&age=20 같이 &로 구분해서 전송한다. 파일을 업로드하기 위해서는 파일은 문자가 아니라 바이너리 데이터를 전송해야 한다는 것을 알아야한다. 또한 보통 폼을 전송할 때는 파일만 전송되지 않음을 알아야한다.

- 이름 
- 나이
- 첨부파일

이름과 나이는 문자로 전송하고 첨부파일은 바이너리 데이터로 전송해야한다. 문자와 바이너리를 동시에 전송해야 하는 상황에서 문제가 발생한다. 이 문제를 해결하기 위해 HTTP는 multipart/form-data라는 전송 방식을 제공한다.

multipart/form-data 방식

이 방식을 사용하려면 Form 태그에 별도의 enctype="multipart/form-data"를 지정해야한다.
multipart/form-data 방식은 다른 종류의 여러 파일과 폼의 내용 또한 전송 할 수있다.

폼의 입력 결과로 생성된 HTTP 메시지를 보면 각각의 전송항목이 구분되어있다. content-disposition이라는 항목별 헤더가 추가되어 있고 여기에 부가 정보가 있다.



서블릿과 파일 업로드

Controller 예제

@Slf4j
@Controller
@RequestMapping("/servlet/v1")
public class ServletUploadControllerV1 {
    @GetMapping("/upload")
    public String newFile() {
        return "upload-form";
	}
    @PostMapping("/upload")
    public String saveFileV1(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);
        return "upload-form";
	}
}

request.getParts() : multipart/form-data 전송 방식에서 각각 나누어진 부분을 받아서 확인할 수 있다.

업로드 사이즈 제한

 spring.servlet.multipart.max-file-size=1MB
 spring.servlet.multipart.max-request-size=10MB

파일을 업로드하기 위해서는 실제 파일이 저장되는 경로가 필요하다.

application.properties

file.dir=파일 업로드 경로 설정(예): /Users/kimyounghan/study/file/
  • 해당 경로에 실제 폴더를 미리 만들어둬야 한다
  • application.properties에서 설정할때 마지막에 /가 포함된 것에 주의해야한다

파일 경로 추가된 Controller

 @Slf4j
 @Controller
 @RequestMapping("/servlet/v2")
 public class ServletUploadControllerV2 {
    @Value("${file.dir}")
    private String fileDir;
    @GetMapping("/upload")
    public String newFile() {
        return "upload-form";
}
    @PostMapping("/upload")
    public String saveFileV1(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));
			}
		//편의 메서드
		//content-disposition; filename
		log.info("submittedFileName={}", part.getSubmittedFileName()); 
    	log.info("size={}", part.getSize()); //part body size
		//데이터 읽기
		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={}", fullPath);
                 part.write(fullPath);
             }
		}
         return "upload-form";
     }
}
@Value("${file.dir}")
 private String fileDir;
  • application.properties에서 설정한 file.dir값을 주입한다

Part 주요 메서드

  • part.getSubmittedFileName() : 클라이언트가 전달한 파일명
  • part.getInputStream():Part의 전송 데이터를 읽을 수 있다
  • part.write(...): Part를 통해 전송된 데이터를 저장할 수 있다



스프링과 파일 업로드

스프링은 MultipartFile이라는 인터페이스로 멀티파트 파일을 매우 편리하게 지원한다

 @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";
     }
}
  • @RequestParam MultipartFile file: 업로드하는 HTML Form의 name에 맞추어 @ReqeustParam을 적용하면된다. 추가로 @ModelAttribute에서도 MultipartFile을 동일하게 사용할 수 있다.

MultipartFile 주요 매서드

  • file.getOriginalFilename() : 업로드 파일 명
  • file.transferTo(...) : 파일 저장



예제로 알아보는 파일 업로드/다운로드

상품 도메인

 @Data
 public class Item {
     private Long id;
     private String itemName;
     private UploadFile attachFile;
     private List<UploadFile> imageFiles;
}

ItemRepository

@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);
	} 
}

업로드 파일 정보 보관

 @Data
 public class UploadFile {
     private String uploadFileName;
     private String storeFileName;
     public UploadFile(String uploadFileName, String storeFileName) {
         this.uploadFileName = uploadFileName;
         this.storeFileName = storeFileName;
	} 
}

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;
     }
     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);
	} 
}

MultipartFile을 서버에 저장하는 역할을 담당하낟

  • createStoreFileName(): 서버 내부에서 관리하는 파일명은 유일한 이름을 생성하는 UUID를 사용해서 충돌하지 않게 한다.
  • extractExt(): 확장자를 별도로 추출해서 서버 내부에서 관리하는 파일명에도 붙여준다. 예를 들어서 고객이 a.png 라는 이름으로 업로드 하면 51041c62-86e4-4274-801d-614a7d994edb.png 와 같이 저장한다.

ItemForm

 @Data
 public class ItemForm {
     private Long itemId;
     private String itemName;
     private List<MultipartFile> imageFiles;
     private MultipartFile attachFile;
}

상품 저장용 폼이다

  • List<MultipartFile> imageFiles: 이미지를 다중 업로드하기 위한 형식
  • MultipartFile attachFile : 멀티파트는 @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());
//데이터베이스에 저장
		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 {
        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 encodedUploadFileName = UriUtils.encode(uploadFileName,StandardCharsets.UTF_8);
        String contentDisposition = "attachment; filename=\"" +encodedUploadFileName + "\"";
        return ResponseEntity.ok()
                .header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition)
                .body(resource);
	}
}








출처: https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2/dashboard

profile
자바를 사랑합니다

0개의 댓글

관련 채용 정보