기록

이미지 처리 및 저장 방법 : S3 업로드/조회/삭제 본문

Web/Spring

이미지 처리 및 저장 방법 : S3 업로드/조회/삭제

youngyin 2024. 2. 12. 12:00

들어가면서

토이 프로젝트를 진행하면서 이미지를 효과적으로 관리하기 위해 로컬 저장소 대신 AWS S3를 활용하려고 한다. 이에 따라 이미지의 안전한 업로드, 조회, 삭제 방법을 찾아보고 적용한 내용을 공유하고자 한다.

(1) 이전 글

2024.01.29 - [Web/Spring] - API/이미지 처리 및 저장 방법

2024.02.05 - [Web/Spring] - 이미지 처리 및 저장 방법 : 로컬 저장소 업로드/조회/삭제

(2) 프로젝트 구성
  • Java Version: 17
  • Build Tool: Gradle
  • Kotlin
  • Spring Boot
  • Swagger

(3) 패키지 구성

아래에서는 S3Service (AWS S3에 이미지 저장)하는 부분을 다루고자 한다.

본문

AWS S3 설정

AWS S3 버킷 생성하기

먼저 AWS Management Console에 로그인하여 S3 서비스로 이동한다. "버킷 만들기"를 선택하고 원하는 버킷 이름을 입력한다. 버킷 이름은 전 세계적으로 고유해야 한다. 

AWS IAM 사용자 및 권한 설정하기

AWS Management Console에서 IAM 서비스로 이동한다. "사용자 추가"를 선택하여 새로운 IAM 사용자를 생성한다.생성한 사용자에게 S3 접근 권한을 부여하기 위해 "권한 연결" 단계에서 "AmazonS3FullAccess" 권한을 추가한다. 액세스 키와 비밀 액세스 키를 안전하게 보관한다.

접근 권한 및 버킷 정책 설정하기

생성한 버킷을 선택한 후, "권한" 탭으로 이동한다. "Bucket Policy"를 선택하고 적절한 정책을 추가한다. 

Spring Boot에서 AWS S3 사용

Spring Boot 프로젝트 의존성 추가

프로젝트의 build.gradle에 AWS S3를 사용하기 위한 의존성을 추가

implementation platform('software.amazon.awssdk:bom')
implementation 'software.amazon.awssdk:s3'

application.yml에 AWS S3 설정 추가

aws:
  accessKeyId: IAM_ACCESS_KEY
  secretKey: IAM_SECRET_KEY
  region: us-east-1
  s3:
    bucket-name: your-bucket-name

S3 Configuration 클래스 생성

AWS S3를 사용하기 위한 Configuration 클래스를 생성

import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider
import software.amazon.awssdk.regions.Region
import software.amazon.awssdk.services.s3.S3Client

@Configuration
class S3Config {
    @Value("\${aws.accessKeyId}")
    private val accessKey: String? = null

    @Value("\${aws.secretKey}")
    private val secretKey: String? = null

    @Value("\${aws.region}")
    private val region: String? = null

    @Bean
    fun amazonS3Client(): S3Client? {
        val awsCredentials = AwsBasicCredentials.create(accessKey, secretKey)
        return S3Client.builder()
            .region(Region.of(region))
            .credentialsProvider(StaticCredentialsProvider.create(awsCredentials))
            .build()
    }
}

S3Service 저장/조회/삭제

1. AWS S3에 이미지 업로드하기

(1) S3Service.uploadImage

class ImgS3ServiceImpl(private val s3Client: S3Client) : ImgStorageService {
    @Value("\${cloud.bucket.name}")
    private val bucketName: String? = null

    override fun uploadImage(fileName: String, imageReq: ImageReq) {
        try {
            val file = imageReq.file
            val putObjectRequest = PutObjectRequest.builder()
                .bucket(bucketName)
                .key("$fileName")
                .contentType(file.contentType)
                .build()

            s3Client.putObject(putObjectRequest, fromBytes(file.bytes))
        } catch (e: IOException) {
            // 처리 중 발생한 예외 처리
            throw e
        }
    }
}

 

