오늘도 프로젝트를 진행하느라 바쁜 하루를 보냈던 것 같다. 나는 AWS S3 저장소를 이용해 프로필 사진 이미지를 저장하는 기능 구현을 맡아서 이 부분을 진행하게 되었다.
S3가 무엇인지 정확히 모르는 상태였기 때문에 어떤 서비스인지부터 찾아보게 되었고, 이후 회원가입 및 계정 보안을 위한 설정을 진행했는데 이 과정도 매우 복잡하고 이것저것 설정해야 할 것이 많아 생각보다 오랜 시간이 걸리게 되었다.
설정 이후에 스프링과 연결하여 이미지 업로드를 구현하는 부분은 생각보다 오래 걸리지 않았다.
여기에 그 내용들을 짧게나마 정리해본다.
1. AWS S3 저장소 이용해보기
1-1. S3에 대한 간략한 정리
AWS S3는 Simple Storage Service(S3, 간단한 저장 서비스)는 객체 스토리지 서비스이다. 간단하게 파일 서버로의 역할을 하는 서비스이다.
S3의 객체는 파일의 소유자, 이름, 데이터, 버전 아이디와 메타데이터(http 통신의 메타데이터와 S3 고유의 메타데이터)로 구성되어있다. 파일을 업로드 하면 메타데이터와 함께 하나의 객체로 저장되게 된다. 객체 스토리지 서비스인 S3에는 파일만 보관이 가능하며 어플리케이션의 설치가 불가능하다.
S3는 99.999999999%의 내구성을 제공하도록 설계되었다. 최소 3개의 가용영역에 데이터를 분산 저장하기 때문에 매우 안전하다. 이 데이터는 내가 저장할 때 지정한 리전 바깥으로는 나가지 않는다는 특징이 있다. 리전과 가용영역에 대해서는 찾아보면 잘 적혀져 있는 글들이 많으니 참고해보면 좋을 것 같다.
S3를 이용하려면 먼저 버킷을 생성해야한다. 이 버킷은 S3의 저장공간을 구분하는 단위이다. 하나의 프로젝트, 폴더, 디렉토리와 같은 개념이라고 생각하면 된다. 버킷 안에는 또 다른 폴더가 있을 수 있으며, 이 버킷에 파일이 보관되게 된다. 버킷의 이름은 전 세계에서 고유 값(unique)이기 때문에 중복된 이름을 사용할 수 없다.
S3를 이용하는 이유는 다음과 같다. 일반적인 파일 서버를 운영한다면 트래픽이 증가함에 따라 서버를 증설하는 작업이 필요하게 되는데 S3를 이용하면 S3가 알아서 이런 작업을 대행하기 때문에 확장에 대한 걱정이 사라지게 된다. 다양한 보안 기능 역시 제공하고, 내구성이 뛰어나며 사용한 만큼만 비용이 지불되기 때문에 비용면에서도 효율적이라고 할 수 있을 것이다.
1-2. S3를 이용하여 프로필 사진 저장 기능 구현해보기
먼저 AWS 회원 가입 및 여러 보안 설정을 해야한다. 꽤 오래 걸리는 작업이었다. S3 서비스 이용을 위한 IAM 사용자를 따로 생성하고, 버킷을 하나 만들었다.
그런 다음 발급받은 엑세스 키와 비밀 엑스스 키를 이용해 Spring Boot 프로젝트와 연동하는 일만 남았다.
먼저 의존성 추가부터 해주었다. 다음 두 줄만 추가하면 된다.
implementation("org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE")
implementation("com.amazonaws:aws-java-sdk-s3:1.12.741")
그런 다음 application.yml 파일에 다음 사항들을 입력해준다.(CloudFormation을 사용하지 않을 것이기 때문에 cloud.aws.stack.auto 설정을 꺼주어야한다.)
cloud:
aws:
s3:
bucket: {버킷 이름}
region:
static: {지역}
credentials:
access-key: {엑세스 키}
secret-key: {시크릿 키}
stack:
auto: false
그리고 AWS S3를 이용하기 위해 다음과 같이 설정 클래스를 만들어 주면 된다.
import com.amazonaws.auth.AWSStaticCredentialsProvider
import com.amazonaws.auth.BasicAWSCredentials
import com.amazonaws.services.s3.AmazonS3Client
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
class S3Config(
@Value("\${cloud.aws.credentials.access-key}") val accessKey: String,
@Value("\${cloud.aws.credentials.secret-key}") val secretKey: String,
@Value("\${cloud.aws.region.static}") val region: String
) {
@Bean
fun amazonS3Client(): AmazonS3Client {
val credentials = BasicAWSCredentials(accessKey, secretKey)
return AmazonS3ClientBuilder
.standard()
.withRegion(region)
.withCredentials(AWSStaticCredentialsProvider(credentials))
.build() as AmazonS3Client
}
}
다음은 S3에 이미지 파일을 저장하고 삭제하는 것이 가능한 서비스 클래스를 만들 차례다. 여러가지를 참고하여 아래와 같이 이미지를 업로드하고, URL을 가져오고, 이미지를 삭제하는 함수를 만들 수 있었다.
import com.amazonaws.services.s3.AmazonS3
import com.amazonaws.services.s3.model.ObjectMetadata
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Component
import org.springframework.web.multipart.MultipartFile
import java.util.UUID
@Component
class S3FileManagement(
@Value("\${cloud.aws.s3.bucket}")
private val bucket: String,
private val amazonS3: AmazonS3,
) {
fun uploadImage(multipartFile: MultipartFile): String {
val originalFilename = multipartFile.originalFilename
?: throw IllegalArgumentException("The image file name is incorrect or missing.")
checkImageFormat(originalFilename)
val fileName = "${UUID.randomUUID()}-${originalFilename}"
val objectMetadata = setFileDateOption(
multipartFile = multipartFile
)
amazonS3.putObject(bucket, fileName, multipartFile.inputStream, objectMetadata)
return fileName
}
fun getUrl(fileName: String): String {
return amazonS3.getUrl(bucket, fileName).toString()
}
fun delete(fileName: String) {
amazonS3.deleteObject(bucket, fileName)
}
// contentType을 image/jpeg로 두고 disposition을 inline으로 두어야
// URL 접속시 바로 다운로드 되지않고 브라우저 창에서 이미지를 볼 수 있다
private fun setFileDataOption(
multipartFile: MultipartFile
): ObjectMetadata {
val objectMetadata = ObjectMetadata()
objectMetadata.contentType = "image/jpeg"
objectMetadata.contentLength = multipartFile.inputStream.available().toLong()
objectMetadata.contentDisposition = "inline"
return objectMetadata
}
// 이미지 파일인지 체크하는 함수
private fun checkImageFormat(fileName: String) {
val regex = Regex("""(?i)\.(jpg|jpeg|png|gif|bmp|webp)$""")
if (!regex.containsMatchIn(fileName)) throw IllegalArgumentException("Invalid file format: $fileName")
}
}
프로필 사진을 업로드하는 것이기 때문에 유저 컨트롤러에도 수정할 부분이 많았다. 일단 파일을 입력으로 받을 수 있어야 했기 때문에 @RequestPart로 수정을 하였다. @RequestBody랑은 같이 사용할 수 없기 때문에 @RequestBody도 @RequestPart로 변경하였다.
그렇게 컨트롤러를 수정하고 서비스 부분에 S3 서비스를 가져와서 추가, 수정한 결과 정상적으로 사진을 업로드하고 URL을 반환받고 저장할 수 있게 되었다. 이 부분 코드는 간단하기 때문에 따로 여기에 정리를 하지는 않으려고 한다.
이렇게 다 완성을 하였는데 뒤늦게 Presigned URL이라는 것에 대해 알게 되었다. 시간이 많았다면 추가로 공부하고 적용해보려고 했는데 일단은 여기서 만족해야할 것 같다. 다음 과제나 프로젝트 때는 꼭 적용할 수 있도록 해봐야겠다.
2. 오늘 배운 것
- S3 저장소에 대해서 알아보고 직접 이용해보는 시간을 가졌다.
- 완전히 처음 접하는 것이라 걱정도 많았는데 여러가지 참고하면서 직접 구현해보면서 꽤 재미난 시간을 보낼 수 있었다.
'오늘 배운 것' 카테고리의 다른 글
24-06-16 알고리즘 문제 풀이 (0) | 2024.06.16 |
---|---|
24-06-15 QueryDsl을 이용한 최적화 (0) | 2024.06.15 |
24-06-13 JPA 복합키 매핑하기 (0) | 2024.06.13 |
24-06-12 알고리즘 문제 풀이 (0) | 2024.06.12 |
24-06-11 알고리즘 문제 풀이 (0) | 2024.06.11 |