파일 업로드

바그다드·2023년 6월 8일
0

파일 업로드

목록 보기
1/2

폼 전송 방식

HTML에서 폼을 이용한 전송 방식은 2가지가 있다.
1. application/x-www-form-urlencoded

우리가 일반적으로 가장 자주 사용하는 기본적인 방법인데 각 필드의 값은 parameter=value¶meter2=value2의 형태로 넘어가는데,
쿼리스트링을 이용한 파라미터 전송방식과 형태가 유사한 것을 알 수 있다.

  • 그런데 프로필 등록을 예시로 생각해보면, 나이와 이름 등의 문자 데이터와 프로필 사진 데이터가 동시에 전송이 되야 하는데, 문제는 나이와 이름 등은 문자 데이터이지만 프로필 사진은 바이너리로 전송해야 한다는 것이다.
    이러한 문제 때문에 http는 multipart/form-data 전송 방식을 제공한다.
  1. multipart/form-data
  • 폼 속성을 보면 enctype으로 "multipart/form-data"라는 값이 부여되어 있는 것을 확인할 수 있다.
  • 이로 인해 생성된 http메세지를 확인해보면 각 필드에는 Content-Disposition이라는 헤더가 생성되어 부가정보가 추가되어 있는 것을 확인할 수 있다.
    part는 다시 헤더와 바디로 구분이 된다!
    위 그림의 경우 username, age, file 필드 모두 form-data;name;이 추가 되어 있는데,
    file필드의 경우에만 filename이 추가되고,
    Content-Type이라는 헤더가 추가된 것을 확인할 수 있다.
  • 각 파트는 폼 데이터별로 구분자가 나누고 있고, 마지막 파트의 끝에는 구분자+'--'형태의 구분자가 들어간다.

Servlet을 이용한 파일 업로드

  • 먼저 서블릿을 이용한 파일 업로드에 대해서 알아보자.

1. 업로드 파일 로그로 찍어보기

업로드에 앞서 우리가 업로드한 데이터가 어떤식으로 넘어가는지 또 어떤식으로 확인하고 활용할 수 있는지 먼저 알아보자.

컨트롤러 생성

@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로 전송된 http바디를 가지고 온다.
    여기서 Part가 http바디의 Parts중 하나를 뜻한다.
  • 또한 request.getParts()의 리턴 타입은 Collection이므로 Collection로 설정해주자!

뷰 생성

  • 파일 업로드를 위해 폼을 생성해주자
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8">
</head>
<body>
<div class="container">
    <div class="py-5 text-center">
        <h2>상품 등록 폼</h2>
    </div>
    <h4 class="mb-3">상품 입력</h4>
    <form th:action method="post" enctype="multipart/form-data">
        <ul>
            <li>상품명 <input type="text" name="itemName"></li>
            <li>파일<input type="file" name="file" ></li>
        </ul>
        <input type="submit"/>
    </form>
</div> <!-- /container -->
</body>
</html>
  • enctype="multipart/form-data"로 설정된 것을 확인하자
  • file 데이터는 type="file"로 설정해주자

설정 추가

  • application.properties에 다음 설정 값을 추가해주자
    http요청 메세지를 로그로 남겨준다.
logging.level.org.apache.coyote.http11=debug

이제 테스트를 해보자

  • 요청 헤더를 확인해보면 Content-Type은 multipart/formdata로 설정되어 있으며, 각 part를 나누는 구분자(boundary)도 확인할 수 있다.
    이제 콘솔을 확인해보자

  • 바운더리로 각 part가 구분되어 있으며, 파일 데이터의 경우 바이너리 데이터라 이상한 문자로 로그가 남아있는 것을 확인할 수 있다.
    로그를 살펴보면 실제로 Collection타입의 parts변수에 2개의 Part객체가 담겨 있는 것을 확인할 수 있다.

멀티파트 사용 옵션

  1. 크기 제한
#파일 하나의 최대 크기, 기본1mb
spring.servlet.multipart.max-file-size=1MB
#전체 파일의 총합, 기본 10mb
spring.servlet.multipart.max-request-size=10MB
  • 업로드 파일의 크기를 무제한으로 둘 수 는 없기 때문에 위의 설정값을 통해 업로드 파일 크기를 제한해주자
  1. 멀티파트 사용 제한
