[ Spring ] 파일 업로드

한대희·2023년 11월 7일

✅ HTML form 데이터 전송 방식

  • 일반적으로 사용하는 HTML form을 통한 파일 업로드 방식은 두가지다.

1. application/x-www-form-urlencoded

  • 이 방식은 HTML Form 데이터를 서버로 전송 하는 가장 기본적인 방식이다.
  • 아래와 같이 데이터를 보내면 application/x-www-form-urlencoded 방식으로 데이터를 보내게 된다.
<form action="/save" method="post">
	<input type="text" name="username" />
    <input type="text" name="age" />
    <button type="submit"> 전송 </button>
</form>    
  • 위의 경우 전송 버튼을 클릭 하면 웹 브라우저는 HTTP메시지의 헤더에 아래와 같은 내용을 추가한다.
// 전송방식, 요청 url, 프로토콜 정보
POST /save HTTP/1.1

// 보내는 곳의 ip
Host: localhost:8080

// body의 데이터 형식
ContentType : application/x-www-form-urlencoded

// body 데이터들을 &로 구분하여 전송한다.
username=hdh&age=30
  • 하지만 파일을 업로드 하려면 문자가 아니라 바이너리 데이터를 전송해야 한다. 따라서 문자를 전송하는 이 방식으로 파일을 전송하기는 어렵다.
  • 또한 보통 Form 데이터를 전송할 때 파일만 전송하는 것이 아니라 부가적인 정보 즉, 이름/나이 등등도 전달할 수 있기 때문에 위의 방식으로는 문자와 바이너리 데이터를 동시에 전송할 수 없다.
  • 이 문제를 해결 하기 위해 multipart/form-data 이라는 전송 방식을 제공 한다.

2.multipart/form-data

  • 이 방식을 사용하려면 Form 태그에 별도로 enctype="multipart/form-data"라는 속성을 지정해야 한다.
  • multipart/form-data 방식은 다른 종류의 여러 파일과 폼의 내용 함께 전송할 수 있다. (그래서 이름이 multipart 이다. )
<form action="/save" method="post" enctype="multipart/form-data">
	<input type="text" name="username" />
    <input type="text" name="age" />
    <input type="file" name="file1 />
    <button type="submit">전송 </button>
</form>    
  • 위와 같이 요청을 하면 HTTP 요청 메세지는 아래와 같이 구성 된다.
// 전송방식, 요청 url, 프로토콜 정보
POST /save HTTP/1.1

// 보내는 곳의 ip
Host: localhost:8080


Content-Type : multipart/form-data, boundary=-----X
Content-Length: 10457

// 🥊아래와 같이 각각의 전송 항목을 구분한다.
// 🥊각각의 항목의 헤더에는 Content-Disposition이라는 항목이 들어 가고
// 🥊여기에 부가 정보가 들어 간다.
// 🥊이렇게 항목별로 구분하여 다른 형식의 데이터를 한번에 전송하는 것이다.

-----XXX
Content-Disposition: form-data; name="username"

hdh
-----XXX
Content-Disposition: form-data; name="age"

30
-----XXX
Content-Disposition: form-data; name="file1"; filename="hello.png"
Content-Type : image/png

321321653216546asdf65w432e1651
-----XXX--
30
  • 그렇다면 이렇게 복잡한 HTTP메시지를 서버에서 어떻게 사용할 수 있을까?
  • multipart로 보낸 데이터를 받아 보기 위해 먼저 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 {
        //🥊 전달 받은 request를 출력
        log.info("request={}", request);
        
        //🥊 입력 폼에서 전달한 itemName을 가져 온다.
        String itemName = request.getParameter("itemName");
        log.info("itemName={}", itemName);

        //🥊 getParts가 multipart로 구성된 http 메세지에서 -----X로 구분된 항목들을 의미한다.        
        Collection<Part> parts = request.getParts();
        log.info("parts={}", parts);
        return "upload-form";
    }
}
  • 만들어 둔 입력 폼 html 파일
<!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>
    
    //🥊 multipart로 데이터 전송
    <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>
  • 파일 이름과 파일을 선택하여 제출하기를 누르면 이제 로그가 찍힐 것이다.
