Spring - 파일 전송(multipart), 파일 업로드, 파일 저장

코찔찔이💻·2023년 2월 16일
1

스프링

목록 보기
1/3

HTML Form Data 전송 방식

1) application/x-www-urlencoded

  • HTML 폼 데이터를 전송하는 가장 기본적인 방법
  • 데이터를 String 형태로 전송
  • enctype 옵션이 없을 경우, default로 해당 전송방식을 헤더에 추가한다.
  • 전송할 데이터를 쿼리 파라미터 형식으로 HTML Body에 넣어 전송
  • 해당 방식은 문자를 전송하는 방식이기 때문에, 파일 전송은 불가능하다.(username=kim&age=20)

2) multipart/form-data 전송 방식

  • 여러 형태의 자료(문자/ 바이너리)를 전송하기 위한 전송 형태
  • Form 태그에 enctype=”multipart/form-data”를 입력해준다.
  • 전송되는 데이터는 Boundary에 의해 나누어지고, 나누어진 것들을 part라고 부른다. 각 part에 해당하는 Header / Body값을 가진 채로 클라이언트에서 서버로 전송

파일 전송 동작 방식

  • multipart/form-data 를 사용할 경우, 스프링의 DispatcherServlet에 의해 standardMultipartHttpServletRequest가 전달
  • multtipart/form-data = true → DispatcherServlet에 의해 MultipartResolver를 실행. 해당 실행으로 인해 MultipartHttpServletRequest 형태로 반환해서 전송

파일 업로드

  • @ModelAttribute를 이용하여 MultiPartFile을 받는다.
  • 해당 객체를 매개변수로 받을 때는 @ModelAttribut를 생략할 수 있다.
  • 파일 업로드에 대한 스프링 설정 정보
    //application.properties
    spring.servlet.multipart.max-file-size=1MB
    spring.servlet.multipart.max-request-size=10MB
    spring.servlet.multipart.enabled=true
    • max-file-size : 파일 하나 당 올릴 수 있는 최대 파일 용량
    • max-request-size: 한번에 올릴 수 있는 전체 최대 파일 용량
    • multipart.enabled : multipart/form-data를 사용할 수 있는지 정하기

파일 업로드 및 파일 저장 시 주의사항

  • 서로 다른 클라이언트가 값을 각각 올리면, 서버는 하나이기 때문에 값이 갱신된다.
  • 이것을 해결하기 위해 파일을 UUID로 저장한다.

FileController.java

@Controller
@RequiredArgsConstructor
public class FileController {
    private final FileService fileService;

    
     
    @GetMapping("/")
    public String uploadFilesList(Model model) throws IOException {
        model.addAttribute("files", fileService.loadAll().map(
                        path -> MvcUriComponentsBuilder.fromMethodName(FileController.class,
                                "serveFile", path.getFileName().toString()).build().toUri().toString())
                .collect(Collectors.toList()));
        return "fileUpload";
    }
}
업로드한 파일들을 웹에서 조회 MvcUriComponentsBuilder -> 복잡한 파라미터를 가진 uri를 생성 , requestMapping 파일 메서드가 존재할 경우, 리스트로 반환하기 위해 files 라는 이름으로 생성하여 modelAttribute로 담아준다.
MvcUriComponentsBuilder -> fromMethodName(methodName,filePath).build().toUri().toString()-> 

builder 패턴으로 생성한 후, uri로 변환한 뒤, 다시 string 타입으로 반환한다.
.collect(Collectors.toList())) -> Collectors 클래스에서 제공하는 toList()메서드로 해당 uri들을 리스트로 반환

loadAll() : 생성 메서드
map() : Stream에서 제공하는 메서드

  @GetMapping("files/{filename:.+}")
  @ResponseBody
  public ResponseEntity<Resource> serveFile(@PathVariable String filename){
      Resource file = fileService.loadAsResource(filename);
      return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,
              "attachment; fileName=\"" + file.getFilename() + "\"").body(file);
  }

