들어가기 전

이번 포스팅에서는 Spring Boot와 Kotlin, AWS S3를 이용하여 이미지를 업로드, 조회, 삭제하는 방법에 대해 알아보겠습니다.

 

 

AWS S3란?

S3(Simple Storage Service)는 AWS에서  제공하는 클라우드 스토리지 서비스입니다. S3는 파일, 데이터 및 다양한 유형의 미디어 등 정적인 파일을 저장하고 관리하는 데 사용되는 웹 기반 스토리지 시스템입니다.
저장하는 데이터 양에 대한 비용도 저렴하고, 많은 양에 데이터를 저장할 수 있습니다.

그리고 하나의 버킷에 여러 객체를 저장할 수 있습니다.

 

버킷과 객체

 

  • 버킷은 하나의 공간, 객체는 공간에 있는 사물이라고 이해하시면 됩니다.
  • 버킷 안에는 여러 형태의 객체를 저장할 수 있고 수정, 삭제할 수 있습니다.
  • 버킷의 이름은 중복되면 안 됩니다.
  • 객체의 하나의 크기는 1Byte ~ 5TB이고 객체별로 권한을 설정할 수 있습니다.

 

S3에 대해서 간단하게 알아보았습니다. 이제 S3를 생성하는 방법에 대해서 알아보겠습니다.

 

 

1. 검색창에 S3를 클릭하고 "S3"를 클릭합니다.

 

 

 

2. 왼쪽 사이드바에 있는 "버킷"을 클릭하고 "버킷 만들기"를 클릭합니다.

 

 

 

3.  버킷  이름에서  생성하여 사용할 버킷이름을 작성하고 "ACL 활성화"를 시킵니다. 만약 S3를 이용하여 민감한 정보를 사용할 경우 "ACL 비활성화"로 설정하는걸 추천드립니다. 필자는 단순히 누구에게나 노출돼도 되는 이미지를 다룰 거여서 활성화로 설정하였습니다.

 

 

 

4. "모든 퍼블릭 액세스 차단" 또한 위에서 설명한 거처럼 민감한 정보를 다루는 버킷이면 차단을 활성화하고 그렇지 않으면 비활성화로 설정을 합니다. 차단을 활성화하더라도 버킷에 객체를 저장할 수 있지만 조회를 할 때 Access Denied이 발생하여 접근을 할 수 없습니다.

 

 

5. 그 외 설정은 프로젝트에 맞게 설정한 뒤  "버킷 만들기"를 하시면 버킷을 만들 수 있습니다.

 

 

 

 

6.  객체에 접근하기 위해 버킷에 대한 정책을 설정해야 합니다. 만들어진 버킷 이름을 선택을 하고 "권한" -> "편집"을 클릭합니다. 그리고 "버킷 ARN" 값을 복사한 뒤 "정책 생성기"를 클릭합니다.

 

 

 

 

7.  "Select Type Of Policy"에는 "S3 Bucket Policy"를 설정을 하고 "Actions"에는 조회, 수정, 삭제 권한을 추가합니다. 그리고 "Amazon Resource Name"에는 앞에서 복사한 "ARN" 값을 붙여 넣기 한 뒤 "Add Statement"를  클릭하면 설정 한 값이 나옵니다. 맞게 설정이 되었다는 것을 확인하였다면 "Generate Policy"를 클릭하여 Json 형태로 생성된 정책을 확인할 수 있습니다.  해당 Json을 복사합니다.

 

 

 

 

8. 위에서 복사한 정책을 붙여 넣기 한 뒤 "변경 사항 저장"을 클릭하면 됩니다.

 

 

 

9. 검색창에 IAM을 검색하고 클릭을 합니다. 

 

 

 

10. 왼쪽 사이드바에 있는 "사용자"를 클릭하고 "사용자 생성"을 클릭합니다.

 

 

 

11.  사용할 "사용자 이름"을 작성하고 "다음"을 클릭합니다.

 

 

12. "직접 정책 연결"을 클릭하고 "권한 정책"에서 "AmazonS3 FullAccess"를 체크하고 다음을 클릭하고 사용자 생성을 클릭합니다.

 

 

13.  생성한 "IAM 사용자 이름"을 클릭하고 "액세스 키 만들기"를 클릭합니다.

 

 

 

14. 해당 설정은 글을 읽는 분들의 상황에 맞게 설정을 하시면 됩니다.

 

 

 

 

15. 비밀 액세스 키 표시를 클릭하여 "액세스 키""비밀 액세스 키값"을 복사합니다.

 

 

지금까지 S3 버킷 생성 및 정책, 권한을 설정하는 방법에 대해서 알아보았습니다. 이제 Spring Boot에 연동하는 방법과 저장, 조회, 삭제하는 방법에 대해서 알아보겠습니다.

 

 

build.gradle.kt

//aws s3
implementation("org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE")

 

 

application.yml

cloud:
  aws:
    s3:
      bucket: 버킷 이름
    region:
      static: 버킷 지역
    stack:
      auto: false
    credentials:
      access-key: IAM 액세스 키
      secret-key: IAM 비밀 액세스 키

 

 

S3 Config

 

@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
    }
}

 

 

FileValidate

 

class FileValidate {

    companion object {
        private val IMAGE_EXTENSIONS: List<String> = listOf("jpg", "png", "gif", "webp")
        fun checkImageFormat(fileName: String) {
            val extensionIndex = fileName.lastIndexOf('.')
            if(extensionIndex == -1) {
                throw CustomExcpetion(ExceptionCode.NOT_EXSITS_FILE_EXTENSION)
            }
            val extension = fileName.substring(extensionIndex + 1)
            require(IMAGE_EXTENSIONS.contains(extension)) {
                throw CustomExcpetion(ExceptionCode.NOT_SUPPORT_FILE_EXTENSION)
            }
        }
    }
}

 

  •  필자는 이미지 확장자는 jpg, png, gif, webp 형식만 프로젝트에서 사용할 거라 그 외는 다 예외처리 하였습니다.

 

