[개발] 이미지 AWS S3 업로드 및 삭제부터 CDN 적용까지

이진규·2023년 3월 30일
1

시작

이미지 파일을 AWS의 S3에 업로드, 조회, 삭제하고 CDN까지 적용하여 빠르게 로딩할 수 있도록 할려고 합니다.

이미지 S3 업로드 및 삭제부터 CDN 적용

1. AWS S3 생성

AWS의 S3에 들어가서 생성버튼을 클릭 후 위의 두 설정 빼고는 전부 디폴트 값으로 하였습니다.

2. AWS IAM 생성

spring boot에서 AWS S3에 접근하기 위한 설정으로 우리가 생성한 S3에 접근할 수 있는 정책을 가진 IAM을 만들어 줍니다. 그리고 접근 수단인 액세스 키를 발급받게 되는데 spring boot의 yml파일에 지정하면 됩니다.

IAM 사용자를 생성합니다.

그리고 위와 같이 IAM 사용자 생성 후에 S3FullAccess 권한을 추가하겠습니다.

3. springboot 설정

3-1 build.gradle

implementation "com.amazonaws:aws-java-sdk-s3:${awsJavaSdkVersion}"
ex) implementation "com.amazonaws:aws-java-sdk-s3:1.12.281

build.gradle에 위의 의존성을 추가합니다.

3-2 yml

cloud:
  aws:
    credentials:
      accessKey: ${AWS_ACCESS_KEY_ID}       # AWS IAM AccessKey 적기
      secretKey: ${AWS_SECRET_ACCESS_KEY}   # AWS IAM SecretKey 적기
    s3:
      bucket: 버킷 이름    # ex) marryting-gyunny
      dir: S3 디렉토리 이름 # ex) /gyunny
    region:
      static: ap-northeast-2
    stack:
      auto: false

application.yml에 위와 같이 작성을 하겠습니다.

3-3 config

@Configuration
public class AwsConfig {

    @Value("${cloud.aws.credentials.accessKey}")
    private String accessKey;

    @Value("${cloud.aws.credentials.secretKey}")
    private String secretKey;

    @Value("${cloud.aws.region.static}")
    private String region;

    @Bean
    public AmazonS3 amazonS3() {
        AWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey);
        return AmazonS3ClientBuilder.standard()
                .withRegion(region)
                .withCredentials(new AWSStaticCredentialsProvider(awsCredentials))
                .build();
    }
}

config 클래스를 새로 만들어 yml에 등록한 accessKey, secretKey, region을 Bean으로 등록해줍니다.

4. 코드 작성

4-1 업로드 및 삭제


@RequiredArgsConstructor
@Service
public class AttachmentService {

	@Value("${cloud.aws.s3.bucket}")
	private String bucket;
    private final AmazonS3 amazonS3;
    
    public void createAttachment(Post post, List<MultipartFile> multipartFiles) {

            for (MultipartFile file : multipartFiles) {

                String serverFileName = UUID.randomUUID() + "-" + file.getOriginalFilename();
                String thumbnailFileName = getThumbnailFileName(serverFileName);

                String fileFormatName = getFileFormatName(file);

                MultipartFile thumbnailFile = resizeAttachment(thumbnailFileName, fileFormatName, file,
                    getTargetWidth(post.getKind()), getTargetHeight(post.getKind()));

                putAmazonS3Object(serverFileName, file, new ObjectMetadata());
                putAmazonS3Object(thumbnailFileName, thumbnailFile, new ObjectMetadata());

                attachmentRepository.save(
                    Attachment.builder()
                        .post(post)
                        .originalFileName(file.getOriginalFilename())
                        .serverFileName(serverFileName)
                        .thumbnailFileName(thumbnailFileName)
                        .serverFileUrl(getAmazonS3Url(serverFileName))
                        .thumbnailFileUrl(getAmazonS3Url(thumbnailFileName))
                        .build());
            }
        }

