MultipartFile 이란 ❓
파일을 백엔드 서버에서 받기 위해서는 스프링에서 제공하는 MultipartFile
인터페이스를 사용한다.
프론트 서버에서 파일을 보낼때는 enctype="multipart/form-data"
로 보내고 백엔드 서버에서 요청을 받으려면 DTO 객체의 파일을 저장할 변수를 String
이 아닌 MultipartFile
로 해야되는 것이다.
만약, String
으로 해서 요청을 받으면 해당 변수에는 단순히 파일의 이름만 입력받게 된다. 하지만 MultipartFile
로 요청을 받으면 binary 형태로 받게 되는데 이것은 파일을 청크 단위로 쪼개서 효율적으로 파일을 업로드 할 수 있도록 도와준다.
🐵 파일 업로드 및 DB 경로 저장 실습하기
application.yml
파일에서 별도 설정을 해줘야된다. 이것은 파일을 업로드 할 때 용량을 초과한다는 에러인데, 설정에서 용량사이즈를 직접 지정해 줄 수 있다.spring:
servlet:
multipart:
max-file-size: 10MB
max-request-size: 10MB
실습은 간단하게 회원가입할때 회원 이미지를 같이 등록하도록 하는 상황으로 진행해보겠다.
💻 pom.xml 파일 추가한 라이브러리
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
💻 application.yml 파일 설정
spring:
datasource:
url: jdbc:mysql://77.77.77.111/shop
username: test01
password: qwer1234
driver-class-name: com.mysql.cj.jdbc.Driver
servlet:
multipart:
max-file-size: 10MB
max-request-size: 10MB
jpa:
hibernate:
ddl-auto: update
naming:
implicit-strategy: org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyJpaImpl
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
properties:
hibernate:
format_sql: true
show-sql: true
logging:
level:
org.springframework.security: DEBUG
project:
upload:
path: C:\test // 파일을 저장할 폴더명 지정
➡ 여기다가 파일을 저장할 폴더명을 설정해준다. 그 이유는, 클래스에 변수로 생성하여
설정하면 나중에 저장할 폴더명을 바꿀때 하나씩 다 바꿔줘야되기 때문에 설정
파일에 설정하고 값을 불러와서 사용토록 할 예정이다.
💻 User Entity 클래스
@Entity
@Builder
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 50, unique = true)
private String email;
@Column(nullable = false, length = 200)
private String password;
@Column(length = 30)
private String name;
// DB에 저장할때는 경로로 저장되기 때문에 String 타입으로 생성한다.
@Column(length = 200, unique = true)
private String image;
}
➡ 여기서 image 변수의 타입을 String
으로 한것에 의문을 가질 수 있다. 이것은 DB에
저장될 속성의 타입으로 우리가 파일을 DB에 저장한다고 하면 파일이 저장된 경로를
저장하기 때문에 String
으로 설정해줘야 된다.
💻 UserRepository 인터페이스
@Repository
public interface UserRepositorty extends JpaRepository<User, Long> {
}
💻 PostCreateUserReq 클래스
@Builder
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class PostCreateUserReq {
private String email;
private String password;
private String name;
private MultipartFile image;
}
💻 PostCreateUserRes 클래스
@Builder
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class PostCreateUserRes {
private Long id;
private String email;
private String name;
private String image;
}
💻 UserService 클래스
@Service
public class UserService {
// 파일을 저장할 경로를 설정한 값을 불러온다.
@Value("${project.upload.path}")
private String uploadPath;
private final UserRepositorty userRepositorty;
public UserService(UserRepositorty userRepositorty) {
this.userRepositorty = userRepositorty;
}
// 폴더가 없다면, 폴더를 생성하기 위한 메서드이다.
public String makeFolder(){
// 현재 시간을 기준으로 2023/11/11 형식으로 설정해준다.
// 폴더를 생성할때 2023 폴더 안에 11 폴더 안에 11 폴더 안에 파일을 저장하기 위한 작업이다.
// 포맷을 지정해주지 않으면, ms 단위 까지 나오기 때문에 폴더를 생성할때 엄청나게 많이 생긴다.
String str = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
// 폴더의 경로를 지정하는데, 이때 File.separator 를 사용하며 "/" 을 "\" 로 바꿔준다.
// 그 이유는 윈도우 OS는 경로를 "\" 로 표현하기 때문이다.
// 폴더 경로는 내가 저장하기로 한 폴더 밑에 날짜들을 분리하여 만들어 준다 ex) c:\test\2023\11\11
String folderPath = str.replace("/", File.separator);
// new File(A, B) 메서드는 A라는 경로에 B라는 이름의 파일을 생성한다는 메서드이다.
File uploadPathFolder = new File(uploadPath, folderPath);
// 경로가 생성되어있는것이 없다면 폴더를 생성해주기 위한 작업이다.
if(uploadPathFolder.exists() == false) {
// 폴더를 만들어주는 메서드이다.
uploadPathFolder.mkdirs();
}
// 폴더 경로를 반환한다.
return folderPath;
}
// 파일을 저장하는 메서드이다.
public String saveFile(MultipartFile file) {
// 전달받은 MutipartFile에서 파일의 이름만 뽑아낸다.
String originalName = file.getOriginalFilename();
// 저장할 폴더 경로가 있는지 확인하여 없다면 경로에 폴더를 생성한다.
String folderPath = makeFolder();
// 파일의 이름이 중복되지 않도록 설정하는 것이다. UUID를 사용하면 중복되지 않는 문자열을 만들 수 있다.
// 만약 이렇게 해주지 않으면, 동시에 똑같은 이름의 파일을 클라이언트가 등록하기 위해 요청하면
// 파일이 저장될때 기존에 있던 것을 덮어쓰기 때문에, 기존의 파일은 사라지게 된다.
String uuid = UUID.randomUUID().toString();
// 생성한 폴더경로에 UUID와 파일의 이름을 더하여 최종 파일 이름을 설정한다.
String saveFileName = folderPath+ File.separator + uuid + "_" + originalName;
// 해당 경로에 파일을 생성한다.
File saveFile = new File(uploadPath, saveFileName);
try {
// 파일을 전달한다. transferTo는 스트림의 일종이기 때문에 예외처리를 해줘야된다.
// 이때 폴더에 파일이 생성되어 저장된다.
file.transferTo(saveFile);
} catch (IOException e) {
throw new RuntimeException(e);
}
// DB에 파일 이름을 저장하기 위해 반환
return saveFileName;
}
// CREATE 메서드
public PostCreateUserRes create(PostCreateUserReq postCreateUserReq) {
String saveFileName = saveFile(postCreateUserReq.getImage());
User user = User.builder()
.email(postCreateUserReq.getEmail())
.password(postCreateUserReq.getPassword())
.name(postCreateUserReq.getName())
// 파일을 윈도우 컴퓨터에 저장하기 위해 "\" 로 바꿧지만,
// 파일을 불러올때는 url로 요청이 들어오기때문에 DB에 저장할때는 다시 "/" 로 바꿔야 한다.
.image(saveFileName.replace(File.separator, "/"))
.build();
User result = userRepositorty.save(user);
PostCreateUserRes response = PostCreateUserRes.builder()
.id(result.getId())
.email(result.getEmail())
.name(result.getName())
.image(result.getImage())
.build();
return response;
}
}
💻 UserController 클래스
@RestController
@RequestMapping("/user")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@RequestMapping(method = RequestMethod.POST, value = "/create")
public ResponseEntity create(PostCreateUserReq postCreateUserReq) {
PostCreateUserRes postCreateUserRes = userService.create(postCreateUserReq);
return ResponseEntity.ok().body(postCreateUserRes);
}
}
이렇게 하고 포스트맨을 이용하여 요청을 보내면 정상적으로 파일이 지정한 경로의 폴더에 생기는 것을 확인할 수 있을 것이다.
🐷 개인 프로젝트에 적용하기
개인적으로 진행중인 쇼핑몰 프로젝트에 이미지 파일을 그동안 "img_url" 로 임시로 입력해놨었는데, 위에서 한 내용을 적용해봤다.
파일을 업로드하는 코드는 위와 동일하기 때문에, 잘 동작하는지만 그림으로 표현하겠다.
1) 서버로 브랜드 등록 요청 보내기
2) DB에 정상적으로 저장됬는지 확인
3) 지정한 폴더 경로에 파일이 생성됬는지 확인
위와 같이 설정한대로 정상적으로 DB에도 저장되고 파일도 폴더에 생성된 것을 확인 할 수 있었다.
하지만, 이렇게 하면 내 하드디스크에 파일들을 저장하기 때문에, 하드 디스크의 용량이 감당안될 수도 있다. 또한 가장 큰 문제는 클라이언트의 요청이 있을때마다 파일을 불러오기 위해서는 내 하드디스크 경로로 저장한 DB에 요청이 들어오기 때문에 서버가 요청이 많이 들어오면 서버가 정상적으로 동작하기가 힘들다.
그렇기때문에, 파일을 클라우드 저장소에 저장해서, 파일을 조회할 때 클라우드 저장소에서 조회해서 요청을 처리하도록 한다. 클라우드는 ASW 클라우드를 사용했다.
🐻 AWS 클라우드 파일 업로드 실습하기
먼저 AWS 로그인을 하고, S3 (클라우드 저장소) 를 검색하여 버킷을 생성해준다.
생성하는 방법은 구글 검색해보면 자세히 나와있다.
그다음 내 계정의 개인 보안에서 루트계정 키를 발급한다. 여기서 주의할점은 루트계정 키가 만약에 공개되면, 해커들이 그것을 통해 악의적 목적으로 사용할 수 있다고 하니 주의해야 한다. 나도 실습을 진행하고 나서 바로 삭제했다.
이렇게 해주면, 이제 스프링 부트의 코드를 일부 수정해주면 된다.
💻 pom.xml 파일 aws 라이브러리 추가
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-aws</artifactId>
<version>2.2.6.RELEASE</version>
</dependency>
💻 application.yml 파일 aws 클라우드 연결을 위한 설정 추가
cloud:
aws:
s3:
bucket: ${bucket} // 생성한 버킷 이름
credentials:
access-key: ${access-key} // 발급받은 액세스 키
secret-key: ${secret-key} // 발급받은 시크릿 키
region:
static: ${region} // 생성한 지역 보통 서울은 ap-northeast-2 이다.
auto: false
stack:
auto: false
➡ 위에서 말한 것과 같이 키에 대한 정보가 실수로라도 깃허브 업로드 과정에서
공개되면 문제가 발생할 수도 있기 때문에 환경변수로 처리해주는 것이 좋다.
💻 UserService 클래스 수정
@Service
public class UserService {
@Value("${project.upload.path}")
private String uploadPath;
private final UserRepositorty userRepositorty;
// ✅ application.yml 파일에서 설정한 bucket 값을 사용
@Value("${cloud.aws.s3.bucket}")
private String bucket;
// ✅ AmazonS3 인터페이스 의존성 주입
private AmazonS3 s3;
public UserService(UserRepositorty userRepositorty, AmazonS3 s3) {
this.userRepositorty = userRepositorty;
this.s3 = s3;
}
public String makeFolder(){
String str = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
String folderPath = str.replace("/", File.separator);
File uploadPathFolder = new File(uploadPath, folderPath);
if(uploadPathFolder.exists() == false) {
uploadPathFolder.mkdirs();
}
return folderPath;
}
public String saveFile(MultipartFile file) {
String originalName = file.getOriginalFilename();
String folderPath = makeFolder();
String uuid = UUID.randomUUID().toString();
String saveFileName = folderPath+ File.separator + uuid + "_" + originalName;
File saveFile = new File(uploadPath, saveFileName);
try {
// 기존 하드디스크 저장 시 사용했던 파일 전송 코드
// file.transferTo(saveFile);
// ✅ AWS 클라우드에 파일을 전송하기 위한 코드
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentLength(file.getSize());
metadata.setContentType(file.getContentType());
s3.putObject(bucket, saveFileName.replace(File.separator, "/"), file.getInputStream(), metadata);
} catch (IOException e) {
throw new RuntimeException(e);
}
// 기존 하드디스크 저장 시 사용했던 코드
//return saveFileName;
// ✅ AWS에 저장한 파일을 불러오기 위한 작업(클라우드 url 자체를 DB에 저장토록 하는 설정)
return s3.getUrl(bucket, saveFileName.replace(File.separator, "/")).toString();
}
// CREATE
public PostCreateUserRes create(PostCreateUserReq postCreateUserReq) {
String saveFileName = saveFile(postCreateUserReq.getImage());
User user = User.builder()
.email(postCreateUserReq.getEmail())
.password(postCreateUserReq.getPassword())
.name(postCreateUserReq.getName())
.image(saveFileName.replace(File.separator, "/"))
.build();
User result = userRepositorty.save(user);
PostCreateUserRes response = PostCreateUserRes.builder()
.id(result.getId())
.email(result.getEmail())
.name(result.getName())
.image(result.getImage())
.build();
return response;
}
}
➡ 수정한 내용은 체크 표시로 표현했다. 보시다시피 기존 코드와 크게 달라진 것은 없다.
➡ AWS 클라우드 저장소에 저장하기 위한 정보들과 파일 전송 코드부분만 변경됬다.
AWS 클라우드에 파일 업로드하는 것을 개인적으로 진행중인 쇼핑몰 프로젝트에 적용해봤다.
1) 서버로 브랜드 등록 요청 보내기
2) DB에 정상적으로 저장됬는지 확인
3) AWS 클라우드의 버킷에 파일이 생성됬는지 확인
✅ 파일을 불러오기 위한 작업
1) 파일을 불러올때는 속성의 객체 URL 을 통해 불러온다.
2) 하지만 눌러보면 접근이 거부됬다고 뜰 것이다.
✅ 이를 해결하기 위해서는 접근 권한을 설정해줘야된다.
1) 버킷정보에서 권한 클릭 ( 속성은 잘못 표시하였다. )
2) 버킷 정책에서 편집 클릭 후 정책생성기 클릭
3) 아래 그림처럼 설정 / 이때 Resources 는 arn:aws:s3:::[버킷이름]/* 로 설정
4) 정책 생성 후 나오는 코드 복사
5) 복사한 코드를 입력 후 저장한 뒤 다시 객체 URL을 클릭하면 정상적으로 파일이
불러와지는것을 확인 할 수 있다.
오늘의 느낀점 👀
오늘은 드디어 이미지 파일 업로드하는 것을 실습해봤다. 그동안 사실 매번 실습하면서 이미지에 img_url 이라고만 적어놔서 게속 궁금했었는데 오늘 궁금증이 완벽히 해결됬다.
이제 나는 개인 프로젝트에 브랜드 이미지와 상품 이미지를 등록하는 과정을 끝냈다. 이제 상품 여러개 생성해서 주문하는 기능을 구현해볼 예정이다.
다음주에는 그럼 결제 기능을 적용시켜볼 수 있을 것 같다.
착착 진행되어 가는 수업 커리큘럼과 나의 프로젝트에 뿌듯함을 느끼며 열정을 불태워본다. 🔥🔥