개요
기존 이미지 업로드는 로컬에서 임의로 파일을 만들어서 구현했지만 이제는 AWS S3를 사용해서 안전하게 이미지를 저장해볼려고 한다.
용도 : 회원정보를 수정할 때 프로필 이미지를 등록
1. Spring Boot에서 S3에 파일을 업로드하는 세 가지 방법
S3에 파일을 업로드하는 방법에는 3가지가 있다.
- Stream 업로드
- MultipartFile 업로드
- AWS Multipart 업로드
1.1 Stream 업로드
Stream 업로드 방식은 파일을 chunk 단위로 읽어서 서버에 전송하는 방식이다. 직렬화된 데이터를 순차적으로 보내므로, 대용량 파일을 안정적으로 전송할 수 있지만, 각 chunk를 순차적으로 전송하기 때문에 전체 파일을 한 번에 업로드하는 방식보다 더 많은 시간이 소요될 수 있다.
1.2 MultipartFile 업로드
MultipartFile 업로드는 Spring에서 제공하는 MultipartFile 인터페이스를 이용하여 파일을 업로드하는 방식이다. 대부분의 웹 개발 프레임워크 및 라이브러리에서 기본적으로 지원하는 방식이므로 구현이 간단하다. 또한 사용자가 파일을 선택하고 업로드 버튼을 클릭하는 것으로 파일을 서버에 업로드할 수 있다. 드어마 파일 전체를 메모리에 로드하므로, 대용량 파일을 처리할 때 메모리 부담이 있을 수 있다. 또한 파일을 서버에 업로드할 때 보안 취약성이 발생할 수 있다.
1.3 AWS Multipart 업로드
AWS Multipart 업로드는 AWS S3에서 제공하는 파일 업로드 방식으로 업로드할 파일을 작은 part로 나누어 개별적으로 업로드한다. 파일의 바이너리가 Spring Boot를 거치지 않고 AWS S3에 다이렉트로 업로드되기 때문에 서버의 부하를 고려하지 않아도 된다. 업로드 중에 오류가 발생해도 해당 부분만 재시도할 수 있다. 그러나 복잡한 구현을 필요로 하고 여러 요청을 병렬로 보내므로, 이에 따른 네트워크 및 데이터 전송 비용이 발생할 수 있다.
- 원래 Presigned URL을 이용하는 3번 방식을 통해 구현해보고 싶었으나 다음과 같은 이유로 2번을 사용하려고 한다.
- 3번의 경우 클라이언트로 부터 파일 업로드 요청을 받으면 프론트는 다시 백엔드로 그 파일을 전달해주고 백엔드에서 다시 S3로 업로드하는 불필요한 과정이 발생한다.
- 프로젝트 규모가 현재 크지 않고, 빠르게 기능 구현을 해야하는 상황에서 나만의 호기심으로 사용해보기엔 구현 복잡도가 높다.
- 따라서 현실과 타협해 Multipart 파일을 S3에 업로드하는 방법을 통해 이미지 업로드 기능을 구현하려고 한다.
나의 환경
Window 11
intelliJ
java 21
spring Boot 3.3.0
spring Security 6
개발 환경 및 S3 버킷 생성
[Spring] S3 이미지 업로드하기 (velog.io)
S3 에 업로드하기 위해서 aws 가입과 S3 발급을 해야 하는데 나는 이분의 블로그를 보고 따라했다. iam 사용자를 만들고 s3 키를 발급받자.
의존성 설치
// s3
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
필요한 환경변수 세팅
application.yml 파일에 다음과 같이 작성한다.
spring:
servlet:
multipart:
maxFileSize: 10MB # 파일 하나의 최대 크기
maxRequestSize: 30MB # 한 번에 최대 업로드 가능 용량
cloud:
aws:
credentials:
access-key: {your access-key}
secret-key: {your secret-key}
s3:
bucket: {your bucket name}
region:
static: ap-northeast-2
stack:
auto: false
- MultipartFile를 사용하기로 했으니 적절한 파일 크키와 용량을 지정하고 S3 연동을 위한 설정을 해준다.
S3Config
@Configuration
public class S3Config {
@Value("${cloud.aws.credentials.access-key}")
private String accessKey;
@Value("${cloud.aws.credentials.secret-key}")
private String secretKey;
@Value("${cloud.aws.region.static}")
private String region;
@Bean
public AmazonS3Client amazonS3Client() {
BasicAWSCredentials awsCredentials= new BasicAWSCredentials(accessKey, secretKey);
return (AmazonS3Client) AmazonS3ClientBuilder.standard()
.withRegion(region)
.withCredentials(new AWSStaticCredentialsProvider(awsCredentials))
.build();
}
}
- 버전이 올라가면서 작성법이 많이 달라져서 고생을 좀 했지만 엑세스와 시크릿 키를 통해 접근을 허용해주면 된다.
이미지 S3 업로드 구현
@Slf4j
@Service
@RequiredArgsConstructor
public class S3Service {
private final AmazonS3 amazonS3;
@Value("${cloud.aws.s3.bucket}")
private String bucket;
private final String DIR_NAME = "{your image file name";
public String upload(String fileName, MultipartFile multipartFile) throws IOException {
File uploadFile = convert(multipartFile)
.orElseThrow(() -> new IllegalArgumentException("MultipartFile -> File 전환 실패"));
String newFileName = DIR_NAME + "/" + fileName + getExtension(multipartFile.getOriginalFilename());
String uploadImageUrl = putS3(uploadFile, newFileName);
removeNewFile(uploadFile); // 로컬에 생성된 파일 삭제
return uploadImageUrl; // 업로드된 파일의 S3 URL 주소 반환
}
private String putS3(File uploadFile, String fileName) {
amazonS3.putObject(new PutObjectRequest(bucket, fileName, uploadFile)
.withCannedAcl(CannedAccessControlList.PublicRead));
return amazonS3.getUrl(bucket, fileName).toString();
}
private void removeNewFile(File targetFile) {
if (targetFile.delete()) {
log.info("파일이 삭제되었습니다.");
} else {
log.info("파일이 삭제되지 못했습니다.");
}
}
private Optional<File> convert(MultipartFile file) throws IOException {
File convertFile = new File(file.getOriginalFilename());
if (convertFile.createNewFile()) {
try (FileOutputStream fos = new FileOutputStream(convertFile)) {
fos.write(file.getBytes());
}
return Optional.of(convertFile);
}
return Optional.empty();
}
private String getExtension(String fileName) {
return fileName.substring(fileName.lastIndexOf("."));
}
- S3 에 이미지를 업로드 하는 클래스이다.
- 각 함수들에 대해서 설명해보겠다.
1. upload 함수
public String upload(String fileName, MultipartFile multipartFile) throws IOException {
File uploadFile = convert(multipartFile)
.orElseThrow(() -> new IllegalArgumentException("MultipartFile -> File 전환 실패"));
String newFileName = DIR_NAME + "/" + fileName + getExtension(multipartFile.getOriginalFilename());
String uploadImageUrl = putS3(uploadFile, newFileName);
removeNewFile(uploadFile); // 로컬에 생성된 파일 삭제
return uploadImageUrl; // 업로드된 파일의 S3 URL 주소 반환
}
}
- 여기서 MutipartFile을 File 형태로 변환해주어야 한다.
- 이 과정에서 로컬에서 파일이 복사되어 저장되기 때문에 로컬에 있는 이미지를 삭제해주어야 한다.
2. putS3 메서드
private String putS3(File uploadFile, String fileName) {
amazonS3.putObject(new PutObjectRequest(bucket, fileName, uploadFile)
.withCannedAcl(CannedAccessControlList.PublicRead));
return amazonS3.getUrl(bucket, fileName).toString();
}
- 이 함수는 MultipartFile형태인 파일을 File 형태로 변환하고 업로드될 파일의 이름을 받아와서 S3에 업로드 하는 함수이다.
3. removeNewFile 메서드
private void removeNewFile(File targetFile) {
if (targetFile.delete()) {
log.info("파일이 삭제되었습니다.");
} else {
log.info("파일이 삭제되지 못했습니다.");
}
}
- 이 함수는 로컬에 저장된 파일을 삭제해주는 메서드이다.
4. Convert 메서드
private Optional<File> convert(MultipartFile file) throws IOException {
File convertFile = new File(file.getOriginalFilename());
if (convertFile.createNewFile()) {
try (FileOutputStream fos = new FileOutputStream(convertFile)) {
fos.write(file.getBytes());
}
return Optional.of(convertFile);
}
return Optional.empty();
}
- 이 함수는 MultipartFile 형태인 파일을 File 형태로 변환해서 S3에 업로드 하는 메서드이다.
5. getExtension 메서드
private String getExtension(String fileName) {
return fileName.substring(fileName.lastIndexOf("."));
}
- S3에 저장될 파일의 확장자를 정의해준다.
UserServiceImpl.java
@Override
public User updateProfileImage(Long userId, MultipartFile file) throws IOException {
User user = userRepository.findById(userId)
.orElseThrow(() -> new IllegalArgumentException("유효하지 않은 사용자 ID"));
String fileName = userId.toString(); // 사용자 ID를 파일명으로 사용
String imageUrl = s3Service.upload(fileName, file); // S3에 이미지 업로드 및 URL 반환
user.setFilename(file.getOriginalFilename()); // 파일 이름 저장
user.setFilepath(imageUrl); // 파일 경로(URL) 저장
return userRepository.save(user); // 사용자 정보 업데이트
}
- S3에 올라갈 때 사용자 Id를 파일명으로 지정하여 올리기로 결정했고
- 클라이언트에서 사용자가 이미지를 등록했을 때 동작할 서비스 클래스 코드이다.
- 그리고 엔티티에 파일경로를 저장하기로 해서 user 엔티티에 2개의 칼럼을 추가해준다.
User.java
@Entity
@Table(name = "users")
@Getter @Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String username; // 이메일
@Column(nullable = false)
private String password;
private String filename; // 파일 이름
private String filepath; // 파일 경로
}
Controller.java
@RestController
@Slf4j
@RequiredArgsConstructor
public class S3Controller {
private final S3Service s3Service;
private final UserService userService;
@PostMapping("/api/s3/image")
public ResponseEntity<String> uploadProfileImage(
@RequestParam("file") MultipartFile file,
@RequestParam("userId") Long userId) throws IOException {
User user = userService.updateProfileImage(userId, file);
log.info("Uploaded file URL: " + user.getFilepath());
return ResponseEntity.ok(user.getFilepath());
}
- 지정한 저 앤드포인트로 요청을 보내서 서버측에서 요청을 처리할 예정이다.
HTML & JS
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>메인 홈 입니다.</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script>
// 프로필 이미지 업로드
$('#profile-form').on('submit', function(event) {
event.preventDefault();
let formData = new FormData(this);
formData.append("userId", $('#userId').val()); // 유저 ID 추가
$.ajax({
type: 'POST',
url: '/api/s3/image',
data: formData,
processData: false,
contentType: false,
success: function(response) {
alert('프로필 이미지가 성공적으로 업로드되었습니다.');
$('#profile-image').attr('src', response);
},
error: function(error) {
alert('프로필 이미지 업로드에 실패했습니다.');
}
});
});
});
</script>
</head>
<body>
<h2>로그인에 성공하셨어요 축하합니다.</h2>
<p>안녕하세요, 당신의 이메일 주소 :: <span th:text="${user.username}"></span>님</p>
<p>안녕하세요, 당신의 이름 :: <span th:text="${user.name}"></span></p>
<!-- 프로필 이미지 편집 섹션 -->
<h3>프로필 이미지 편집</h3>
<input type="hidden" id="userId" th:value="${user.id}"/>
<form id="profile-form">
<input type="file" name="file" accept="image/*" required />
<button type="submit">프로필 이미지 업로드</button>
</form>
<img id="profile-image" th:src="${user.filepath != null ? user.filepath : '/files/lioon.png'}" alt="Profile Image" style="max-width: 150px; max-height: 150px;" />
</body>
</html>
자바스크립트 파일로 지정한 앤드포인트로 요청을 보내면 S3에 업로드가 될 것이다.
기본 사진은 미리 로컬에 지정해둔 사진으로 보여질 수 있도록 했다. 그 후 사용자가 프로필을 변경하면 변경될 수 있도록..
화면단에서 사진도 잘 올라가고 잘 보인다~~~ㅎㅎ
이렇게 이미지를 S3에 올리는 부분을 수행했다.
'Spring > Spring Boot' 카테고리의 다른 글
Spring : 알림 기능 구현 (5) | 2024.08.28 |
---|---|
Spring : 알림 기능 구현 종류 (0) | 2024.08.28 |
Docker 및 Redis 설치/설정 (0) | 2024.08.22 |
Spring 이메일 인증 기능 구현하기 2 : 비밀번호 찾기 (0) | 2024.08.20 |
Spring 이메일 인증 기능 구현하기 1편 : 회원가입시 이메일 인증 (0) | 2024.08.20 |