S3 File Upload PresignedUrl로 처리

xlwdn·2023년 1월 20일
0

기본 제가 작성한 서버에서는 클라이언트(FE)로부터 파일을 직접 받아서 s3에 업로드하는 방식을 사용했습니다. 하지만 이러한 방식에서는 많은 요청이 입력될 경우 부하가 발생할 수 밖에 없습니다. 때문에 이 문제를 해결하고자 기존 방식에서, FE가 BE를 거치지 않고 직접 S3에 파일을 업로드할 수 있도록 수정하였습니다.

추가적인 S3 upload Diagram 첨부합니다.

기존 S3upload 과정

PresignedURL upload 과정

위와 같이 처리함으로써 비정상적인 파일의 서버 업로드시 발생할 수 있는 위협 제거와 불필요한 네트워크 비용을 획기적으로 줄일 수 있습니다.

기존 UploadAttachment.kt

	@Async
    override fun uploadAttachment(request: GenerateFileRequest, noticeId: String) {
        val fileId = UUID.randomUUID().toString()
        
        val dto = uploadFilePort.upload(attachment, "NOTICE/${noticeId}", "ATTACHMENT/${fileId}")
        val attachment = Attachment(
            fileId,
            dto,
            AttachmentNotice(
                noticeId
            )
        )

        removeAttachmentPort.remove(noticeId)
        saveAttachmentPort.save(attachment)

    }

기존 S3Uploader.kt

@Component
class S3Uploader (
    private val s3Property: S3Property,
    private val s3: AmazonS3Client
): UploadFilePort {

    override fun upload(file: MultipartFile, rootPathName: String, middlePathName: String): FileDto {
        val objectMetadata = ObjectMetadata()
        val bytes: ByteArray = IOUtils.toByteArray(file.inputStream)

        objectMetadata.contentLength = bytes.size.toLong()
        val ext = (file.originalFilename?: file.name).substring((file.originalFilename?:file.name).lastIndexOf(".") + 1)

        var fileType: FileType = FileType.IMAGE
        ImageExt.values().filter { it.extension ==  ext }.map {
            objectMetadata.contentType = it.contentType
        }.ifEmpty {
            DocsExt.values().filter { it.extension == ext }.map {
                objectMetadata.contentType = it.contentType
                fileType = FileType.DOCS
            }
        }.ifEmpty {
            fileType = FileType.UNKNOWN
        }

        val byteArrayInputStream = ByteArrayInputStream(bytes)

        val fileName = "${s3Property.bucketName}/${rootPathName}/${middlePathName}/${file.originalFilename}"

        try {
            s3.putObject(PutObjectRequest(s3Property.bucketName, fileName, byteArrayInputStream, objectMetadata))
        } catch (err: Exception) {
            throw BusinessException(err.message, ErrorCode.BAD_GATEWAY_ERROR)
        }
        return FileDto(
            getFileUrl(fileName),
            fileType,
            ext,
            file.originalFilename.toString()
        )
    }

    fun getFileUrl(fileName: String): String {
        return s3.getResourceUrl(s3Property.bucketName, fileName)
    }

}

수정 후 UploadAttachment.kt

		@Async
    override fun uploadAttachment(request: GenerateFileRequest, noticeId: String) {
        val fileId = UUID.randomUUID().toString()
        
        val dto = uploadFilePort.getPresignedUrl(request.fileName, request.contentType, "NOTICE/${noticeId}", "ATTACHMENT/${fileId}")
        
        val attachment = Attachment(
            fileId,
            dto,
            AttachmentNotice(
                noticeId
            )
        )

        removeAttachmentPort.remove(noticeId)
        saveAttachmentPort.save(attachment)

    }

수정 후 S3Uploader.kt

@Component
class S3Uploader (
    private val s3Property: S3Property,
    private val s3: AmazonS3
): UploadFilePort {

    override fun getPresignedUrl(originalFileName: String, contentType: String, rootPathName: String, middlePathName: String): FileDto {
        val fileName = getFileName(rootPathName, middlePathName, originalFileName)
        val ext = getExt(originalFileName)

        val generatePresignedUrlRequest = getGeneratePreSignedUrlRequest("info-dsm", fileName)
        val url = s3.generatePresignedUrl(generatePresignedUrlRequest)
        return FileDto(
            url.toString(),
            getFileType(contentType, ext),
            ext,
            originalFileName
        )
    }

    private fun getExt(originalFileName: String): String {
        return originalFileName.substring(originalFileName.lastIndexOf(".") + 1)
    }

    private fun getFileType(type: String, ext: String): FileType {
        var contentType = type
        var fileType: FileType = FileType.IMAGE
        ImageExt.values().filter { it.extension ==  ext }.map {
            contentType = it.contentType
        }.ifEmpty {
            DocsExt.values().filter { it.extension == ext }.map {
                contentType = it.contentType
                fileType = FileType.DOCS
            }
        }.ifEmpty {
            fileType = FileType.UNKNOWN
        }
        return fileType
    }

    private fun getGeneratePreSignedUrlRequest(bucket: String, fileName: String): GeneratePresignedUrlRequest {
        val generatePresignedUrlRequest = GeneratePresignedUrlRequest(bucket, fileName)
            .withMethod(HttpMethod.PUT)
            .withExpiration(getPreSignedUrlExpiration())
        generatePresignedUrlRequest.addRequestParameter(
            Headers.S3_CANNED_ACL,
            CannedAccessControlList.PublicRead.toString()
        )
        return generatePresignedUrlRequest
    }

    private fun getPreSignedUrlExpiration(): Date {
        val expiration = Date()
        var expTimeMillis = expiration.time
        expTimeMillis += (1000 * 60 * 2).toLong()
        expiration.time = expTimeMillis
        return expiration
    }

    private fun getFileName(rootPathName: String, middlePathName: String, originalFileName: String): String {
        return "${s3Property.bucketName}/${rootPathName}/${middlePathName}/${originalFileName}"
    }

}

0개의 댓글