본문 바로가기
Spring&Spring Boot

[Spring Boot] Spring MultipartFile 파일업로드 구현하기

by 피자보다 치킨 2022. 7. 7.

※ 개발 환경:

Back-end

Project: Gradle Project, Language: Java 11, Spring Boot: 2.6.7

Dependencies: SpringWeb, Thymeleaf, Lombok ,Validation, H2 Database, MySQL Driver, Spring Data JPA

Front-end

JavaScript, Thymeleaf, HTML5

 

 

파일은 문자와 다르게 바이너리 데이터를 전송해야 한다.

그리고 보통 폼을 전송할 때 문자와 바이너리를 동시에 전송해야하는 경우가 대부분일 것이다.

이 문제를 해결하기 위해 스프링이 제공하는 multipart/form-data 라는 전송 방식을 사용한다.

 

바이너리 파일(binart file)
바이너리 파일은 데이터의 저장과 처리를 목적으로 
0과 1의 이진 형식으로 인코딩된 파일을 가리킵니다. (텍스트 파일이 아닌 컴퓨터 파일)
프로그램이 이 파일의 데이터를 읽거나 쓸 때는 데이터의 어떠한 변환도 일어나지 않습니다.

 

html

<form action="/save" method="post" enctype="multipart/form-data">
	<input type="text" name="name">
	<input type="file" name="image">
    <button type="submit">전송</button>
</form>

 

데이터를 전송하게 되면 아래와 같은 내용으로 HTTP Body에 담긴다.

HTTP Message Body

------WebKitFormBoundaryMVA4MPoFDDjKPJl2
Content-Disposition: form-data; name="name"

kim!
------WebKitFormBoundaryMVA4MPoFDDjKPJl2
Content-Disposition: form-data; name="image"; filename="사진.jpg"
Content-Type: image/jpeg

... ÿØÿà·'j©?AGÙ'ìÿÙ ...
------WebKitFormBoundaryMVA4MPoFDDjKPJl2--
  • "---xxx"로 영역 구분
  • "Content-Disposition:form-data; data="data" ~ → 영역의 시작(해당 영역에 대한 정보)
  • "---xxx--" 끝 명시

위와 같은 식으로 각각의 항목을 구분해서 한번에 형식이 다른 여러 항목들을 전송할 수 있다.

 

 

 

multipart/form-data 방식

 

1. HTML <form></form>

<form th:action="@{/product/new}" th:object="${form}" method="post" enctype="multipart/form-data">
	<div class="form-group">
    <label th:for="name">대표 이미지</label>
    <input type="file" th:field="*{thumbnail}" class="form-control"
           th:class="${#fields.hasErrors('thumbnail')}? 'form-control fieldError' : 'form-control'"><!--에러 테두리 붉은색으로-->
    <p th:if="${#fields.hasErrors('thumbnail')}" th:errors="*{thumbnail}">Incorrect date</p><!--에러 메시지-->
    </div>
    <button type="submit" class="btn btn-primary">Submit</button>
</form>

우선 이 방식을 사용하려면 Form 태크에 enctype="multipart/form-data"를 추가로 지정해야 한다.

 

2. 파일 저장과 관련된 업무를 처리할 수 있는 Class (컨트롤러에서 넣어줘도 상관없다.)

package project.toyproject.service;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;
import java.util.UUID;

@Component
public class FileUpload {
    /**
     * @param filename: 서버에 업로드되는 파일명
     * @param fileDir: 파일이 저장되는 경로
     */
    public String getFullPath(String filename, String fileDir) {
        return fileDir + filename;
    }

    public String serverUploadFile(MultipartFile multipartFile, String fileDir) throws IOException {
        if (multipartFile.isEmpty()) { //파일 없으면 null 반환
            return null;
        }
        String originalFilename = multipartFile.getOriginalFilename(); //원래 파일명

        String serverUploadFileName = createServerFileName(originalFilename); //uuid 생성해서 뒤에 원래파일명의 확장자명 붙이기
        multipartFile.transferTo(new File(getFullPath(serverUploadFileName, fileDir)));//저장: (서버에 업로드되는 파일명, 업로드 되는 경로)

        return serverUploadFileName;
    }

    //uuid 생성해서 뒤에 원래파일명의 확장자명 붙이기
    private String createServerFileName(String originalFilename) {
        String uuid = UUID.randomUUID().toString();
        String ext = extractExt(originalFilename);
        return uuid + "." + ext;
    }

    //원래 파일명에서 확장자 뽑기(.jpg, .png ...)
    private String extractExt(String originalFilename) {
        int pos = originalFilename.lastIndexOf(".");
        return originalFilename.substring(pos + 1);
    }
}

extractExt(): 원래 파일명에서 확장자를 뽑는다. (.jpg, .pnp ...)

createServerFileName(): 서버 내부에서 관리하는 파일명으로 UUID를 생성한 후 원래 파일명 확장자를 붙인다. (ex: UUID.jpg)

서버에 업로드되는 파일명은 UUID 외에도 "업로드날짜 + 원래파일명" 식으로 해도 상관없다.

하지만 중복된 파일명으로 저장될 아주 작은 가능성을 생각해서는 UUID를 사용하는 것이 좋다. (개인적인 생각)

serverUploadFile(): 파일을 저장한다.

file.transferTo(new File("PATH") 을 이용해 파일을 저장할 수 있다.

사용자가 업로드한 파일명은 file.getOriginalFilename() 으로 받을 수 있다.

 

 

3. Cotroller (파일이 저장되는 경로 구하기)

@PostMapping("/new")
public String create(@Valid @ModelAttribute("form") CreateProductForm form, BindingResult result,
                     @LoginCheck SessionMemberData loginMember,
                     RedirectAttributes redirectAttributes,
                     HttpServletRequest request
) throws IOException {
    if (result.hasErrors()) { //만약에 result 안에 에러가 있으면
        return "product/createProductForm"; //다시 폼으로 이동
    }

    String realPath = request.getSession().getServletContext().getRealPath("/upload/");// 저장 경로
    String uploadFile = fileUpload.serverUploadFile(form.getThumbnail(), realPath);

    //데이터 베이스에 저장
    Long productId = productService.saveProduct(uploadFile);

    redirectAttributes.addAttribute("productId", productId);

    return "redirect:/product/detail/{productId}"; // 상품디테일 페이지로 넘어가게
}

realPath: 파일이 저장되는 경로이다.

 

request.getSession().getServletContext().getRealPath("/"): 내 프로젝트/src/main/webapp 파일 경로이다.

즉, 저 경로에 webapp 파일이 없다면 에러가 나거나 톰캣의 임시폴더에 저장되게 된다.

즉, 이러한 오류를 방지하려면 webapp 파일을 꼭 만들어줘야한다.

 

✔️ Spring Boot 와 Thymeleaf 적용중일 경우 참고사항

Spring Boot 와 Thymeleaf 적용중이라면 기본적으로 static에서 파일을 읽게 된다. (css 또는 js 파일 등)
그래서 파일 업로드된 이미지파일을 불러올 때 경로 오류로 불러오지 않는 상황이 생길 수도 있다.
이 때는 정적 리소스에 접근해서 경로를 바꿔줘야한다.
application.yml 에서 다음과 같이 코드를 적어주자.

이렇게 static 경로를 webapp 밑으로 된 파일들에서 찾는다고 변경한다는 코드이다.

html에서 Bootstrap css와 기타 css파일 위치를 수정해주었다.

 

이미지를 HTML에서 보여줄 때는 아래코드를 이용하여 Resource를 보여주었다.

<img th:src="@{/webapp/upload/} + ${fileName}">

 

댓글