목차
1. AWS S3 버킷 생성
2. AWS IAM 생성
3. S3 버킷 정책 편집
4. SpringBoot 설정
5. Config, Controller, Service 작성
1. AWS S3 버킷 생성
버킷 만들기를 클릭해서 S3 버킷을 생성한다.
📷 Amazon S3 란?
- 객체 스토리지 서비스
- 이미지 서버로 활용하는 경우가 많다!
🧺 Bucket 이란?
- Amazon S3에 저장된 객체에 대한 컨테이너
- 버킷에 저장할 수 있는 객체 수에는 제한이 없다
- 계정에 버킷을 최대 100개까지 포함 가능
- 버킷 이름과 AWS 리전 설정 (ex. 버킷 이름 = memotion_bucket)
- 객체 소유권은 default로 설정
- IAM으로 권한 부여하고, 허용된 사용자만 접근 가능하도록 설정할 예정이므로 ACL 비활성화하는 것이 좋다!
- 액세스 차단 설정 해제 : 파일 조작 권한은 Spring Security를 통해 설정할 예정!
- ✅️ 하단의 현재 설정으로 인해 이 버킷과 그 안에 포함된 객체가 퍼블릭 상태가 될 수 있음을 알고 있습니다. 체크
- 버킷 생성하기
2. AWS IAM 생성
- IAM > 사용자 > 사용자 추가 선택
- S3 접근하려면 IAM 사용자에게 S3 접근 권한 주기!!!!!!
- S3에 접근할 땐 액세스키 + 비밀키로 접근하기
- 다음 클릭 후 직접 정책 연결 선택
- 권한 정책에서 AmazonS3FullAccess를 추가
- 권한 추가 클릭 시 권한 추가 완료
- 권한 추가가 완료되었다면 사용자 생성도 완료된 것이다!
- 이번엔 외부에서 접속할 수 있도록 사용자의 액세스키를 생성해주려고 한다
- IAM > 액세스 관리자 > 사용자 > 생성한 사용자 이름 > 보안 자격 증명 > 액세스 키 만들기 클릭
- 액세스 키 모범 사례 및 대안 : 아무거나 클릭해도 된다
- 설명 태그를 설정하고 액세스 키 만들기를 클릭한다
- 액세스 키 생성이 완료되면 공개키와 비밀키를 확인할 수 있다
- 더 이상 액세스키를 확인할 수 없기 때문에 .csv 파일을 저장해두는 것이 좋다
3. S3 버킷 정책 편집
- 버킷 > 권한 > 버킷 정책 > 편집을 클릭
- 정책이 비어있으면 새 문 추가 선택 후 아래의 내용을 넣어준다
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "Statement1",
"Principal": "*",
"Effect": "Allow",
"Action": "s3:*",
"Resource": "arn:aws:s3:::<버킷 이름>/*"
}
]
}
4. SpringBoot 설정
먼저 build.gradle에 다음과 같은 의존성을 주입해준다.
⚠️ 다른 내용은 넣지 않았고, S3와 관련된 설정만 작성해두었다.
build.gradle
dependencies {
// AWS S3
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
}
다음으로 application.yml 파일안에 S3 버킷 정보 및 액세스 키 정보를 작성해주어야 한다. 허용된 사용자만 S3에 접근할 수 있도록 하는 접근 제어의 첫 단계이다.
또한 프리티어를 사용한다면 S3 표준 스토리지는 5GB, GET 요청 20000건, PUT 요청 2000건으로 제한되어 있으므로 파일 업로드 크기의 제한을 두어야 한다!
⚠️ 다른 내용은 넣지 않았고, S3와 관련된 설정만 작성해두었다.
(prod 파일의 전문을 가져와서 키값들이 하드코딩 되어있지만, local에서는 환경 변수로 주입받는게 좋다!)
application.yml
spring:
# 파일 업로드 크기 제한
servlet:
multipart:
max-file-size: 5MB
max-request-size: 10MB
cloud:
aws:
s3:
bucket: <버킷 이름>
credentials:
access-key: <액세스키 값>
secret-key: <비밀키 값>
region:
static: ap-northeast-2
auto: false
stack:
auto: false
5. Config, Controller, Service 작성
AWSS3Config.java
@Configuration
public class AWSS3Config {
@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 credentials = new BasicAWSCredentials(accessKey, secretKey);
return (AmazonS3Client) AmazonS3ClientBuilder
.standard()
.withRegion(region)
.withCredentials(new AWSStaticCredentialsProvider(credentials))
.build();
}
}
AWSS3Controller.java
@RestController
@RequiredArgsConstructor
public class AWSS3Controller {
private final AWSS3Service awsS3Service;
// S3 서버에 이미지 업로드
@PostMapping("/s3")
public BaseResponse<String> uploadFile(@RequestPart MultipartFile multipartFile) throws IOException {
String fileName = null;
if(multipartFile != null && !multipartFile.isEmpty()) {
fileName = awsS3Service.uploadFile(multipartFile);
}
return BaseResponse.onSuccess(fileName);
}
// S3 서버에 저장된 이미지 교체
@PatchMapping("/s3")
public BaseResponse<String> modifyFile(@RequestParam("fileUrl") String fileUrl, @RequestPart MultipartFile multipartFile) throws IOException {
if(fileUrl != null) {
String[] url = fileUrl.split("/");
awsS3Service.deleteImage(url[3]); // https~ 경로 빼고 파일명으로 삭제
if(multipartFile != null && !multipartFile.isEmpty()){
String fileName = awsS3Service.uploadFile(multipartFile);
return BaseResponse.onSuccess(fileName);
}
}
throw new BaseException(ErrorCode.AWS_S3_ERROR);
}
}
AWSS3Service.java
@Slf4j
@Service
@RequiredArgsConstructor
public class AWSS3Service {
@Value("${cloud.aws.s3.bucket}")
private String bucket;
private final AmazonS3 amazonS3;
// MultipartFile을 전달받아 File로 전환 후 S3 서버에 파일 업로드
public String uploadFile(MultipartFile file) throws IOException {
String fileName = createFileName(file.getOriginalFilename());
try{
ObjectMetadata objectMetadata = new ObjectMetadata();
objectMetadata.setContentLength(file.getSize());
objectMetadata.setContentType(file.getContentType());
InputStream inputStream = file.getInputStream();
amazonS3.putObject(new PutObjectRequest(bucket, fileName, inputStream, objectMetadata));
} catch(AmazonServiceException e){
e.printStackTrace();
} catch (IOException e){
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "이미지 업로드에 실패했습니다.");
}
log.info("서버에 등록한 파일명: " + fileName);
// S3 이미지 서버에 등록한 URL을 반환
return amazonS3.getUrl(bucket, fileName).toString();
}
// S3 서버에서 파일 삭제
public boolean deleteImage(String fileName) {
amazonS3.deleteObject(new DeleteObjectRequest(bucket, fileName));
return true;
}
// 기존 확장자명을 유지하면서, 식별되는 파일명을 생성
private String createFileName(String fileName) {
return UUID.randomUUID().toString().concat(getFileExtension(fileName));
}
// 파일 확장자 알아내기
private String getFileExtension(String fileName) {
try {
return fileName.substring(fileName.lastIndexOf("."));
} catch (StringIndexOutOfBoundsException e) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "잘못된 형식의 파일(" + fileName + ") 입니다.");
}
}
}
나는 S3에 업로드 된 파일의 URL 자체를 DB image 칼럼에 저장했기 때문에 기존 이미지를 교체하려고 할 때
① 기존 이미지 삭제
② 새 이미지 업로드 후 DB 데이터 변경
로직을 사용했다.
Service에서 각 메소드의 역할은 다음과 같다.
Method 명 | 역할 |
uploadFile | MultipartFile을 전달받아 S3 서버에 파일 업로드 후 해당 파일의 URL 반환 |
deleteImage | S3 서버에서 파일 삭제 |
createFileName | 기존 확장자명을 유지하면서, 식별되는 파일명 생성 (ex. cat.jpg → adf5468764adf.jpg) |
getFileExtension | 파일 확장자 알아내기 |
'Framework > Spring' 카테고리의 다른 글
[SpringBoot] Swagger 3.0 + 전역적 Bearer 토큰 적용 (0) | 2023.10.31 |
---|---|
[SpringBoot] Naver CLOVA Sentiment를 활용한 감정분석 (0) | 2023.08.30 |
[SpringBoot] ChatGPT를 활용한 API 작성 (0) | 2023.08.21 |
[SpringBoot] JPA ConverterNotFoundException 에러 (0) | 2023.05.16 |
[SpringBoot] JPA NoViableAltException: unexpected token: value 에러 (0) | 2023.04.02 |