파일 업로드를 위해서 사용하는 라이브러리는 COS.jar, Apache Commons FileUpload, Servlet(3.0이후 version)의 Part(API) 등이 있으며 본 프로젝트에서는 Apache Commons FileUpload를 사용하였다.
프로젝트 빌드 툴을 Gradle로 사용하므로 build.gradle의 dependencies에 라이브러리를 추가해주었다.
https://mvnrepository.com/artifact/commons-fileupload/commons-fileupload
// https://mvnrepository.com/artifact/commons-fileupload/commons-fileupload
implementation group: 'commons-fileupload', name: 'commons-fileupload', version: '1.4'
구글링시 라이브러리 설정을 pom.xml에 하라는 글을 볼 수 있다. pom.xml은 의존성 관리도구 중 Maven을 사용했을 때 생성되는 라이브러리 관리 파일이므로 사용된 라이브러리를 찾아 Gradle 형식에 맞춰 설정해주면 된다.
create table image(
image_id int primary KEY AUTO_INCREMENT,
origin_image_name VARCHAR(256) NOT null,
new_image_name VARCHAR(256) NOT null,
image_path VARCHAR(256) NOT null
);
이미지 파일은 이미지 정보와 이미지 자체의 정보 두 가지가 저장되어야 한다.
DB에는 PK값(image_id), 사용자가 업로드하는 파일의 원본이름(origin_image_name), 이미지 중복방지를 위해 붙여질 고유한 새 이름(new_image_name), 이미지 경로(image_path) 값이 삽입된다.
👉 new_image_name이 있는 이유
: 이미지 파일 업로드시, 기존에 있는 파일과 동일한 파일명으로 업로드하면 기존 파일이 새로 업로드한 파일로 덮어씌워진다. 따라서 파일명이 곂치지 않도록 고유한 파일명으로 저장해주어야 한다.
[Image.java]
package site.metacoding.frontproject.domain.Image;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
public class Image {
private Integer id;
private String originImageName;
private String newImageName;
private String imagePath;
}
[ImageDto]
package site.metacoding.frontproject.domain.Image;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@NoArgsConstructor
@Getter
@Setter
public class ImageDto {
private String originImageName;
private String newImageName;
private String imagePath;
@Builder
public ImageDto(String originImageName, String newImageName, String imagePath) {
this.originImageName = originImageName;
this.newImageName = newImageName;
this.imagePath = imagePath;
}
}
DTO를 사용하지 않고 Image로 바로 받아도 될 것 같지만
깔끔히 DTO 테이블을 만들어 값을 받는 방향으로 했다.
@builder 어노테이션에 대해 쓰기
https://atoz-developer.tistory.com/120
static을 가지고 있ㄷ음
[Image.xml]
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="site.metacoding.frontproject.domain.Image.ImageDao">
<insert id="save">
INSERT INTO image(origin_image_name, new_image_name, image_path)
VALUES(#{originImageName}, #{newImageName}, #{imagePath})
</insert>
</mapper>
[ImageDao.java]
package site.metacoding.frontproject.domain.Image;
public interface ImageDao {
public void save(ImageDto imageDto);
}
insert할 때 필요한 값만 받는 ImageDTO를 파라매터로 설정했다.
(Controller에서 받을) 파일 데이터를 전송할 페이지를 만든다.
[upload.jsp]
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>사진</title>
</head>
<body>
<h1>사진업로드테스트</h1>
<form method="post" action="/upload" enctype="multipart/form-data">
<ul>
<li>이미지 파일
<input type="file" multiple="multiple" name="image">
<input type="submit" id="submit" value="전송" />
</li>
</ul>
</form>
</body>
form 태그에 enctype 속성을 "multipart/form-data"로 설정해야 한다.
- enctype : 폼 데이터(form data)가 서버로 제출될 때
해당 데이터가 인코딩되는 방법을 명시한다. (enctype을 지정해주지 않을 경우 기본적으로 x-www-form-urlencoded으로 설정되어 있다.)- multipart/form-data : 모든 문자를 인코딩하지 않음을 명시한다.
form 요소로 파일이나 이미지를 서버로 전송할 때 주로 사용한다.
input 태그의 경우 type="file" multiple="multiple" 으로 설정해야 한다.
multipart
multiple이 없을 경우 오류가 난다 < 왜인지 적기 (두가지 정보를 전달해야 해서)
[UploadController.java]
package site.metacoding.frontproject.web;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import lombok.RequiredArgsConstructor;
import site.metacoding.frontproject.service.ImageService;
@Controller
@RequiredArgsConstructor
public class UploadController {
private final ImageService imageService;
//upload 페이지
@GetMapping("/upload")
public String upload() {
return "upload";
}
//upload 페이지에서 전송버튼을 누르면 해당 메서드 실행
@PostMapping("/upload")
public String uploadImage(@RequestParam MultipartFile image){
imageService.insertImage(image);
return "upload";
}
}
MultipartFile
이란 사용자가 업로드한 File을 Handler에서 손쉽게 다룰 수 있도록 도와주는 Handler의 매개변수 중 하나이다.
<input type="file" multiple="multiple" name="image">
에서 file 데이터를 전송했고, Controller와 Service에서 코드를 용이하게 사용하기 위해 MultipartFile 타입으로 받았다.
이거 수정해야됨 왜 MultipartFile로 받았는가
Controllor에서 패러미터에 값을 할당하는 대표적인 방법으로는 @RequestBody 와 @RequestParam 이 있다.
🔎 @RequestBody 어노테이션
- HTTP request의 body에 담긴 데이터를 받는다. HttpMessageReader가 body를 읽어 Java Object로 역직렬화하여 주는 것.
- 각 변수별로 데이터를 저장할 순 없다.
💡 @RequestParam 어노테이션
- HTTP 단일 요청 패러미터의 값을 메서드 패러미터에 넣어주는 어노테이션. (1:1로 값 바인딩)
- 요청 패러미터의 값은 메서드 패러미터의 타입에 따라 적절히 변환 된다.
- @RequestParam으로 데이터를 받을 때에는 데이터를 저장하는 이름으로 메서드의 변수명을 설정해 주어야 한다.
(@RequestPart를 사용해도 무관하다)
<설명 적기
https://velog.io/@yu-jin-song/SpringBoot-%EA%B2%8C%EC%8B%9C%ED%8C%90-%EA%B5%AC%ED%98%84-4-MultipartFile-%EB%8B%A4%EC%A4%91-%ED%8C%8C%EC%9D%BC%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%97%85%EB%A1%9C%EB%93%9C
package site.metacoding.frontproject.service;
import java.io.File;
import java.util.UUID;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import lombok.RequiredArgsConstructor;
import site.metacoding.frontproject.domain.Image.ImageDao;
import site.metacoding.frontproject.domain.Image.ImageDto;
@Service
@RequiredArgsConstructor
public class ImageService {
private final ImageDao imageDao;
public void insertImage(MultipartFile image) {
// save할 DTO 띄우기
ImageDto imageDto = new ImageDto();
//저장할 파일경로 지정
String absolutePath = new File("src/main/resources/static/images/").getAbsolutePath();
// 확장자 추출
if (!image.isEmpty()) {
String contentType = image.getContentType();
String originalImageExtension;
if (contentType.contains("image/jpeg")) {
originalImageExtension = ".jpg";
} else if (contentType.contains("image/png")) {
originalImageExtension = ".png";
} else if (contentType.contains("image/gif")) {
originalImageExtension = ".gif";
}
}
//UUID로 랜덤으로 이름 생성
String newImageName = UUID.randomUUID().toString() + originalImageExtension;
// DTO에 담기
imageDto = ImageDto.builder()
.originImageName(image.getOriginalFilename())
.newImageName(newImageName)
.imagePath(absolutePath)
.build();
//DAO 실행
imageDao.save(imageDto);
}
}
해당 메서드의 역할은 request로 받은 image 파일 정보를 DB에 insert하는 것이므로 우선 imageDao.save()에 필요한 패러미터인 DTO 객체를 생성한다. DTO에는 originImageName, newImageName, imagePath가 필요하다.
String absolutePath = new File("src/main/resources/static/images/").getAbsolutePath();
✍️ File 타입
: File 객체는 하드디스크에 존재하는 실제 파일이나 디렉토리가 아니다. 그것에 대한 경로(Pathname) 또는 참조(reference)를 추상화한 객체이다. 즉, File 객체는 새 파일에 대한 경로나 만들고자 하는 디렉토리를 캡슐화한 것이다.
- File 클래스의 생성자는 인자로 전달된 경로를 확인하지 않는다. => 추후 해당 경로를 만들어주어야 한다.
- File 클래스의 생성자로 어떤 문자열을 넘겨도 된다.
- File 객체는 불변적으로 객체를 생성하고 나면 그것이 가진 경로를 바꿀 수 없다. ⇒ 따라서 실제 저장 경로는 맨 마지막에 지정해줄 것이다. & 지금은 File 객체로 받는 것이 아닌, 절대경로를 추출하여 String으로 받는 것이기 때문에 상관 없다.
getPath() vs getAbsolutePath()
- getPath()
: File에 입력한 경로를 return한다. 인자로 전달한 경로를 그대로 리턴하기에 상대경로 가 된다.File file = new File("path/to/file"); System.out.println(file.getPath()); // console : path/to/file2
- getAbsolutePath()
: 현재 실행 중인 Working directory + File 인자 경로 => 절대 경로 가 된다.File file = new File("path/to/file"); System.out.println(file.getPath()); // console : C:/Users/Desktop/workspace/first_project/frontproject/path/to/file2
따라서 String absolutePath
는 image를 저장할 정확한 파일 위치의 절대경로 값이다.
String contentType = image.getContentType();
String originalImageExtension;
if (contentType.contains("image/jpeg")) {
originalImageExtension = ".jpg";
}
👉 .getContentType()
: file의 확장자를 확인하여 MIME 타입을 확인한다.
👉 .contains()
: 대상 문자열에 특정 문자열이 포함되어 있는지 확인하는 함수이다. 대/소문자를 구분한다.
String newImageName = UUID.randomUUID().toString() + originalImageExtension;
✅ UUID(Universally Unique IDentifier) 범용 고유 식별자
//imageDto는 맨 처음 객체 생성을 해놓았음
imageDto = ImageDto.builder()
.originImageName(image.getOriginalFilename())
.newImageName(newImageName)
.imagePath(absolutePath)
.build();
💡 Builder 패턴
객체를 생성할 때 사용하는 패턴 중 하나. 생성자에 들어갈 패러미터를 하나씩 받아들인 뒤 (패러미터 순서는 상관없다) 모든 패러미터를 받은 뒤에 이 변수들을 통합해 한번에 객체를 생성하는 방식이다.
< 문법 >변수타입 변수명 = 생성자.builder() .파라매터(인자) .파라매터(인자) .build();
imagePath 값의 경우 일단은 세분화된 하위 폴더를 만들어 저장하지 않고 images 폴더에 모두 저장하기로 했다. 그래서 절대경로만 할당하였다.
imageDao.save(imageDto);
위의 코드는 DB에 파일 정보값을 insert하는 코드일 뿐, 전송한 파일을 저장하는 코드는 아니다.
파일 경로에 실제로 파일을 저장하는 코드를 맨 밑에 추가해준다.
File file = new File(absolutePath + "/" + newImageName);
if (!file.exists()) {
file.mkdirs();
}
image.transferTo(file);
👉.transferTo(경로)
default void transferTo(Path dest) throws IOException, IllegalStateException {
FileCopyUtils.copy(getInputStream(), Files.newOutputStream(dest));}
package site.metacoding.frontproject.service;
import java.io.File;
import java.util.UUID;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import lombok.RequiredArgsConstructor;
import site.metacoding.frontproject.domain.Image.ImageDao;
import site.metacoding.frontproject.domain.Image.ImageDto;
@Service
@RequiredArgsConstructor
public class ImageService {
private final ImageDao imageDao;
public void insertImage(MultipartFile image) throws Exception {
// 파일이 빈 것이 들어오면 메서드 종료
if (image.isEmpty()) {
return;
}
// save할 DTO 띄우기
ImageDto imageDto = new ImageDto();
// 절대경로 추출
String absolutePath = new File("src/main/resources/static/images/").getAbsolutePath();
// jpeg, png, gif 파일들만 받아서 처리
if (!image.isEmpty()) {
String contentType = image.getContentType();
String originalImageExtension;
// 확장자 명이 없으면 종료
if (!StringUtils.hasText(contentType)) {
return;
} else {
if (contentType.contains("image/jpeg")) {
originalImageExtension = ".jpg";
} else if (contentType.contains("image/png")) {
originalImageExtension = ".png";
} else if (contentType.contains("image/gif")) {
originalImageExtension = ".gif";
}
// 기타 확장자명일 경우 메서드 종료
else {
return;
}
}
String newImageName = UUID.randomUUID().toString() + originalImageExtension;
// DTO에 담기
imageDto = ImageDto.builder()
.originImageName(image.getOriginalFilename())
.newImageName(newImageName)
.imagePath(absolutePath)
.build();
imageDao.save(imageDto);
// 파일을 전송하기
File file = new File(absolutePath + "/" + newImageName);
if (!file.exists()) {
file.mkdirs();
}
image.transferTo(file);
}
}
}
null 체크 후 String 클래스의 imEmpty를 호출하여 길이를 체크, 공백(" ")이 아닌 문자가 포함되어 있는지까지 체크하여 판별한다.
❗️주의점 :
문자열을 하나씩 순회하여 공백이 아닌 character가 1개 이상 포함되면 true를 리턴한다. 즉, isEmpty와 다르게 null 체크시 결과가 반대로 전달된다. (StringUtils.hasText(null); ==> false
)
if (!StringUtils.hasText(contentType)) {
return;}
👉
throw Exception 공부
모델어트리뷰트
참조 :https://parkadd.tistory.com/70
https://simple-ing.tistory.com/47
왜 Param에 file을 붙이지 않아도 되는지 공부하기
나중에 resume 한번에 저장할 때 참고할 사이트
https://velog.io/@yu-jin-song/SpringBoot-%EA%B2%8C%EC%8B%9C%ED%8C%90-%EA%B5%AC%ED%98%84-4-MultipartFile-%EB%8B%A4%EC%A4%91-%ED%8C%8C%EC%9D%BC%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%97%85%EB%A1%9C%EB%93%9C