이번 게시물에서는 Amazon S3에서 이미지를 업로드 하기 위한 Pre-Sign url를 생성하는 방법을 다룰 것이다!!
요즘 Naver Clova OCR을 활용하여 텍스트를 추출하여 AI 모델에 해당 결과를 프롬프트에 포함하여 정해진 json 형식에 맞추어 결과를 반환하는 일을 하고 있는데, OCR에 이미지를 url로 요청하게 되는데 이때 이미지 호스팅이 필요하고, 이 이유 외에도 추후에 모델 학습이나 비지니스 로직에 의해서 변환을 시도한 이미지를 저장해두는 편이 좋기 때문에 S3에 호스팅하기로 결정했다.
🤔 Presigned url 이란?
미리 서명된 url이다. 다른 사람(클라이언트)로 하여금 버킷에 객체를 업로드/조회할 수 있다. 해당 url을 사용할 경우 AWS 보안 자격 증명이나 권한이 없어도 접근 할 수 있다.
해당 URL을 생성하는 사용자의 권한에 따라서 제한되며, 발급할 때 Expire Time을 지정할 수 있다.
S3 Bucket 생성
가볍게 짚고 넘어가자면, S3에서 Bucket은 폴더의 개념에 해당하고 Object는 사진에 대응한다. 물론 Bucket안에서 세부 구분이 존재하기는 하나, Bucket안에 Bucket이 들어갈 수 있는 것은 아니다. Obect의 Key값에 /를 사용하면 폴더와 같이 사용할 수 있다.
Bucket을 생성할 때는 퍼블릭 액세스 차단설정 4가지를 다 풀어줘야한다.
그리고 CORS 설정과 추가적인 설정을 해주어야 하는데, 아래와 같이 버킷 정책을 등록해주고
{
"Version": "2024-04-19",
"Statement": [
{
"Sid": "Statement1",
"Effect": "Allow",
"Principal": "*",
"Action": [
"s3:Put*",
"s3:Get*"
],
"Resource": "arn:aws:s3:::image-test/*"
}
]
}
CORS 설정을 해주어야 한다.
[
{
"AllowedHeaders": [
"*"
],
"AllowedMethods": [
"HEAD",
"GET",
"PUT",
"POST"
],
"AllowedOrigins": [
"*"
],
"ExposeHeaders": [
"ETag"
]
}
]
외부에서 호스팅된 서버의 자원을 사용하려고 요청하는 것이기 때문에 CORS와 엑세스 권한을 설정해주는 것이 맞다.
Configure Amazon S3 Client
Amazon S3 Client의 빈을 생성한다.
@Configuration
class AmazonS3Config {
@Value("\${amazon_s3_access_key}")
private lateinit var accessKey: String
@Value("\${amazon_s3_secret_key}")
private lateinit var secretKey: String
@Bean
fun amazonS3(): AmazonS3 {
val awsCredentials = BasicAWSCredentials(accessKey, secretKey)
return AmazonS3ClientBuilder.standard()
.withCredentials(AWSStaticCredentialsProvider(awsCredentials))
.withRegion(Regions.AP_NORTHEAST_2)
.build()
}
}
BasicAwsCredientials를 통해서 인증정보를 입력하는데, IAM에서 S3에 Full Access 권한이 있는 사용자를 생성하여 Key값을 넣도록 한다.
이렇게 하면 먼저 amazonS3에 관한 인증정보가 들어가있는 Spring Bean을 생성할 수 있다.
PUT Request Presigned url 발급 코드
val generatePresignedRequest: GeneratePresignedUrlRequest =
GeneratePresignedUrlRequest(bucketName, objectKey)
.withMethod(com.amazonaws.HttpMethod.PUT)
.withExpiration(expiration)
.withContentType(contentType.mimeType)
amazonS3Config.amazonS3().generatePresignedUrl(generatePresignedRequest).toString()
해당 코드를 통해서 PUT 요청이 가능한 Presigned url을 생성할 수 있다.
여기서 주의할 점은 반드시 ContentType을 지정해주어야 한다는 점이다. 그렇지 않으면 파일이 저장될 때 형식을 지정하지 않았기 때문에 S3에서 파일이 손상된 형태로 들어가게 된다. 또한 Client에서도 반드시 ContentType를 Header에 포함하여 보내야 한다!
Enum으로 Content Type 관리
참고로 나는 코드의 재사용성을 위해서 Content Type을 Enum으로 관리하고 있다.
enum class ContentType(val mimeType: String, val extension: String) {
IMAGE_PNG("image/png", ".png"),
IMAGE_JPEG("image/jpeg", ".jpeg"),
IMAGE_JPG("image/jpg", ".jpg")
}
Get Request Presigned url 발급 코드
fun getGetRequestPresignedUrl(bucketName: String, key: String): String {
val generatePresignedUrlRequest: GeneratePresignedUrlRequest =
GeneratePresignedUrlRequest(bucketName, key)
.withMethod(com.amazonaws.HttpMethod.GET)
.withExpiration(Date(System.currentTimeMillis() + 1000 * 60 * 5))
return amazonS3Config.amazonS3().generatePresignedUrl(generatePresignedUrlRequest).toString()
}
해당코드는 bucketName과 key를 통해 해당 Object에 임시로 접근할 수 있는 Presigned url을 생성하는 코드다.
해당 url도 5분이 지나면 자동으로 만료되어 접근되지 않는다.
나름의 고민이었던 것!
약간의 고민이 있는데 Client가 이미지에 대해서 접근할 때 presigned url을 생성하는 것은 당연히 맞다고 생각한다. 직접적으로 url을 노출하는 것은 좋지 않기 때문에 Request Presigned url을 통해서 접근하는게 맞는데, 클라이언트에 노출되지 않는 단순히 외부 OCR에 노출되는 것인데 이것도 Presigned url을 사용해야하나? 생각해보게 되었다.
그런데 아무래도 보안적인 면에서 필요하다고 생각하고 접근 제어에 관해서도 어느정도 컨트롤 할 수 있기 때문에 맞다고 생각한다.
내부 네트워크 상에서는 문제가 되지 않지만 서버 밖으로 이런 중요한 도메인을 노출시키는 것은 옳지 않다고 생각한다!
따라서 사용하기로 결정했다!
'Backend' 카테고리의 다른 글
Soft Delete 테스트: @SQLDelete와 @Where의 효과적인 검증 방법 (0) | 2024.11.30 |
---|---|
MySQL vs PostgreSQL데이터베이스의 성능 및 확장성 비교 (4) | 2024.10.31 |
비동기 처리를 지원하는 모델(스레드 기반/이벤트 루프 기반) (0) | 2024.06.23 |
Kotlin+Spring Kinesis 비동기 처리 EFO(Coroutine) (0) | 2024.03.20 |
스트리밍 데이터(Data Stream) (0) | 2024.03.18 |