S3 FileManagement

 

@Component
class S3FileManagement(
    @Value("\${cloud.aws.s3.bucket}")
    private val bucket: String,
    private val amazonS3: AmazonS3,
) {
    companion object {
        const val TYPE_IMAGE = "image"
    }
    fun uploadImage(multipartFile: MultipartFile): String {
        val originalFilename = multipartFile.originalFilename
            ?: throw CustomExcpetion(ExceptionCode.WRONG_FORMAT_FILE_NAME)
        FileValidate.checkImageFormat(originalFilename)
        val fileName = "${UUID.randomUUID()}-${originalFilename}"
        val objectMetadata = setFileDateOption(
            type = TYPE_IMAGE,
            file = getFileExtension(originalFilename),
            multipartFile = multipartFile
        )
        amazonS3.putObject(bucket, fileName, multipartFile.inputStream, objectMetadata)
        return fileName
    }

    fun getFile(fileName: String): String {
        return amazonS3.getUrl(bucket,fileName).toString()
    }

    fun delete(fileName: String) {
        amazonS3.deleteObject(bucket,fileName)
    }

    private fun getFileExtension(fileName: String): String {
        val extensionIndex = fileName.lastIndexOf('.')
        return fileName.substring(extensionIndex + 1)
    }

    private fun setFileDateOption(
        type: String,
        file: String,
        multipartFile: MultipartFile
    ): ObjectMetadata {
        val objectMetadata = ObjectMetadata()
        objectMetadata.contentType = "/${type}/${getFileExtension(file)}"
        objectMetadata.contentLength = multipartFile.inputStream.available().toLong()
        return objectMetadata
    }
}

 

 

   fun uploadImage(multipartFile: MultipartFile): String {
        val originalFilename = multipartFile.originalFilename
            ?: throw CustomeException(ExceptionCode.WRONG_FORMAT_FILE_NAME)
        FileValidate.checkImageFormat(originalFilename)
        val fileName = "${UUID.randomUUID()}-${originalFilename}"
        val objectMetadata = setFileDateOption(
            type = TYPE_IMAGE,
            file = getFileExtension(originalFilename),
            multipartFile = multipartFile
        )
        amazonS3.putObject(bucket, fileName, multipartFile.inputStream, objectMetadata)
        return fileName
    }

 

  • MutlipartFile을 S3에 업로드하는 메서드입니다.
  •  amazonS3.putObject(bucket, fileName, multipartFile.inputStream, objectMetadata) : S3 버킷에 객체를 저장하는 로직입니다.

 

fun getFile(fileName: String): String {
    return amazonS3.getUrl(bucket,fileName).toString()
}

 

  • S3에 저장된 파일을 버킷과 파일 이름으로 조회하는 로직입니다.

 

fun delete(fileName: String) {
    amazonS3.deleteObject(bucket,fileName)
}

 

  • 버킷 이름과 파일명으로 S3 버킷에 저장된 객체를 삭제하는 로직입니다.

 

Images

@Entity
class Images(
    var image: String,
)

 

 

Controller

@Api(tags = ["이미지"])
@RequestMapping("/api/image")
@RestController
class ImageController(
    private val imageWriteService: ImageWriteService,
    private val imageReadService: ImageReadService,
) {

    @ApiOperation(value = "이미지 등록" )
    @PostMapping
    fun create(
        @RequestPart image: MultipartFile
    ) {
        imageWriteService.create(image = image)
    }

    @ApiOperation(value = "이미지 조회")
    @GetMapping
    fun get(): String {
        return imageReadService.get()
    }

    @ApiOperation(value = "이미지 삭제")
    @DeleteMapping("/{imageId}")
    fun delete(@PathVariable(value = "imageId") imageId: Long) {
        imageWriteService.delete(imageId)
    }
 }

 

 

Service

 

@Transactional
@Service
class ImageWriteService(
    private val s3FileManagement: S3FileManagement,
    private val imageRepository: ImageRepository,
) {
    fun create(
        image: MultipartFile
    ) {
        val uploadImage = s3FileManagement.uploadImage(image)
        imageRepository.save(
            Image(
                image = uploadImage
            )
        )
    }

    fun delete(imageId: Long) {
        val file = imageRepository.findByIdOrNull(imageId)
            ?: throw CustomException(ExceptionCode.NOT_EXSITS_IMAGE)
        s3FileManagement.delete(file.image)
    }
}


@Transactional(readOnly = true)
@Service
class ImageReadService(
    private val s3FileManagement: S3FileManagement,
    private val imageRepository: ImageRepository,
) {

    fun get(): String {
        return s3FileManagement.getFile()
    }
}

 

 

이미지 등록

 

 fun create(
        image: MultipartFile
    ) {
        val uploadImage = s3FileManagement.uploadImage(image)
        imageRepository.save(
            Image(
                image = uploadImage
            )
        )
    }

 

 

 

  • form-data 형식으로 요청을 합니다.

 

 

  • S3 버킷에 UUID + 파일명 형식으로 저장이 됩니다.

 

이미지 조회

 

fun get(): String {
    return s3FileManagement.getFile()
}

 

응답된 이미지 경로를 브라우저에 복사/붙여 넣기 하면 이미지가 다운로드됩니다.

 

 

 

이미지 삭제

fun delete(imageId: Long) {
    val file = imageRepository.findByIdOrNull(imageId)
        ?: throw CustomException(ExceptionCode.NOT_EXSITS_IMAGE)
    s3FileManagement.delete(file.image)
}