SpringBoot

SpringBoot Async를 이용한 multipartfile처리

xlwdn98767 2023. 6. 10. 00:37
728x90

문제 발생


@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 모듈을 제작하여 추후 여러 부분에서 공통으로 사용할 수 있도록 처리하였습니다.

FileConvert.kt

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()}")

    }

}

Usage


@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) {
        ...
    }

}