파일 다운로드
@ResponseBody -> 포워딩 필요 없이 스스로 요청에 대한 응답(REST API에서 주로 사용)
@PathVariable -> 파라미터를 url의 파라미터값으로 받을 때 사용
{filename(파일명):.+(확장자)}
ResponseEntity : HttpEntity를 상속받아서 Header와 Body를 같이 반환할 수 있다.이때 statusCode도 함께 담을 수 있다.
return 값 : attachment; -> 파일을 다운로드 받을 수 있다. 이때 fileName으로 이름을 정의한다.

@PostMapping("/")
  public String handleFileUpload(@RequestParam("file") MultipartFile file,
                                 RedirectAttributes redirectAttributes){
      fileService.store(file);
      redirectAttributes.addFlashAttribute("message",
              "You successfully uploaded" + file.getOriginalFilename() + "!");
      return "redirect:/";
  }

  @ExceptionHandler(StorageFileNotFoundException.class)
  public ResponseEntity<?> handleStorageFileNotFound(StorageFileNotFoundException exc) {
      return ResponseEntity.notFound().build();
  }

파일 업로드
-MultipartFile 타입으로 file을 받는다.
-RedirectAttributes 에 의해 Post에서도 redirect를 사용할 수 있다.
-addFlashAttribute : flash속성에 객체를 저장.일회성으로 데이터를 전달.redirect 후 소멸.즉, post방식에서 사용해야 하기 때문에
데이터가 남는 방식은 get방식에서 사용 해야한다. 이럴때는 addAttribute를 사용한다.
파일 업로드 성공시, 메시지를 담아 view에 전달.

FileService.java


public interface FileService {

    void init();
    void store(MultipartFile file);

    Stream<Path> loadAll();

    Path load(String filename);

    Resource loadAsResource(String filename);

    void deleteAll();
}

FileServiceImpl.java

    @Override
    public void store(MultipartFile file) {
        String filename = StringUtils.cleanPath(file.getOriginalFilename());
        //StringUtils.cleanPath :불필요한 패스를 정리. 파일 시스템에 저장된 파일 이름을 불러와서 path 주소를 path/.. 로 정리해준다.
        try {
            if (file.isEmpty()) {
                throw new StorageException("Failed to store empty file "+ filename);
            }
            //유효하지 않은 주소 ->..가 들어있는 url을 반환하여 예외 처리(해킹이나 오류 방지)
            if(filename.contains("..")){
                throw new StorageException(
                        "Cannot store file with relative path outside current directory "
                + filename);
            }
            //실행 부분
            //copy 옵션을 주어, 해당 파일의 이름으로 지정한 절대경로에 파일을 복사한다.
            //이때 파일이 존재해도 덮어쓰고 싶다면, StandardCopyOption.REPLACE_EXISTING 옵션을 사용한다.
            try (InputStream inputStream = file.getInputStream()) {
                Files.copy(inputStream,this.rootLocation.resolve(filename),
                        StandardCopyOption.REPLACE_EXISTING);
            }
        }catch (IOException e) {
                throw new StorageException("Faild to store file" + filename, e);
            }
    }

 @Override
        public Stream<Path> loadAll() {
            try {
                return Files.walk(this.rootLocation, 1)
                        .filter(path -> !path.equals(this.rootLocation))
                        .map(this.rootLocation::relativize);
            } catch (IOException e) {
                throw new StorageException("Failed to read stored files", e);
            }

        }

Files.walk -> 파일의 주소 중 시작 파일을 탐색, maxDepth를 1로 지정하면 자기자신과 직전 하위경로까지만 탐색
.filter -> 조건을 지정하여 해당 조건에 맞을 경우 필터링
.map(this.rootLocation::relativize) ->현재 path와 지정한 경로사이의 상대경로를 구한다.
::-> 람다식에서 함수의 결과를 반환

StorageProperties.java

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties("storage")
public class  StorageProperties {
    
	//파일을 저장할 위치
    private String location = "upload-dir";

    
    public String getLocation() {
        return location;  /* Folder location for storing files */
    }
    public void setLocation(String location){
        this.location = location;
    }
}
profile
버티면 다 되는거야.

0개의 댓글