        private void putAmazonS3Object(String fileName, MultipartFile file, ObjectMetadata objMeta) {
            try {
                objMeta.setContentLength(file.getInputStream().available());
                amazonS3.putObject(bucket, fileName, file.getInputStream(), objMeta);
            } catch (IOException e) {
                throw new FailConvertOutputStream();
            }
        }
        
    	public void deleteAttachment(Long postId, List<String> serverFileNames) {

		for (String serverFileName : serverFileNames) {

			if (!"".equals(serverFileName) && serverFileName != null) {
				boolean isExistObject = amazonS3.doesObjectExist(bucket, serverFileName);

				if (!isExistObject) {
					throw new NotFoundObjectException();
				}

				Attachment attachment = getAttachmentByServerFileName(serverFileName);

				if (!attachment.getPost().getId().equals(postId))
					throw new NotEqualAttachmentAndPostAttachment();

				amazonS3.deleteObject(bucket, serverFileName);
				amazonS3.deleteObject(bucket, getThumbnailFileName(serverFileName));

				attachmentRepository.delete(attachment);
			}
		}
	}

저는 다중 파일을 업로드 하고 썸네일 이미지 또한 생성해야 하기 때문에 다음과 같이 작성하였습니다. 썸네일에 대한 글은 제가 작성한 썸네일 블로그에서 참조하면 좋을 것 같습니다.

업로드는 간단하게 버킷에 대한 정보만 가지고 있으면 간단하게 AWS S3에 올릴 수 있습니다. 삭제 또한 버킷에 대한 정보와 서버에 저장된 파일 이름으로 간단히 진행할 수 있습니다.

4-2 조회

조회같은 경우는 캐싱 개념을 이용한 AWS의 CDN을 적용할려고 합니다. 이미지를 조회할 때마다 S3에 URL 요청을 보내게 될 텐데 당장 게시글 목록 조회를 생각해 보면 게시글의 썸네일 이미지가 있고 무한 스크롤로 구현되어 있습니다. 드래그를 내릴 때마다 게시글 목록을 조회하는데 드래그 몇 번으로 순식간에 썸네일 이미지 URL 요청양이 엄청 많아집니다. 과금에 대한 위험도 있고, 비효율적이기 때문에 CDN을 적용합니다.

4-2-1 CDN 생성

CDN 생성에 대한 내용은 다른 분이 작성한 블로그를 참조하면 좋을 것 같습니다.

4-2-2 yml 설정

cloud:
  aws:
    credentials:
      accessKey: ${AWS_ACCESS_KEY_ID}       # AWS IAM AccessKey 적기
      secretKey: ${AWS_SECRET_ACCESS_KEY}   # AWS IAM SecretKey 적기
    s3:
      bucket: 버킷 이름    # ex) marryting-gyunny
      dir: S3 디렉토리 이름 # ex) /gyunny
    region:
      static: ap-northeast-2
    stack:
      auto: false
    cloudfront:
      domain: CDN도메인 # CDN 도메인 적기 ex) https://~~~

위에서 작성했던 yml파일에 cloudfront 관련 내용만 추가해줍니다.

4-2-3 코드 작성

@RequiredArgsConstructor
@Service
public class AttachmentService {

	@Value("${cloud.aws.cloudfront.domain}")
	private String cloudfrontDomain;
    
    private String getAmazonS3Url(String fileName) {
		return cloudfrontDomain + amazonS3.getUrl(bucket, fileName).getPath();
	}
}

그 다음 조회 코드를 작성하게 되는데 버킷에 대한 정보와 파일이름을 가지고 경로를 반환받은 다음 도메인을 앞에 써주게 되면 완성입니다.

참고자료

AWS S3를 이용한 블로그
AWS S3를 이용한 블로그2
S3 업로드 및 CDN 이용

profile
항상 궁금해하고 공부하고 기록하자.

0개의 댓글