파일 업로드

민지·2024년 6월 23일
0

SpringMVC2

목록 보기
6/6

핵심

웹페이지에서 파일업로드하고, 첨부파일 다운로드를 어떻게 하는지, 그 원리와 방법을 정리해보자.


1. HTML form 데이터 전송 2가지 방법

HTML form을 통한 파일업로드를 이해하려면, 먼저 폼에서 전송하는 2가지 방식을 알아야 한다.

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

1. application/x-www-form-urlencoded 방식

application/x-www-form-urlencoded 방식은 우리가 익히 아는, HTML 폼 데이터를 서버로 전송하는 가장 기본적인 방법이다.

form 태그에 별도의 enctype 옵션이 없다면, 웹 브라우저는 요청 HTTP 헤더의 다음의 내용을 추가한다.

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

그리고 폼에 입력한 항목들을 HTTP Body에 문자username=kim&age=20와 같이 &로 구분해서 전송한다.


그런데, 파일을 업로드하려면 문자가 아닌 바이너리 데이터를 전송해야 한다. 또한 보통 폼을 전송할 때는 파일만 전송하는 것이 아니다.

다음의 예시를 보자.

- 이름
- 나이
- 첨부파일

이때 문자(이름,나이)와 바이너리(파일)을 동시에 전송해야 하는 상황이다..

이 문제를 해결하기 위해 HTTP는 multipart/form-data라는 전송방식을 제공했다 !!


2. multipart/form-data

이 방식을 사용하려면 앞선 방식과 달리,
form 태그에 별도의 enctype="multipart/form-data" 을 지정해야 한다.

multipart/form-data는 다른 종류의 여러 파일과 폼의 내용을 같이 전송할 수 있다. (그래서 이름이 multipart임)

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

예제에서는 username, age, file1이 각각 분리되어 있고, 폼의 일반데이터는 각 항목별로 '문자'가 전송되며, 파일의 경우 파일이름과 Content-Type이 추가되고, '바이너리 데이터가 전송'된다.

multipart/form-data는 이렇게 각각의 항목을 구분해서, 한번에 전송하는 것이다.


2. 서블릿 - 파일업로드

참고 설정

# application.properties에서

-- HTTP 요청 메세지 확인가능
logging.level.org.apache.coyote.http11=trace

-- 업로드 사이즈 제한
spring.servlet.multipart.max-file-size=1MB
spring.servlet.multipart.max-request-size=10MB

max-file-size : 파일 하나의 최대 사이즈, 기본 1MB
max-request-size : 멀티파트 요청 하나에 여러 파일을 업로드할 수 있는데, 그 전체의 함. 기본 10MB


서블릿과 파일업로드

서블릿이 제공하는 Part에 대해서 알아보고, 실제 파일도 서버에 업로드해보자.

파일업로드를 하려면, 실제파일저장경로가 필요하다.

-- application.properties

file.dir=파일업로드경로설정

ex) file.dir=/Users/minji/Desktop/spring/study/mvc2/upload/file/

-- 주의사항
	1. 해당경로에 실제 폴더를 미리 만들어둘 것
    2. 경로설정 적을때, 마지막에 / (슬래시)가 포함된 것

upload-form.html

<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>

enctype으로 multipart/form-data를 설정해줬고, input type="file"로 파일1개를 업로드할 수 있게 했다.

ServletUploadController.java

@Slf4j
@Controller
@RequestMapping("/servlet/v2")
public class ServletUploadControllerV2 {

    @Value("${file.dir}") // application.properties에서 설정한 속성값을 가져오자
    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 =====");

            // PART name 출력
            log.info("name : {}", part.getName());

            // PART의 각각의 헤더name - 헤더value 출력
            Collection<String> headerNames = part.getHeaderNames();
            for (String headerName : headerNames) {
                log.info("header {} : {}", headerName,
                        part.getHeader(headerName));
            }

            // 편의메서드
            // content-Disposition ; filename
            log.info("submittedFileName : {}", part.getSubmittedFileName()); // -- cat.jpg (업로드한 파일명)
            log.info("size : {}", part.getSize()); // part body size

            // 데이터 읽기 -- 파일 body 내용 읽어오기
            InputStream inputStream = part.getInputStream();
            // 파일 body부분이 바이너리 내용이니까, 바이너리 -> String으로 형변환
            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";
    }
}

멀티파트 형식은, 전송데이터를 하나하나 각각 부분(Part)로 나누어서 전송한다.
서블릿이 제공하는 Part는 멀티파트 형식을 편리하게 읽을 수 있는 다양한 메서드를 제공했다.

실행
다음의 내용을 전송했다.

  • itemName : 상품A
  • file : 스크린샷.png

결과로그

==== PART ====
name=itemName
header content-disposition: form-data; name="itemName"
submittedFileName=null
size=7
body=상품A

==== PART ====
name=file
header content-disposition: form-data; name="file"; filename="스크린샷.png"
header content-type: image/png
submittedFileName=스크린샷.png
size=112384
body=qwlkjek2ljlese...

파일 저장 fullPath=/Users/kimyounghan/study/file/스크린샷.png

이때, content-disposition 헤더의 name="itemName"이나 name="file"은 upload-file.html 폼에서 input태그에 작성한 name과 일치한다.

실행결과, 파일저장경로에 가보면 실제 파일이 저장된 것을 알 수 있다. 만일 저장이 안되었다면, 파일저장경로를 다시 한번 확인해보자.


서블릿이 제공하는 Part는 편하기는 하지만, HttpServletRequest를 사용해야 하고, 추가로 파일부분을 구분하려면 여러가지 코드를 추가해야 한다.

이번에는, 스프링이 해당부분을 얼마나 편리하게 제공하는지 확인해보자.


3. 스프링 - 파일업로드

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

@Controller
@Slf4j
@RequestMapping("/spring")
public class SpringUploadController {

    @Value("${file.dir}") // application.properties에서 설정한 속성값을 가져오자
    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";
    }
}

업로드하는 HTML form의 name에 맞추어, @RequestParam을 적용해주면 된다.

실행로그

request=org.springframework.web.multipart.support.StandardMultipartHttpServletRequest@5c022dc6

itemName=상품A

multipartFile=org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile@274ba730

파일 저장 fullPath=/Users/kimyounghan/study/file/스크린샷.png

4. 예제로 구현하는 파일업로드, 다운로드

예제의 파일이 많으므로, 각 파일의 흐름과 역할을 정리해보았으니, 실제 코드에서 확인해보자.

깃허브 프로젝트 링크

ㄴmain.java.hello.upload
	ㄴdomain
    	ㄴ Item - 데이터베이스 도메인 !! 
        	(DB에는 파일자체 저장 X, 파일관련정보 저장O)
        ㄴ ItemRepository
        ㄴ UploadFile - File DTO !!
        	(서버에서 관리할 파일명, 사용자가 올린 파일명 저장_
    
    ㄴ file
    	ㄴ FileStore - 파일업로드,다운로드 관련 비즈니스로직
        
    ㄴ controller
    	ㄴ ItemForm - 상품저장용 폼 DTO!! (Item엔디티랑 다름)
    	ㄴ ItemController - 상품업로드폼,상품업로드, 첨부파일 다운로드
profile
배운 내용을 바로바로 기록하자!

0개의 댓글