이 글은 Spring Boot를 공부하며 정리한 글입니다.
지금까지는 데이터베이스에 데이터를 생성, 조회, 삭제, 수정 등의 내용을 다루었습니다. 하지만, 사용자들은 데이터 뿐만 아니라 이미지 파일 혹은 비디오 파일 같은 파일을 생성, 조회, 삭제, 수정을 해야할 수 있습니다. 실제로 대부분의 어플리케이션은 이미지와 함께 글을 생성하는것이 많죠.
이미지같은 파일들은 데이터베이스에 저장할 수 없습니다. 데이터베이스가 아니라 스토리지라고 불리우는 저장장치에 저장하게 됩니다. 그렇다면 스토리지에 저장된 파일은 어떻게 조회할 수 있을까요? 보통은 리소스 Url을 이용합니다. 사용자가 서버에 파일 저장을 요청하면 서버는 해당 데이터를 입력받아 스토리지에 저장 후 url을 데이터베이스에 저장합니다. 그리고 사용자는 이미지 파일 조회 요청을 데이터베이스에서 서치한 후 사용자에게 반환하는 것이죠.
보통은 물리적인 스토리지가 필요하지만, 요새는 클라우드 서비스를 이용하면 간단하게 스토리지를 이용할 수 있습니다. 대표적으로 파이어베이스, AWS같은 것들이 있는데요. 오늘은 AWS에서 제공하는 Amazon S3에 파일을 업로드 하는 방법에 대해서 알아보겠습니다.
만약, S3가 뭔지 모르는 분들께서는 하단의 글을 읽어보시면 될 것 같습니다.
AWS S3를 이용한 파일 저장
AWS는 보통 IAM 계정을 이용해서 이용하는 것이 AWS에서 권장하는 방법입니다. 따라서, S3 접근을 위한 IAM 계정을 생성하겠습니다. 다만, 이 계정은 콘솔에 직접 로그인할 수는 없고 오직 Access Key, Secret Key를 통해서만 인증이 가능하도록 생성할 것입니다.
만약, IAM 계정 생성에 대한 내용이 궁금하다면 하단의 글을 읽어보시면 좋을 것 같습니다.
IAM 계정 생성하기저는 s3manager라는 IAM 계정을 생성하였습니다. 이 계정은 오직 AmazonS3FullAccess 권한만 갖고 있습니다. 따라서, 다른 AWS 리소스는 접근이 불가능합니다.
프로젝트를 위한 S3 버킷을 생성해 줍니다. 상단의 링크를 따라가면 S3 버킷 생성 가이드도 있습니다. 그리고 이 버킷에 이미지를 생성, 조회, 삭제는 s3manager 계정만 가능해야 합니다. 따라서, 아래와 같은 정책을 적용시켜줍시다.
{
"Version": "2012-10-17",
"Id": "Policy1720437774949",
"Statement": [
{
"Sid": "AccessAllowWrite",
"Effect": "Allow",
"Principal": {
"AWS": [
"[s3manager IAM 계정의 ARN]"
]
},
"Action": [
"s3:DeleteObject",
"s3:GetObject",
"s3:PutObject"
],
"Resource": "arn:aws:s3:::[생성 버킷 이름]/*"
},
{
"Sid": "AccessAllowRead",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::[생성 버킷 이름]/*"
}
]
}
여기서, s3manager와 무관하게 모든 사용자는 버킷의 리소스를 조회할 수 있어야 이미지를 보여줄 수 있습니다. 따라서, 모든 사용자에 대해 조회만 허용해줍니다.
s3manager를 생성하면 accessKey와 secretKey가 존재하지 않습니다. 보안자격 증명에서 액세스 키를 생성해주면 되고, 해당 키들은 모두 안전한 곳에다가 복사해두세요.
이제 Spring boot에서 Amazon S3를 이용하기 위한 의존성을 추가해주겠습니다.
dependencies {
...
implementation("org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE")
...
}
Spring boot에서 Amazon S3를 이용하기 위해서는 S3에 접근이 가능한 IAM 계정의 accessKey, secretKey, region, 버킷 이름이 필요합니다. 이는 Config파일에서 필요한 것이니 application.yml 파일에서 따로 관리하겠습니다.
cloud:
aws:
credentials:
access-key: [access key]
secret-key: [secret key]
stack:
auto: false
region:
static: [region]
s3:
bucket: [버킷 이름]
각 항목들에 알맞은 값들을 입력해주세요. 또한, 이러한 정보는 깃허브에 올리게 되면 악용될 수 있으니 환경변수 혹은 ignore 파일에 추가하는 것이 좋습니다.
amazonS3에 접근하기 위해서 AmazonS3Manager를 빈에 등록해주어야 합니다. 따라서, S3Config를 생성해주겠습니다.
import ...
@Configuration
class S3Config(
@Value("\${cloud.aws.credentials.access-key}")
private val accessKey : String,
@Value("\${cloud.aws.credentials.secret-key}")
private val secretKey : String,
@Value("\${cloud.aws.region.static}")
private val region: String,
) {
@Bean
fun amazonS3Client() : AmazonS3Client {
val awsCredentials = BasicAWSCredentials(accessKey, secretKey)
return AmazonS3ClientBuilder
.standard()
.withRegion(region)
.withCredentials(AWSStaticCredentialsProvider(awsCredentials))
.build() as AmazonS3Client
}
}
이제 S3사용을 위한 모든 준비가 완료되었습니다.
기본적으로 이미지같은 파일들을 업로드 하는 방식은 여러가지가 있지만 우리는 Mutliparfile 방식을 이용하겠습니다.
import ...
@Service
class S3Service(
private val amazonS3 : AmazonS3Client
) {
// 버킷 이름 명시
@Value("\${cloud.aws.s3.bucket}")
private lateinit var bucketName : String
// 이미지 생성 함수
fun uploadImage(image : MultipartFile) : String {
// 이미지가 없거나 파일 이름이 없다면 예외처리
if (image.isEmpty || image.originalFilename == null) {
throw AmazonS3Exception("파일이 비었습니다.")
}
// 파일 이름 추출
val fileName = image.originalFilename!!
// 업로드 파일 이름 생성
val s3FileName = UUID.randomUUID().toString().substring(1, 10) + fileName
// inputStream 생성
val inputStream = image.inputStream
// s3 업로드 메타데이터 생성
val metadata = ObjectMetadata().apply {
// 반드시 지정해줘야 이미지를 볼 수 있음.
this.contentType = image.contentType
this.contentLength = image.size
}
try {
// 버킷 하위에 image 디렉토리에 모두 저장
val putObjectRequest : PutObjectRequest =
PutObjectRequest(bucketName, "image/$s3FileName", inputStream, metadata)
.withCannedAcl(CannedAccessControlList.PublicRead)
amazonS3.putObject(putObjectRequest)
} catch (e: Exception) {
throw AmazonS3Exception("이미지 업로드 에러가 발생했습니다!")
} finally {
inputStream.close()
}
return amazonS3.getUrl(bucketName, s3FileName).toString()
}
}
입력받은 MultipartFile은 존재하지 않다면 업로드 에러가 발생합니다. 따라서, 미리 예외처리를 해줍니다. 그리고 파일이름을 이용해서 업로드 객체의 이름을 지정해주어야 하는데, 이름은 고유해야 합니다. 따라서, UUID를 이용해서 생성해 줍니다. 제일 중요한게 metadata인데, contentType이 제대로 지정되지 않으면 이미지를 볼 수 없고 계속 다운로드만 됩니다. 그러니 위에 처럼 입력받은 MultipartFile의 contentType을 지정해줍시다.