(2) S3에서 저장된 이미지 확인

 

2. AWS S3에서 이미지 조회하기

(1) S3Service.downloadImage

class ImgS3ServiceImpl(private val s3Client: S3Client) : ImgStorageService {
    @Value("\${cloud.bucket.name}")
    private val bucketName: String? = null

    override fun downloadImage(fileName: String): ByteArray {
        val getObjectRequest = GetObjectRequest.builder()
            .bucket(bucketName)
            .key(fileName)
            .build()

        try {
            val responseBytes: ResponseBytes<*> = s3Client.getObjectAsBytes(getObjectRequest)
            return responseBytes.asByteArray()
        } catch (e: SdkException) {
            // S3에서 이미지를 찾을 수 없는 경우
            e.printStackTrace()
            throw FileNotFoundException("File not found in S3: $fileName")
        } catch (e: IOException) {
            // 처리 중 발생한 예외 처리
            throw e
        }
    }
}

(2) 결과값 확인

이전글에서 작성한 조회 엔드포인트를 통해 이미지를 다운받을 수 있다. 

3. AWS S3에서 이미지 삭제하기

(1) S3Service.deleteImage

class ImgS3ServiceImpl(private val s3Client: S3Client) : ImgStorageService {
    @Value("\${cloud.bucket.name}")
    private val bucketName: String? = null

    override fun deleteImage(fileName: String) {
        val deleteObjectRequest = DeleteObjectRequest.builder()
            .bucket(bucketName)
            .key(fileName)
            .build()

        try {
            s3Client.deleteObject(deleteObjectRequest)
        } catch (e: SdkException) {
            // S3에서 이미지를 삭제할 수 없는 경우
            e.printStackTrace()
            throw IOException("Error deleting file from S3: $fileName")
        }
    }
}

(2) S3에 삭제된 이미지 확인

이미지 삭제 소스를 실행한 뒤에 S3 버킷에 접속해보면 해당 이미지가 제거된 것을 확인할 수 있다.

마무리

민감 데이터 처리

IAM에 대한 사용자 정보나 버킷명 등은 외부에 노출하면 안되는 값이다. 이를 위해 .gitignore에 등록하여 git으로 관리되지 못하게 하고 있지만 이 정보가 외부로 노출된 확률은 완전히 사라진 것은 아니다. .jar가 외부에 노출되는 경우에도 설정 정보가 외부로 노출될 수 있다. 그래서 민감한 정보를 암호화 한 뒤에 저장/처리하여 외부로 노출되더라도 보안에 문제가 없도록 구성해야 한다. 이를 위해 jasypt를 사용하여 암호화를 진행하려고 한다.

S3 가격 정책

2024년 1월 기준으로 S3 요금 부과 정책을 고려하였다. 이전 글에서 이미지를 저장하는 프로세스를 참고하면, 사용자가 EC2에 있는 서비스에 접근하고, EC2에 있는 서비스가 S3에 접근하면서 두 번의 데이터 전송이 이루어진다.

확인해보니 같은 지역에 있는 인스턴스로 S3가 데이터 전송은 요금 부과시에 제외된다고 하였다. 그래서 사용자에게 직접 S3의 리소스 위치를 노출하지 않고, 서비스 내에 엔드포인트를 만들어서 위치 정보를 감추도록 설계할 수 있었다.

이미지 데이터 용량 제한

S3 요금을 산정하는데 데이터 이미지 용량도 포함된다. 또한 이미지 용량이 너무 크면 서버에 부하가 있을 수 있다. 이를 위해서 업로드/다운로드 할수 있는 이미지의 크기를 지정해야 한다. 이후 포스팅에서는 이미지 크기를 제한하고 그 예외처리에 대해 정리해보겠다. 

 

Comments