[위드마켓 개발기] Amazon S3를 이용해서 이미지 업로드 서버를 개발해보자

Doccimann·2022년 5월 24일
0

위드마켓 개발기

목록 보기
5/10
post-thumbnail

🌈 본격적인 작성 전에

글을 본격적으로 작성하기 전에, 이번 포스트에서 이미지 업로드 서버 를 지금 시점에서 구성하게된 이유를 말씀드려보려고 합니다.

이전 포스트에서 가게 정보를 노출하는 서버를 구성했던 기억이 납니다. 그런데, 가게 정보를 노출할 때는, 가게와 관련된 이미지들(리뷰 이미지, 가게를 대표하는 이미지)을 제어할 수 있어야합니다. 따라서, 위드마켓에서도 이미지를 업로드를 하고, url을 관리해주는 서버를 구성해야할 필요성을 느꼈습니다.

위드마켓의 이미지 서버는 Amazon S3에 이미지를 저장하고, url을 뽑아오는 방식으로 구현하겠습니다.

🚨 주의 할 점!
저는 미리 Amazon S3 버킷을 생성해두었고, 퍼블릭 권한을 열어둔 상태이며, Amazon S3에 대한 IAM User를 미리 생성해둔 상태입니다. 따라서, 따라하실 분은 미리 저런 S3를 만들어두시고 시작하셔야합니다!
물론 config server 기반이라서 따라해도 오류가 터지겠지만....


🔥 우선, yml 파일을 config server에 등록하겠습니다.

yml 파일들을 등록하는 repository에다가 storage-s3.yml을 작성하겠습니다.

🔨 storage-s3.yml

cloud:
  aws:
    s3:
      bucket: "{cipher}encrypted bucket name"
    credentials:
      access-key: "{cipher}encrypted access key"
      secret-key: "{cipher}encrypted secret key"
    region:
      static: "{cipher}encrypted region"

위와 같이 storage-s3.yml에 암호화된 각 정보들을 넣고, 이 yml 파일을 config server로부터 가져와서 사용하는 방식으로 구현하도록 하겠습니다.


🔥 AwsS3Config.kt를 작성하겠습니다.

우선, build.gradle.kts에 아래의 의존성 2개를 추가하도록 하겠습니다.

// config server로부터 정보를 가져오기 위해서 추가하는 의존성
implementation("org.springframework.cloud:spring-cloud-starter-config")
// aws에서 제공하는 라이브러리를 사용하기위한 의존성
implementation("org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE")

다음으로, local, server 환경에서 사용할 환경을 분리시키기 위해서 yml 파일 3개를 추가하겠습니다.

🔨 application-locals3.yml

spring:
  config:
    activate:
      on-profile: locals3
    import: "optional:configserver:http://localhost:9500/"
  cloud:
    config:
      name: storage
      profile: s3

encrypt:
  key: encrypt key

🔨 application-servers3.yml

spring:
  config:
    activate:
      on-profile: servers3
    import: "optional:configserver:http://[my config server's IP]:9500/"
  cloud:
    config:
      name: storage
      profile: s3

encrypt:
  key: encrypted key

🔨 application.yml

spring:
  profiles:
    active: locals3

cloud:
  aws:
    stack:
      auto: false

우선 저는 개발환경을 활성화 시켜둔 상태입니다. 서버 환경을 활성화 시키려면 active를 servers3로 변경하시면 되겠습니다!

다음으로, AwsS3Config.kt를 작성하겠습니다.

🔨 AwsS3Config.kt

@Configuration
class AwsS3Config(
    @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 amazonS3(): AmazonS3 = AmazonS3ClientBuilder.standard()
        .withRegion(region)
        .withCredentials(
            AWSStaticCredentialsProvider(
                BasicAWSCredentials(accessKey, secretKey)
            )
        )
        .build()
}

위와 같은 방식으로 S3 설정 정보를 이용해서 amazonS3 객체를 Bean으로 등록시킵니다.


🔥 Service logic을 구현하겠습니다.

일단 코드부터 보겠습니다.

🔨 AwsS3Service.kt

