https://innovation123.tistory.com/198
[AWS/S3] Spring boot project 이미지 업로드를 위해 S3 버켓 만들기
Amazon S3 버킷 만들기 IAM 만들기 생성 완료 IAM accessKey, secretKey 얻기 IAM - 사용자 - 보안 자격 증명 액세스 키 만들기 CLI 선택 accessKey, secretKey 저장 저 두 가지 Key를 저장해 뒀다가 spring properties에 등
innovation123.tistory.com
1. S3 버킷 만들기
-> 웹 사이트의 이미지를 사용자가 볼 수 있게 하려면 버킷의 퍼블릭 엑세스를 허용해주고
+ 파일 업로드 시 퍼블릭 ACL을 설정해주어야하는데 얘는 개발하는 코드에서 설정해주면 됨!
2. IAM 사용자에게 s3 full access 권한 갖게 하기
3. AmazonS3 객체를 생성하고 Bean으로 등록하기
package com.ceos20.instagram.global.config;
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class S3Config {
@Value("${cloud.aws.credentails.accessKey}") // @Value : application.properties나 application.yml파일에서 설정한 값을 가져온다
private String accessKey;
@Value("${cloud.aws.credentails.secretKey}")
private String secretKey;
@Value("${cloud.aws.region.static}")
private String region;
@Bean
public AmazonS3 s3Client() {
AWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);
return AmazonS3ClientBuilder
.standard()
.withCredentials(new AWSStaticCredentialsProvider(credentials))
.withRegion(region)
.build();
}
}
AWS S3와의 연결을 설정하여 애플리케이션이 AWS S3 버킷에 파일을 업로드하거나 다운로드할 수 있도록 하기 위한 설정을 담당하는 클래스이다.
앞으로 애플리케이션 내에서 위와 같이 생성한 AmazonS3 객체를 주입받아 S3와 관련된 작업을 처리하게 된다.
<이미지 s3에 업로드>
// 이미지파일 저장하고 url 반환
private String saveImage(MultipartFile image) {
if(image.isEmpty()){
return null;
}
//확장자 명이 올바른지 확인 (파일 확장자가 jpg, jpeg, png, gif 중에 속하는지)
validateFileExtension(image.getOriginalFilename());
//파일 이름에 uuid를 붙여 unique하게 만들어줌
String filename= UUID.randomUUID().toString()+"-"+image.getOriginalFilename();
try{
ObjectMetadata metadata=getObjectMetaData(image);
PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, filename, image.getInputStream(), metadata).clone().withCannedAcl(
CannedAccessControlList.PublicRead);
amazonS3.putObject(putObjectRequest); //bucket에 저장된 파일의 url 경로 반환
} catch (IOException e){
throw new RuntimeException("이미지를 s3에 업로드 하는 중에 문제 발생", e);
}
return amazonS3.getUrl(bucketName, filename).toString();
}
private void validateFileExtension(String filename) {
if(filename==null || filename.isEmpty()){
throw new BadRequestException(ExceptionCode.NO_FILENAME);
}
int lastDotIndex=filename.lastIndexOf(".");
String extension=filename.substring(lastDotIndex+1);
List<String> extensionList= Arrays.asList("jpg", "jpeg", "png", "gif");
if(!extensionList.contains(extension)){
throw new BadRequestException(ExceptionCode.INVALID_EXTENSION);
}
}
private ObjectMetadata getObjectMetaData(MultipartFile image) {
ObjectMetadata metadata=new ObjectMetadata();
metadata.setContentLength(image.getSize());
metadata.setContentType(image.getContentType());
return metadata;
}
버킷을 만들 때 "퍼블릭 엑세스 차단 해제" + 각 파일에 대해 `withCannedAcl(CannedAccessControlList.PublicRead)` 를 통해 퍼블릭 권한을 부여해야만 해당 파일을 누구나 접근할 수 있게 된다!!!
-> 버킷 권한과 객체 권한은 별개이기에 버킷이 퍼블릭이더라도 객체에 별도로 퍼블릭 권한을 추가해주어야 사용자들이 객체에 접근(파일을 다운받을)할 수 있다.
-> 버킷만 퍼블릭으로 해준다면, 버킷 소유자만 해당 객체에 접근 가능하다.
-> 이를 구현하기 위해 PutObjectRequest 객체를 복사한 뒤 ACL(Access Control List)을 추가 설정해준다.
metadata.setContentType(image.getContentType());
: metadata contentType을 설정하지않으면, S3의 contentType에 jpg, png이런식으로만 올라가게된다.
: jpg, png으로만 되면 url으로 접속시에 무조건 다운로드 되는 문제가 발생한다. contentType에 image/jpg로 명시하게되면 바로 다운로드가 아닌 이미지를 브라우저로 조회만도 가능하므로 metadata.setContentType을 해주어야 한다.
: contentType이 제대로 지정되지 않으면 이미지를 볼 수 없고 계속 다운로드만 된다.
따라서 MultipartFile의 contentType을 반드시 지정해줘야 한다.
: ContentType이 아래 3가지중에 설정되어야 브라우저에서 이미지를 조회할 수 있다.
• image/jpeg → (JPG 파일)
• image/png → (PNG 파일)
• image/gif → (GIF 파일)
PutObjectRequest
PutObjectRequest : Aws S3에 객체를 업로드 할 때 메타 데이터, 권한 설정 등의 상세한 설정을 할 수 있어 많이 사용된다.
따라서 S3에 putObject 메소드를 통해 업로드 할 때 PutObjectRequest 객체를 인자로 받아 업로드를 많이 하며 권장되는 방식이다.
생성자는 위와 같이 세 가지가 있는데 S3에 이미지를 업로드 할 때 InputStream을 사용하는 두 번째 생성자를 사용하는 것이 일반적이다.
- File을 사용하여 업로드하는 경우, 파일이 로컬 파일 시스템에 존재해야 하는데, InputStream은 로컬 파일뿐만 아니라 메모리나 네트워크에서 데이터를 바로 읽어올 수 있어 더 유연하다는 장점이 있다.
- ObjectMetadata를 통해 파일의 ContentType, 크기, 캐시 정책 등을 S3 객체에 추가할 수 있다. 특히 ContentType을 설정하지 않으면 기본값(application/octet-stream)이 적용되며 파일의 MIME 타입이 올바르게 지정되지 않아(MIME 타입은 “알 수 없는 파일”로 간주) 브라우저에서 파일를 열지 않고 다운로드 하려고 시도해버린다.올바른 MIME 타입(ex: image/jpeg, image/png)을 설정하면, 브라우저는 해당 파일을 다운로드 대신 바로 열어준다.
amazonS3.getUrl( String bucketName, String key )
:파일이 저장된 bucketName과 파일이 저장된 경로를 인자로 주면 저장된 object의 url을 반환해준다.
폴더 만들어 해당 저장하기
키(Key)에 폴더경로를 포함하면 S3가 자동으로 해당 폴더구조를 만들어준다.
내가 S3에 직접 폴더구조를 만들어줄 필요가 없다. 그냥 filename에 폴더 경로만 명시해주기!
String filename = folder + "/" + UUID.randomUUID().toString() + "-" + image.getOriginalFilename();
오류 1)
업로드 했는데 처음 보는 413 에러가 떴다. 찾아보니, 서버가 클라이언트로부터 전송된 요청의 크기가 너무 커서 처리할 수 없을 때 발생하는 에러였다. 이를 해결하기 위해 Spring Boot는 기본적으로 업로드 파일 크기에 제한을 두고 있는데, 이 제한을 늘려서 큰 파일도 업로드할 수 있도록 설정해야 한다.
<application.yml>
spring:
servlet:
multipart:
max-file-size: 10MB # 파일 하나의 최대 크기
max-request-size: 30MB # 전체 요청의 최대 크기
아래 사진을 통해 s3에 객체가 잘 저장되었음을 확인할 수 있다.
<이미지 s3에서 삭제>
@Transactional
public void deleteAllImages(Long postId) {
List<PostImage> images=postImageRepository.findAllPostImageByPostId(postId);
for(PostImage image:images){
if(image.getPostImageurl()!=null){
deleteImage(image.getPostImageurl());
}
}
}
//하나의 이미지 s3에서 삭제
private void deleteImage(String postImageUrl){
//s3에서 삭제
try{
//존재하는지 확인하고나서 지우기
int idx=postImageUrl.indexOf(bucketUrl);
String filenameWithUuid=postImageUrl.substring(idx+bucketUrl.length()+1);
//System.out.println("대상"+filenameWithUuid);
if(amazonS3.doesObjectExist(bucketName, filenameWithUuid)){
DeleteObjectRequest deleteObjectRequest=new DeleteObjectRequest(bucketName, filenameWithUuid);
amazonS3.deleteObject(deleteObjectRequest);
}
else{
System.out.println("대상없음");
}
} catch (S3Exception e){
throw new RuntimeException("s3에서 이미지삭제 실패", e);
}
}
doesObjectExist
: bucket 이름과, bucket에 저장할 때 사용했던 filename을 넘겨주면 해당 버킷에 해당 객체가 있나 확인할 수 있다.
deleteObject
bucket 이름과, bucket에 저장할 때 사용했던 filename을 넘겨주면 버킷에 저장된 객체를 삭제할 수 있다.
DeleteObjectRequest (AWS SDK for Java - 1.12.778)
Used for conducting this operation from a Requester Pays Bucket. If set the requester is charged for requests from the bucket. It returns this updated DeleteObjectRequest object so that additional method calls can be chained together. If a bucket is enable
docs.aws.amazon.com
오류1)
그런데,,,
객체 삭제가 안되었다ㅠㅠ
이런식으로 "대상없음"이 출력되었다,, doesObjectExist에서 해당 object를 못찾았다는 건데 대상 filename은 제대로 콘솔창에 출력되고 있었다ㅠㅠ
찾아보니 IAM 사용자 권한에 S3FullAccess 뿐만 아니라 추가적으로 GetObject와 DeleteObject 권한을 줘야되었던 것!!!
내 코드 자체가 doesObjectExist 로 객체가 존재하는지 확인하기 때문에 Delete권한 뿐만 아니라 Get 권한도 필요했다!
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject", // S3 객체 읽기 권한
"s3:DeleteObject" // S3 객체 삭제 권한
],
"Resource": "arn:aws:s3:::your-bucket-name/*"
}
]
}
root 계정으로 들어가 해당 IAM 사용자 선택해 권한 추가누르고 json으로 위의 코드 입력해주니까 잘 됐다 ㅎㅎㅎ
+ 버킷에도 권한부여해줌
+ 나중을 위해 CORS 설정도 해주어야 하는데 포스트 하단에 링크 걸어놓은 블로그 참고하기
오류2)
근데 한글 같은 경우는 여전히 같은 오류가 뜬다ㅠㅠ url을 통해 반환된 filename은 ASCII 문자 형식으로 뜨고, bucket에는 한글과 공백으로 filename이 저장되어있어, 둘을 서로 다른 filename으로 판단해 여전히 "대상없음"이라고 뜬다..
따라서 한글 파일인 경우를 고려해 디코딩 작업을 따로 해주어야 한다.
S3의 파일 URL은 브라우저 또는 클라이언트에서 접근할 때 URL 인코딩 규칙을 따르기 때문에 한글이나 공백, 특수 문자는 ASCII 문자로 변환되어 %XX 형식으로 URL에 나타나게 된다고 한다.
따라서 url.substring()을 사용해 파일명을 추출하면, 인코딩된 상태로 파일명이 추출된다.
S3에서 파일을 삭제하거나 조회할 때는 URL에서 인코딩된 파일명을 다시 디코딩해야 한글이나 특수 문자를 제대로 인식해서 버킷에 저장된 filename대로 가져올 수 있게 된다.
private void deleteImage(String postImageUrl){
//s3에서 삭제
try{
//존재하는지 확인하고나서 지우기
int idx=postImageUrl.indexOf(bucketUrl);
String filenameWithUuid=postImageUrl.substring(idx+bucketUrl.length()+1);
//System.out.println("대상"+filenameWithUuid);
//디코딩을 해야 한글 및 공백이 포함된 파일명이 깨지지 않은채 가져와짐
String decodedFileName=URLDecoder.decode(filenameWithUuid, StandardCharsets.UTF_8);
if(amazonS3.doesObjectExist(bucketName, decodedFileName)){
DeleteObjectRequest deleteObjectRequest=new DeleteObjectRequest(bucketName, decodedFileName);
amazonS3.deleteObject(deleteObjectRequest);
}
else{
System.out.println("대상없음");
}
} catch (S3Exception e){
throw new RuntimeException("s3에서 이미지삭제 실패", e);
}
}
-> 따라서 UTF_8로 디코딩 한 decodedFileName을 이용하여 doesObjectExist와 deleteObject를 수행하도록 했다.
[AWS] 📚 S3 개념 & 버킷 · 권한 설정 방법
S3 (Simple Storage Service) 개념 AWS S3는 업계 최고의 확장성과 데이터 가용성 및 보안과 성능을 제공하는 온라인 오브젝트(객체) 스토리지 서비스이다. (참고로 S 앞글자가 3개라서 S3 이라고 한다.) 쉽
inpa.tistory.com
'백 > spring boot' 카테고리의 다른 글
@RequestBody vs @ModelAttribute vs @RequestPart vs @RequestParam (0) | 2024.11.26 |
---|---|
이미지 업로드 비동기 처리 (0) | 2024.10.24 |
cascade type 속성 + N+1문제 해결법 결론 (0) | 2024.09.29 |
Base Entity 구현 (0) | 2024.09.29 |
JPA 관련 문제들 (주로 N+1문제) (0) | 2024.09.29 |