- 지난번에 구현한 Local에 파일 저장하기
이번에는 S3에 저장으로 바꾸어보았다- 팀원들 각자 로컬에 이미지를 저장하다보니 공유가 안되어서, 한곳에서 관리하기 위해 S3와 연동해보았다
- S3의 가장 큰 장점은 확장,축소에 신경쓰지 않아도 된다는 점이라고 생각한다
- 간단하게 S3의 특징도 같이 정리해보기
Amazon S3 (AWS Simple Storage Service)
인터넷용 객체 스토리지 서비스
- 개발자가 웹 규모 컴퓨팅 작업을 더 쉽게 할 수 있도록 설계되어 있음
- 쉽게 말하면 Goole One, iCloud 같은 파일 저장 서비스
- bucket이라는 폴더에 object인 파일을 저장할 수 있음
특징
- S3의 버킷은 무한대의 객체 저장 가능
-> 확장,축소에 신경쓰지 않아도 됨- 일반적인 파일서버는 트래픽이 증가함에 따라서 장비를 증설하는 작업을 해야 하는데 S3는 이와 같은 것을 대행
= 많은 사용자가 접속을 해도 이를 감당하기 위해서 시스템적인 작업을 하지 않아도 됨
- 일반적인 파일서버는 트래픽이 증가함에 따라서 장비를 증설하는 작업을 해야 하는데 S3는 이와 같은 것을 대행
- 제공하는 단순한 웹 서비스 인터페이스 사용
-> 언제 어디서나 원하는 양의 데이터를 저장, 검색 가능 - 높은 확장성 + 신뢰성 + 빠름 + 경제적인 데이터 스토리지 인프라에 access할 수 있음
- Amazon이 자체 웹 사이트의 글로벌 네트워크 운영에 사용하는 것과 같은
- 단독 스토리지로도 사용할 수 있고, 다른 AWS service(EC2, EBS, Glacier 등)랑 함께 사용할 수도 있음
- HTTPS protocol을 사용하여 SSL로 암호화된 end point를 통해 데이터를 안전하게 업로드, 다운로드 가능
- 키 관리 방법 선택 가능
- 상주 데이터를 자동으로 암호화 하고 AWS KMS를 통해 S3에서 키를 관리하게 하는 방법
- 고유한 키를 제공하는 방법
- 키 관리 방법 선택 가능
- 사용한 스토리지만큼 요금 청구
- 해당 region 내에서는 데이터 송수신은 무료 (다른 AWS region으로는 무료가 아님)
용어 정리
- Object : S3에 데이터가 저장되는 기본 단위
- S3에 저장된 데이터 하나 하나를 object라고 명명
- object 하나의 크기는 1 byte 부터 5TB까지 허용
- 파일과 메타데이터로 이루어져있음
- 메타데이터는 MIME 형식으로 파일 확장자를 통해 자동으로 설정됨(사용자 임의로도 지정 가능)
- Bucket : S3에서 생성할 수 있는 최상위 디렉토리 개념
- 연관된 object들을 그룹핑한 최상위 디렉토리
- bucket 단위로 region 지정 가능
- bucket에 포함된 모든 object에 대해 일괄적으로 인증과 접속제한 걸기 가능
- 이름이 s3 region 중 유일해야함
- 계정별로 버킷 100개까지 생성 가능
- 버킷에 저장할 수 있는 객체수와 용량은 무제한
- 하위 폴더는 prefix라고 부름 (실제로 폴더 역할을 하는게 아니고 단순히 하나의 경로 역할만 함)
- 버전관리 : s3에 저장된 object들의 변화를 저장함
- ex) A라는 object를 사용자가 삭제하거나 변경해도 각각의 변화를 모두 기록하기 때문에 실수 만회 가능
- 표준스토리지 : 객체에 대해 높은 내구성과 가용성을 제공하는 스토리지 서비스
- 비용 높음
- 유실되면 안되는 원본 데이터, 민감정보, 개인정보 등의 중요한 데이터를 저장에 적합
- RSS(Reduced Redundancy Storage) : 일반 S3 객체에 비해 데이터가 손실될 확률이 높은 스토리지 서비스
- 대신에 가격이 저렴
- 복원 가능한 데이터(ex) thumbnail image) 저장에 적합
- 그럼에도 불구하고 물리적인 하드 디스크 대비 400배 가량 안전하다는 것이 아마존의 주장
- Glacier : 매우 저렴한 가격으로 데이터를 저장 할 수 있는 스토리지 서비스
S3 bucket 생성
- 버킷 만들기
- 버킷 이름, region 아시아 태평양(서울)로 선택
- 퍼블릭 액세스 설정
- 퍼블릭 액세스 차단하면 -> IAM에서 AWS access key와 AWS secret key를 발급받고 이를 이용해서 S3 객체에 접근할 수 있음
- 퍼블릭 정책 활성화
- 정책이 명시된 Json을 복사 후 버킷 정책에 입력해주기
action does not apply to any resource(s) in statement s3 bucket
가 떠서 찾아보니 일부 서비스에서는 개별 리소스에 대한 작업을 지정할 수 없다고 한다- Resource 끝에 와일드카드를 붙여주면 되었다
"Resource": "arn:aws:s3:::~~~/*"
- 액세스가 퍼블릭으로 변경 = 외부에서 S3에 접근 가능한 상태
AWS S3 설정
dependency 추가
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
application-aws.yml
- access key, secret key 등은 노출이 되면 일이 커지므로 겸사겸사 별도로 관리한다
cloud:
aws:
credentials:
access-key: {access key}
secret-key: {secret key}
region:
static: ap-northeast-2
auto: false
s3:
bucket: {bucket 이름}
stack:
auto: false
S3 configuration file
@Configuration
public class AwsConfig {
private final String accessKey;
private final String secretKey;
private final String region;
public AwsConfig(
@Value("${cloud.aws.credentials.access-key}") String accessKey,
@Value("${cloud.aws.credentials.secret-key}") String secretKey,
@Value("${cloud.aws.region.static}") String region) {
this.accessKey = accessKey;
this.secretKey = secretKey;
this.region = region;
}
@Bean
public AmazonS3 amazonS3() {
AWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey);
return AmazonS3ClientBuilder.standard()
.withRegion(region)
.withCredentials(new AWSStaticCredentialsProvider(awsCredentials))
.build();
}
}
- property 파일에 작성한 값들을 읽어와서 AmazonS3 객체를 만들어 Bean으로 주입해주는 것
구현하기
S3에 이미지 업로드 로직
@Service
public class ImageS3Service implements ImageService {
private final String bucket;
private final AmazonS3 amazonS3;
private final ImageRepository imageRepository;
public ImageS3Service(
@Value("${cloud.aws.s3.bucket}") String bucket, AmazonS3 amazonS3, ImageRepository imageRepository) {
this.bucket = bucket;
this.amazonS3 = amazonS3;
this.imageRepository = imageRepository;
}
- AmazonS3 주입
@Override
public void register(List<MultipartFile> multipartFiles, Long referenceId, DomainType domainType) {
if (multipartFiles != null && !multipartFiles.isEmpty()) {
List<Image> images = uploadImages(multipartFiles, referenceId, domainType);
imageRepository.saveAllBulk(images);
}
}
private List<Image> uploadImages(List<MultipartFile> multipartFiles, Long referenceId, DomainType domainType) {
return multipartFiles.stream()
.map(multipartFile -> uploadImage(multipartFile, referenceId, domainType))
.collect(Collectors.toList());
}
private Image uploadImage(MultipartFile multipartFile, Long referenceId, DomainType domainType) {
String originalName = multipartFile.getOriginalFilename();
String uniqueName = createUniqueName(originalName);
String fullPath = storeAndGetPath(multipartFile, uniqueName);
return Image.builder()
.originalName(originalName)
.fullPath(fullPath)
.referenceId(referenceId)
.domainType(domainType)
.build();
}
private String createUniqueName(String originalName) {
String extension = extractExtension(originalName);
String uuid = UUID.randomUUID().toString();
return uuid + "-" + LocalDate.now() + extension;
}
private String extractExtension(String originalName) {
try {
return originalName.substring(originalName.lastIndexOf("."));
} catch (StringIndexOutOfBoundsException e) {
throw new FileUploadFailedException("invalid file format");
}
}
- 여기까지는 지난번과 같다
- 새로운 파일명으로 만드는 방식과 확장자 추출하는 부분만 조금 바꿔보았다
private String storeAndGetPath(MultipartFile multipartFile, String uniqueName) {
ObjectMetadata objectMetadata = new ObjectMetadata();
try {
objectMetadata.setContentType(multipartFile.getContentType());
objectMetadata.setContentLength(multipartFile.getInputStream().available());
amazonS3.putObject(bucket, uniqueName, multipartFile.getInputStream(), objectMetadata);
} catch (IOException e) {
throw new FileUploadFailedException("Failed to save (S3)");
}
return amazonS3.getUrl(bucket, uniqueName).toString();
}
- 이 부분이 S3에 업로드 하는 로직
- S3 API 메소드
- putObject(String bucketname, String key, InputStream input, ObjectMetadata metadata)
- 요거 선택함
- 여기서 key 값은 버킷 내에서 객체를 찾기위해 사용되는 고유 식별자를 의미
- putObject(String bucketname, String key, File file)
- file을 넘겨줄 때 실제 파일이 존재해야해서, 로컬에도 파일이 저장되게 됨 -> 자원 낭비
- putObject(PutObjectRequest putObjectRequest) : S3에 대한 세부 설정 가능
- 세부 설정 불필요했음
- putObject(String bucketname, String key, InputStream input, ObjectMetadata metadata)
ObjectMetadata
사용 : 파일에 대한 정보 추가- 내가 선택한 메서드에서는 InputStream을 통해 byte만 전달되기 때문에 해당 파일에 대한 정보가 없어서
getUrl()
: S3에 업로드된 image URL 가져옴
S3에 이미지 삭제 로직
@Override
public void deleteAllByReference(Long referenceId, DomainType domainType) {
List<Image> images = imageRepository.findAllByReferenceIdAndDomainType(referenceId, domainType);
images.stream()
.map(image -> image.getFullPath().substring(56))
.forEach(this::deleteImage);
imageRepository.deleteAllByReferenceIdAndDomainType(referenceId, domainType);
}
private void deleteImage(String fileName) {
try {
amazonS3.deleteObject(bucket, fileName);
} catch (AmazonServiceException e) {
throw new FileDeleteFailedException("Failed to delete (S3)");
}
}
}
- 위의 2개 메서드 중 두번째가 실제로 S3의 이미지 파일을 지우는 로직
- 마찬가지로, deleteObject(String bucketname, String key)를 사용했다
- 삭제는 업로드보다도 더 쉽게 구현 가능
구현하다 만난 에러
com.amazonaws.SdkClientException: Failed to connect to service endpoint:
Caused by: java.net.SocketTimeoutException: connect timed out
aws sdk error
: spring-cloud-starter-aws 의존성 주입시, 로컬환경은 aws환경이 아니기때문에 나는 에러라고 함- InstanceMetadataServiceResourceFetcher class의 readResource를 호출하면서 발생한 에러
- EC2 인스턴스가 아닌 다른곳에서 해당 애플리케이션을 실행할 때 나는 에러
- 서비스의 endpoint를 연결하지 못해 발생하는 에러
- EC2의 메타데이터를 읽다가 발생하는 에러로써, EC2인스턴스가 아닌 곳에서는 의미가 없는 에러
- application이 돌아는가는, 꼭 해결해야하는 에러는 아니였지만,
실행 시 시간이 오래걸림 + 콘솔에 찍히는게 거슬림으로 조치를 취해보았다- 관련 클래스를 auto configuration loading에서 제외시켰다
@SpringBootApplication(
exclude = {
org.springframework.cloud.aws.autoconfigure.context.ContextInstanceDataAutoConfiguration.class
}
)
- 또 다른 해결방안 : VM option 커스텀 + yml 파일에 로깅관련 설정 추가하기
-Dcom.amazonaws.sdk.disableEc2Metadata=true
logging:
level:
com:
amazonaws:
util:
EC2MetadataUtils: error
Reference
https://aws.amazon.com/ko/s3/faqs/
https://aws.amazon.com/ko/blogs/korea/getting-started-with-spring-boot-on-aws/
https://opentutorials.org/course/608/3006
https://devocean.sk.com/blog/techBoardDetail.do?ID=163606
https://stackoverflow.com/questions/44228422/s3-bucket-action-doesnt-apply-to-any-resources
https://docs.aws.amazon.com/ko_kr/sdk-for-java/v1/developer-guide/examples-s3-objects.html