#기본 true
spring.servlet.multipart.enabled=false
  • 스프링에서 서블릿 컨테이너에 멀티파트를 처리하지 않도록 설정하는 값이다.

MultipartResolver

나중에 나올 MultipartFile을 사용하는 것이 더 편하기 때문에 참고만 하자
1. 멀티파트 처리 설정이 켜져 있다면
2. 스프링의 DispatcherServlet은 MultipartResolver를 실행한다.
3. 멀티파트 리졸버는 요청 타입이 multipart인 경우 서블릿 컨테이너가 전달하는 HttpServletRequest를 MultipartHttpServletRequest로 변환후 이를 반환한다.
- MultipartHttpServletRequest는 HttpServletRequest의 자식 인터페이스로 멀티파트 관련 추가 기능을 제공한다.
4. 멀티파트 리졸버는 MultipartHttpServletRequest의 구현체인 StandardMultipartHttpServletRequest를 반환하고, 이를 이용해 멀티파트와 관련된 기능을 사용할 수 있게 된다.

2. 서블릿으로 파일 업로드 하기

이제 실제 파일을 업로드해보자

1. 파일 저장 경로 설정

  • application.properties에 파일 경로를 설정해주자
#파일 저장 경로
file.dir=D:/SpringStudy/springmvc2/upload/src/main/resources/uploadfiles/
  • 경로 끝에 '/'를 꼭 붙여주자! 안그러면 updatefiles파일이름.확장자 이런식으로 파일이 저장되어 버린다ㅜ

컨트롤러 생성

@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();
            // 바이너리 데이터를 문자로 바꿀 경우에는 CharSet을 설정해주자!
            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);
            }

            inputStream.close();
        }

        return "upload-form";
    }
}
  • @Value("${file.dir}")
    private String fileDir;
    @Value를 이용해 application.properties에서 파일 경로 설정 값을 가져온다.
  • 첫번째 반복문을 통해 각 part에 접근한다.
  • part.HeaderNames()를 이용해 헤더의 각 키 값을 가져오고,
    반복문에서 getHeader()를 이용해 각 헤더 키값과 value를 로그로 남긴다.
  • part.getSubmittedFileName() : 파일 이름을 가지고 온다.
  • part.getSize() : part의 body크기를 가지고 온다.
  • part는 InputStream을 제공하는데, 스프링에서 제공하는 StreamUtils를 활용해 바이너리 데이터를 String 데이터로 가져와 로그로 남긴다.
  • if (StringUtils.hasText(part.getSubmittedFileName()))
    만약 part가 filename이라는 헤더를 가지고 있다면, 이 part는 파일 데이터이므로
    fileDir + part.getSubmittedFileName();
    파일 저장 경로 + 파일 이름으로 전체 경로를 설정한 뒤
    part.write()를 이용해 파일을 저장한다.

이제 테스트를 해보면

설정한 경로에 파일이 저장되어 있는 것을 확인할 수 있다!!

이상으로 서블릿에서 제공하는 part를 통해 일일이 업로드를 구현하는 것보다 편하게 업로드를 구현할 수 있었지만 for문이나 if문을 통해 파일 part만 구분해서 업로드를 구현하기 위한 추가적인 코드가 필요했다. 이번에는 스프링에서 제공하는 기능을 통해 업로드를 구현하는 방법을 알아보자!

스프링을 이용한 파일 업로드

  • 스프링에서는 파일 업로드를 어떻게 구현하는지 코드로 확인해보자

컨트롤러 생성

@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";
    }
}
  • 스프링에서는 아예 MultpartFile이라는 인터페이스를 제공한다. 이를 통해 파일과 관련된 데이터만을 사용할 수 있게 되어 코드가 훨씬 깔끔해진 것을 확인할 수 있다!!!
  • @ModelAttribute도 마찬가지로 MultipartFile을 사용할 수 있다.
profile
꾸준히 하자!

0개의 댓글