기본 제가 작성한 서버에서는 클라이언트(FE)로부터 파일을 직접 받아서 s3에 업로드하는 방식을 사용했습니다. 하지만 이러한 방식에서는 많은 요청이 입력될 경우 부하가 발생할 수 밖에 없습니다. 때문에 이 문제를 해결하고자 기존 방식에서, FE가 BE를 거치지 않고 직접 S3에 파일을 업로드할 수 있도록 수정하였습니다.
추가적인 S3 upload Diagram 첨부합니다.
위와 같이 처리함으로써 비정상적인 파일의 서버 업로드시 발생할 수 있는 위협 제거와 불필요한 네트워크 비용을 획기적으로 줄일 수 있습니다.
@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)
}
@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)
}
}
@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)
}
@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}"
}
}