※ 개발 환경:
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}">
'Spring&Spring Boot' 카테고리의 다른 글
[Spring Boot] 스프링 시큐리티 - 패스워드 암호화 적용하기 (0) | 2022.06.29 |
---|---|
Junit5 에서 Exception 테스트 (Junit4 @Test(expected = exception) (0) | 2022.05.31 |
JSP와 Thymeleaf (차이점, 동작 원리) (0) | 2022.04.01 |
스프링 컨테이너(BeanFactory, ApplicationContext) (0) | 2022.03.19 |
댓글