@Service
class AwsS3Service(
    private val amazonS3: AmazonS3,
    @Value("\${cloud.aws.s3.bucket}")
    private val bucket: String
) {

    fun uploadImages(multipartFiles: List<MultipartFile>): List<String> {
        val fileNameList = mutableListOf<String>()

        multipartFiles.forEach { file ->
            val fileName: String = createRandomFileName(file.originalFilename!!)
            val objectMetadata = ObjectMetadata()
            with(objectMetadata) {
                this.contentLength = file.size
                this.contentType = file.contentType
            }

            putS3(file, fileName, objectMetadata)

            fileNameList.add(fileName)
        }

        return fileNameList
    }

    fun getImageUrl(fileName: String): String {
        if(amazonS3.doesObjectExist(bucket, fileName))
            return "https://$bucket.s3.ap-northeast-2.amazonaws.com/$fileName"
        else
            throw AmazonS3Exception("file $fileName does not exist!")
    }

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

    private fun createRandomFileName(fileName: String): String = UUID.randomUUID().toString() + getFileExtension(fileName)

    private fun getFileExtension(fileName: String): String {
        try {
            return fileName.substring(fileName.lastIndexOf("."))
        } catch (e: StringIndexOutOfBoundsException) {
            throw ResponseStatusException(HttpStatus.BAD_REQUEST, "잘못된 형식의 파일($fileName) 입니다.")
        }
    }

    private fun putS3(file: MultipartFile, fileName: String, objectMetadata: ObjectMetadata): Unit {
        try {
            val inputStream = file.inputStream

            amazonS3.putObject(PutObjectRequest(bucket, fileName, inputStream, objectMetadata)
                .withCannedAcl(CannedAccessControlList.PublicRead))
        } catch (e: IOException) {
            throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "이미지 업로드에 실패했습니다.")
        }
    }
}

service logic의 핵심은 아래의 3가지입니다.

  • 이미지를 업로드한다.
  • 이미지의 url을 제공한다
  • 이미지의 fileName을 이용해서 스토리지로부터 파일을 삭제한다

일단, 메소드를 하나하나 설명드리겠습니다.

fun uploadImages(multipartFiles: List<MultipartFile>): List<String>

이 메소드는 multipartFile 타입으로 들어온 이미지 파일들을 S3 상에 올린 다음, 파일 이름들을 모두 반환시키는 메소드입니다.

실제로는 이 메소드에서 S3에 파일을 올리는 역할을 하는 것은 putS3 라는 메소드입니다.

putS3 에서는 아래의 코드를 통해서 아마존 S3에 파일을 업로드합니다.

amazonS3.putObject(PutObjectRequest(bucket, fileName, inputStream, objectMetadata)
                .withCannedAcl(CannedAccessControlList.PublicRead))

만일 이 코드를 통해서 업로드가 실패하면, IOException을 뱉어서 예외 처리를 수행합니다. 나중에 ExceptionHandler 붙여서 꼼꼼하게 예외처리 해야하는건 안비밀

다음으로, url을 뽑아오는 메소드를 소개하겠습니다.

fun getImageUrl(fileName: String): String

이 메소드는 매우 간단합니다. fileName으로 전달된 이름에 대응되는 이미지 파일이 S3에 존재하지 않는다면 예외를 발생시키고, 존재하는 경우에는 해당 이미지의 url을 반환하는 기능이 끝입니다.

이미지를 삭제시키는 메소드는 매우 간단하니 설명을 생략하겠습니다.


🔥 Controller를 작성합시다.

controller는 매우 간단합니다.

🔨 AwsS3Controller.kt

@RestController
@RequestMapping("/v1/s3")
class AwsS3Controller(
    private val awsS3Service: AwsS3Service
) {

    @GetMapping("/image-url")
    fun getImageUrl(@RequestParam(name = "name") fileName: String) = awsS3Service.getImageUrl(fileName)

    @PostMapping("/image-upload")
    fun uploadImage(@RequestPart multipartFile: List<MultipartFile>): ResponseEntity<List<String>> {
        return ResponseEntity.ok(awsS3Service.uploadImages(multipartFile))
    }

    @DeleteMapping("/image-delete")
    fun deleteImage(@RequestParam(name = "name") fileName: String): ResponseEntity<String> {
        awsS3Service.deleteImage(fileName)

        return ResponseEntity.ok("Success to delete $fileName")
    }
}

위의 코드에 대한 자세한 설명은 생략하겠습니다. 대신에 Insomnia를 이용해서 실제로 작동하는지 테스트를 해보겠습니다.

아래의 치타 이미지를 업로드 해보도록 하겠습니다.

아래와 같이, 이미지에는 multipartFile 이라는 request 이름으로 붙여서 보내보겠습니다.

성공한 모습을 확인할 수 있고, 실제로도 s3에 올라가있는지 체크를 해보겠습니다.

잘 올라갔습니다!


🌲 글을 마치며

다음 포스트에서는, 이전에 작성하고있었던 가게정보 노출 서버를 더 작성해보거나, 혹은 이미지 서버로부터 이벤트를 받아내기 위해서 Kafka에 대해서 공부하고, 공부한 과정들에서 얻은 정보들을 공유하게 될 것 같습니다.

다음 포스트에서 뵙겠습니다. 감사합니다!


🌲 References

AWS S3 이미지 업로드

AWS S3에 여러 파일 업로드 및 삭제

profile
Hi There 🤗! I'm college student majoring Mathematics, and double majoring CSE. I'm just enjoying studying about good architectures of back-end system(applications) and how to operate the servers efficiently! 🔥

0개의 댓글