@RestController
class Controller(
private val imageService: ImageService
) {
@PutMapping("/image")
fun addImage(@RequestPart image: MultipartFile) {
imageService.upload(image)
}
}
interface ImageService {
fun upload(image: MultipartFile)
}
@Serice
class ImageServiceImpl: ImageService {
@Async
override fun upload(image: MultipartFile) {
...
}
}
위와 같이 Async 어노테이션을 통해 비동기 함수를 통해 multipartfile 처리 시 아래 오류가 발생합니다.
java.nio.file.NoSuchFileException: /var/folders/mr/6qhlv4b50k56nfskt9_jqtd00000gn/T/undertow.8080.4380841185194887786/undertow15885674167083388220upload
Controller를 통해 multipartfile 입력 시 웹서버 내부에 잠시 저장됩니다. 하지만, 해당 공간은 thread 별로 할당되므로 비동기 함수로 처리 시 다른 thread가 처리하게 되면 참조하지 못하게 되어 oSuchFileException이 발생합니다.
multipartfile을 로컬에 저장한 이후, 함수 종료 시 삭제하도록 하였습니다.
우선 multipartfile interface의 구현체로 CommonsMultipartFile을 채택하여 최소한의 조건으로 multipartfile을 구현하였습니다.
implementation("commons-fileupload:commons-fileupload:1.4")
이후, static한 FileConvert 모듈을 제작하여 추후 여러 부분에서 공통으로 사용할 수 있도록 처리하였습니다.
object FileConvert {
fun fileToMultipartFileConvert(localFile: File): MultipartFile {
val fileItem: FileItem = DiskFileItem(
localFile.name,
Files.probeContentType(localFile.toPath()),
false,
localFile.name,
localFile.length().toInt(),
localFile.parentFile
)
try {
IOUtils.copy(FileInputStream(localFile), fileItem.outputStream);
} catch (ex: IOException) {
throw BusinessException("파일을 처리하던 중 오류가 발생했습니다.", ErrorCode.BAD_GATEWAY_ERROR)
}
return CommonsMultipartFile(fileItem)
}
fun multipartFileToFileConvert(file: MultipartFile, path: String): File {
val localFile: File = File(
Paths.get(path + file.originalFilename).toAbsolutePath().toString()
)
localFile.createNewFile()
file.transferTo(localFile)
println("CONVERT: ${localFile.absolutePath}")
return localFile
}
fun removeLocalFile(path: String) {
val localFile: File = File(
Paths.get(path).toAbsolutePath().toString()
)
println("REMOVE : ${localFile.absolutePath} / STATUS: ${localFile.delete()}")
}
}
@RestController
class ExampleController(
private val uploadFileUsecase: UploadFileUsecase
) {
...
@PutMapping("/example")
fun uploadFile(
@RequestPart(value = "file") file: MultipartFile
) {
uploadFileUsecase.uploadFile(
FileConvert.fileToMultipartFileConvert(
FileConvert.multipartFileToFileConvert(file, "file/src/main/resources/tmp/")
)
)
FileConvert.removeLocalFile("$IMAGE_PATH${file.originalFilename}")
}
...
}
interface UploadFileUsecase {
fun uploadFile(file: MultipartFile)
}
@Service
class UploadFile(
private val uploadFilePort: UploadFilePort
) {
@Async //비동기 작동 선언!!
override fun uploadFile(file: MultipartFile) {
...
}
}