Spring Boot

[Spring boot] S3 이미지 업로드 및 예외 처리

Happy._. 2024. 6. 26. 16:32

build.gradle.kts 내 의존성 추가

implementation("io.awspring.cloud:spring-cloud-aws-starter-s3:3.1.1")

 

application.yml 설정

spring:
  cloud:
    aws:
      credentials:
        access-key: <access-key>
        secret-key: <secret-key>
      s3:
        bucket: <bucket-name>
      region:
        static: <bucket-region>
  servlet:
    multipart:
      max-file-size: 20MB # 개별 파일의 크기, FileSizeLimitExceededException
      max-request-size: 40MB # 전체 파일의 크기, SizeLimitExceededException
      resolve-lazily: true # 파일에 접근하는 시점에 파일 체크

 

S3 Service 클래스 작성

@Service
class S3Service(
    private val s3Operations: S3Operations,
    @Value("\${spring.cloud.aws.s3.bucket}")
    private val bucket: String,
) {

    @Transactional
    fun upload(file: MultipartFile, key: String): String {
        // 업로드할 이미지 확장자 목록 정의
        val imageTypes = listOf("jpg", "jpeg", "png", "gif", "bmp")

        // contentType의 확장자 부분만 추출(예: image/png -> png)해 미리 정의한 확장자와 일치하는지 확인
        if (!imageTypes.contains(file.contentType.toString().split("/")[1])) {
            throw IllegalArgumentException("이미지 파일만 업로드가 가능합니다.") // 일치하지 않을 경우 예외 발생
        }

        file.inputStream.use { it -> // use: 블록 내 코드 실행 후 예외 발생 여부에 관계없이 close
            return s3Operations.upload(
                // 버킷명, key(버킷 업로드 시 적용되는 파일명), 업로드할 파일(InputStream), 파일의 메타데이터(ObjectMetadata)
                bucket, key, it,
                ObjectMetadata.builder().contentType(file.contentType).build()
            ).url.toString() // 업로드된 URL을 반환
        }
    }
}

 

위 3개의 설정만으로 S3 이미지 업로드가 가능하다.

다음은 업로드를 요청하는 파일의 개별 크기 및 전체 크기에 대한 예외 처리 코드이다.

해당 부분은 ControllerAdvice로 처리가 되지만 실제 디버그 모드로 돌려보면 우리가 만드는 Controller 전에 예외가 Catch 된다.

디버그 모드로 확인 해보면 많은 클래스들을 거쳐서 우리가 작성한 GlobalExceptionHandler로 오는 것을 볼 수 있다.

 

GlobalExceptionHandler 클래스 작성

@RestControllerAdvice
class GlobalExceptionHandler {

    // ...

    @ExceptionHandler(FileSizeLimitExceededException::class)
    fun handleFileSizeLimitExceededException(e: FileSizeLimitExceededException): ResponseEntity<ErrorResponse> {
        return ResponseEntity.status(HttpStatus.BAD_REQUEST)
            .body(
                ErrorResponse(
                    e.message?.let {
                        CustomFileSizeLimitExceededException(
                            it, // message
                            e.actualSize, // 실제 파일 크기
                            e.permittedSize // 최대 허용 파일 크기
                        ).message
                    }
                )
            )
    }

    @ExceptionHandler(SizeLimitExceededException::class)
    fun handleSizeLimitExceededException(e: SizeLimitExceededException): ResponseEntity<ErrorResponse> {
        return ResponseEntity.status(HttpStatus.BAD_REQUEST)
            .body(
                ErrorResponse(
                    e.message?.let {
                        CustomSizeLimitExceededException(
                            it, // message
                            e.actualSize, // 실제 파일 크기
                            e.permittedSize // 최대 허용 파일 크기
                        ).message
                    }
                )
            )
    }
}

 

FileSizeLimitExceededException 클래스를 상속받는 CustomFileSizeLimitExceededException 클래스 작성

class CustomFileSizeLimitExceededException(
    message: String, actual: Long, permitted: Long
) :
    FileSizeLimitExceededException(message, actual, permitted) {

    override val message: String
        get() = "업로드 가능한 이미지는 최대 크기는 ${permittedSize / (1024 * 1024)}MB 입니다."
}

 

SizeLimitExceededException 클래스를 상속받는 CustomSizeLimitExceededException 클래스 작성

class CustomSizeLimitExceededException(
    message: String, actual: Long, permitted: Long
) : SizeLimitExceededException(message, actual, permitted) {

    override val message: String
        get() = "업로드 가능한 이미지의 전체 크기는 ${permittedSize / (1024 * 1024)}MB 입니다."
}

 

예외 처리하면서 시도했던 내용 기록

  • 하나의 파일(31.6MB)을 업로드 요청할 때
    • resolve-lazily 설정 X
    • @ExceptionHandler(FileSizeLimitExceededException::class)만 등록한 상태
      • @ExceptionHandler(FileSizeLimitExceededException::class)에서 처리됨
      • Response body: "업로드 가능한 이미지는 최대 크기는 20MB 입니다."
  • 두 개의 파일(31.6MB, 18.5MB)을 업로드 요청할 때
    • resolve-lazily 설정 X
    • @ExceptionHandler(FileSizeLimitExceededException::class)만 등록한 상태
      • 콘솔 출력 메시지: Resolved [org.springframework.web.multipart.MaxUploadSizeExceededException: Maximum upload size exceeded]
      • Response body: X
  • 두 개의 파일(31.6MB, 18.5MB)을 업로드 요청할 때
    • resolve-lazily 설정 X
    • @ExceptionHandler(FileSizeLimitExceededException::class)를 등록한 상태
    • @ExceptionHandler(SizeLimitExceededException::class)를 등록한 상태
      • Response body : "업로드 가능한 이미지의 전체 크기는 40MB 입니다."
  • 두 개의 파일(31.6MB, 18.5MB)을 업로드 요청할 때
    • resolve-lazily = true 설정
    • @ExceptionHandler(FileSizeLimitExceededException::class)를 등록한 상태
    • @ExceptionHandler(SizeLimitExceededException::class)는 주석 처리한 상태
      • Http Status Code 500
        - 콘솔 출력 메시지: org.apache.tomcat.util.http.fileupload.impl.SizeLimitExceededException: the request was rejected because its size (52628367) exceeds the configured maximum (41943040)

 

 

참고 자료