request=org.springframework.web.multipart.support.StandardMultipartHttpServletRequest@5aea357d
itemName=��ǰA
//🥊 두개의 part가 등록 된 거 확인
parts=[org.apache.catalina.core.ApplicationPart@726ffff8, org.apache.catalina.core.ApplicationPart@37a4e696]

multipart 사용 옵션

  • 먼저 multipart에는 사이즈 제한 옵션이 있다. 큰 파일을 무제한으로 업로드하게 둘 수는 없기 때문이다.
// 파일 하나의 최대 사이즈, 기본 1MB
max-file-size
// 멀티파트 요청 하나에 여러개의 파일이 들어갈 텐 데 그 전체 합, 기본 10MB
max-request-size

서버에 업로드

  • 먼저 실제 파일을 서버에 업로드 할려면 파일이 저장되는 경로가 필요하다.
  • application.properties에 가서 파일 경로를 입력해줘야 한다.
// application.properties에 아래와 같이 입력
// 끝에 '/'를 붙여 줘야 한다.
file.dir=D:/localUpload/
  • 컨트롤러를 만들어 보자.
@Slf4j
@Controller
@RequestMapping("/servlet/v2")
public class ServletUploadControllerV2 {
    // 🥊 application.properties에 입력한 파일 저장 장소의 경로를 가져 온다.
    // 🥊 @Value(${})를 사용하면 application.properties이 있는 속성을 가져올 수 있다.
    @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);


        // 🥊 http 메서드의 각각의 part의 이름를 가져오기 위해 반복문을 사용
        for (Part part : parts) {
            log.info("==== PART ====");
            log.info("name={}", part.getName());

            // 🥊 http전체 메세지의 헤더가 아닌 각각의 part도 헤더와 바디로 구성되어 있음
            // 🥊 그 정보를 가져옴
            Collection<String> headerNames = part.getHeaderNames();
            for (String headerName : headerNames) {
                log.info("header {}: {}", headerName,
                        part.getHeader(headerName));
            }
            
            // 🥊 편의 메서드
            // 1. 제출한 파일 이름과 content-body의 크기 가져오기
            //요청한 http메시지를 보면 입력한 상품 이름과 파일
            // 각각 content-disposition을 가지고 있고, 파일에는 filename과 같은
            // 좀 더 자세한 정보가 있다. 그 정보들을 가져 오는 것
            log.info("submittedFileName={}", part.getSubmittedFileName());
            log.info("size={}", part.getSize()); //part body size

            // 🥊 2. 데이터 읽기
            // spring에서는 inputStream을 쉽게 읽을 수 있도록 StreamsUtils라는 메서드를
            // 제공한다. 해당 메서드를 통해 inputStream값을 string으로 바꾸고, 언어세트를 지정해 준것
            InputStream inputStream = part.getInputStream();
            String body = StreamUtils.copyToString(inputStream,
                    StandardCharsets.UTF_8);
            log.info("body={}", body);

            // 🥊 파일에 저장하기
            if (StringUtils.hasText(part.getSubmittedFileName())) {
                // 저장될 파일의 경로를 입력한다.
                // application.properties에 지정한 경로에 파일의 이름을 붙인 것 (그게 그 파일의 경로이기 때문)
                String fullPath = fileDir + part.getSubmittedFileName();
                log.info("파일 저장 fullPath={}", fullPath);
                part.write(fullPath);
            }
        }
        return "upload-form";
    }
}
  • 여기까지 서블릿을 만들어 파일을 업로드 해보았는데, 사용했던 Part는 편리하기는 하지만 HttpServletRequest를 사용해야 하고, 파일 부분만 따로 구분 하려면 여러가지 코드를 추가해야 했다.
  • 이러한 불편함을 스프링을 사용한다면 해소 할 수 있다.

💡스프링을 통한 파일 업로드

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

파일 업로드 다운로드

요구사항

  • 상품을 관리 ( 상품 이름, 첨부파일 하나, 이미지 파일 여러개 )
  • 첨부파일을 업로드 하고 다운로드 할 수 있다.
  • 업로드한 이미지를 웹 브라우저에서 확인할 수 있다.
profile
개발 블로그

0개의 댓글