브라우저에서 이미지 파일을 첨부하고, 조회하기 위한 기능을 구현하고자 합니다.
전체적인 흐름은 아래와 같습니다.
S3 버킷 생성
버킷 정책
IAM 액세스 키 발급
→ 여기까지는 방법이 아주 간단하기도 하고, 구글에 방법이 많이 나와있어 찾기 쉽습니다.
4. 개발 컴퓨터(로컬)에 AWS 프로파일 생성
5. S3Config 설정
6. Spring Boot에서 S3 이미지 파일 업로드/삭제/조회
Spring Boot 3
JDK 17
AWS S3
버킷 정책
퍼블릭 액세스 차단
aws configure
IAM 생성 후 받은 Access Key와 Secret Access Key를 입력하고, S3의 리전을 입력합니다.
setx AWS_PROFILE "원하는 프로파일 명"
별도로 저장하지 않으면 컴퓨터 재부팅 시 내용이 삭제돼 매번 입력해야 합니다.
개발 서버에 시스템 환경 변수로 저장하는 것을 추천합니다.
작업한 CLI는 변경 사항이 적용되지 않으니, 꼭 새 CLI 창을 열어서 확인해야 합니다.
IntelliJ의 Terminal에서 새 Terminal을 여는 것도 적용되지 않아서, 윈도우의 CMD를 새로 열어서 확인했습니다.
위 명령 입력 후 윈도우의 시스템 환경 변수에 들어가면 아래와 같이 생성된 것을 확인할 수 있습니다.
AWS 프로파일 파일에 ‘default’ 라는 이름의 프로파일이 없다라는 예외입니다.
저의 경우, 런타임 시점에서 AWS 접근을 위한 객체를 생성할 때 AWS 프로파일을 찾을 수 없어 발생했습니다.
CLI에서 설정한 AWS Profile을 꼭 시스템 환경 변수로 저장해줘야 컴퓨터를 재부팅해도 변수가 유지됩니다.
시스템 환경 변수로 저장하지 않고 재부팅을 한 경우, 다시 key 설정을 해줘야 합니다.
◾ build.gradle
implementation 'software.amazon.awssdk:s3:2.20.90'
◾ application.yml
spring:
cloud:
aws:
active: false
profiles:
active: dev #보안 정보가 담긴 yml 파일 명
◾ application-dev.yml
cloud:
aws:
active: true
auth: profile
s3:
bucket: #버킷 이름
region: #리전 이름
profile: #프로파일 이름
인증 정보와 같은 민감한 정보는 절대 GitHub에 올리지 말고 따로 관리해야 합니다.
특히, 과금이 발생할 수 있는 AWS 서비스를 사용할 때는 더욱 조심해야 합니다.
◾ S3Config.java
S3Client
를 Bean으로 등록@Configuration
public class S3Config {
@Value("${cloud.aws.s3.region}")
private String region;
@Value("${cloud.aws.s3.profile:#{null}}")
private String profile;
@Bean
public S3Client s3Client() {
return S3Client.builder()
.region(Region.of(region))
.credentialsProvider(ProfileCredentialsProvider.create(profile))
.build();
}
}
Failed: Unable to load credentials from any of the providers in the chain AwsCredentialsProviderChain ‥‥‥ (이하생략)
AWS 접근을 위해 사용자의 자격 증명을 담은 객체를 생성할 때, 자격 증명을 찾을 수 없어 발생하는 예외입니다.
저의 경우, S3 연동에 필요한 S3Client
S3Presigner
객체를 Bean으로 등록할 때 발생했습니다.
@Value("${cloud.aws.s3.profile:#{null}}")
private String profile;
절대! 하드 코딩하지 않고 @Value를 사용해서 환경 변수로 가져옵니다.
Task
에 N개의 Attach가 저장되고, N개의 Attach
들을 group id로 그룹핑합니다.◾TaskForm.ts
const formData = new FormData();
fileArray.forEach((file) => {
formData.append("files", file);
});
formData.append("content", content.value);
formData.append("year", splitDate[0]);
formData.append("month", splitDate[1]);
formData.append("day", splitDate[2]);
formData.append("createdBy", "1");
formData.append("groupId", crypto.randomUUID());
const response = await createTask(formData);
◾ TaskController.java
@RequestParam
사용@ModelAttribute
로 매핑 /* TaskController.java */
@PostMapping
public ApiResponse<String> create(@RequestParam("files") List<MultipartFile> files,
@ModelAttribute TaskVO taskVO) {
if (taskService.create(files, taskVO)) {
return ResponseUtil.createSuccessResponse("Success Create Task");
}
return ResponseUtil.createErrorResponse(HttpStatus.NOT_FOUND, "Failed Create Task");
}
◾ AttachServiceImpl.java
UUID
를 이용하여 해당 파일의 고유 이름이 될 key
생성S3 Key
를 저장해 삭제와 Presigned URL 생성에 사용됩니다./* AttachServiceImpl.java */
@Transactional
@Override
public boolean save(List<MultipartFile> files, String groupId, Long createdBy) {
for (MultipartFile file : files) {
String key = "uploads/" + UUID.randomUUID() + "-" + file.getOriginalFilename();
try {
s3UploadServiceImpl.saveFile(file, key); // S3 업로드
Attach attachEntity = new Attach(
key
, attach.getOriginalFilename()
, attach.getContentType()
, attach.getSize()
, groupId
, createdBy
);
attachRepository.save(attachEntity);
} catch (Exception e) {
logger.error(e.getMessage());
return false;
}
}
return true;
}
◾ S3UploadServiceImpl.java
getInputStream()
사용/* S3UploadServiceImpl.java */
public boolean saveFile(MultipartFile file, String key) {
try {
PutObjectRequest putObjectRequest = PutObjectRequest.builder()
.bucket(bucketName)
.key(key)
.build();
s3Client.putObject(putObjectRequest,
software.amazon.awssdk.core.sync.RequestBody.fromInputStream(file.getInputStream(), file.getSize()));
return true;
} catch (S3Exception | IOException e) {
logger.warn(e.getMessage());
return false;
}
}
◾ S3UploadServiceImpl.java
key
사용/* S3UploadServiceImpl.java */
public void deleteFile(String key) {
try {
DeleteObjectRequest deleteObjectRequest = DeleteObjectRequest.builder()
.bucket(bucketName)
.key(key)
.build();
s3Client.deleteObject(deleteObjectRequest);
} catch (S3Exception e) {
logger.warn(e.getMessage());
}
}
◾ S3Config
S3Presigner
을 Bean으로 등록/* S3Config.java */
@Value("${cloud.aws.s3.profile:#{null}}")
private String profile;
@Bean
public S3Presigner s3Presigner() {
return S3Presigner.builder()
.region(Region.AP_NORTHEAST_2)
.credentialsProvider(ProfileCredentialsProvider.create(profile))
.build();
}
◾ S3UploadServiceImpl.java
Duration.ofMinutes()
: URL 만료 시간/* S3UploadServiceImpl.java */
public String generatePreSignedUrl(String key) {
try {
GetObjectRequest getObjectRequest = GetObjectRequest.builder()
.bucket(bucketName)
.key(key)
.build();
GetObjectPresignRequest getObjectPresignRequest = GetObjectPresignRequest.builder()
.signatureDuration(Duration.ofMinutes(10))
.getObjectRequest(getObjectRequest)
.build();
return s3Presigner.presignGetObject(getObjectPresignRequest).url().toString();
} catch (S3Exception e) {
logger.error(e.getMessage());
◾ TaskServiceImpl.java
/* TaskServiceImpl.java */
private List<AttachDTO> getImageDTOsForTask(Task task) {
List<Attach> attaches = attachRepository.findByGroupId(task.getGroupId());
return attaches.stream()
.map(attach -> new AttachDTO(
attach.getId(),
attach.getOriginName(),
s3UploadService.generatePreSignedUrl(attach.getS3Key())
))
.collect(Collectors.toList());
}