SpringBoot Async를 이용한 multipartfile처리

xlwdn·2022년 11월 29일
0

문제 발생


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

}

0개의 댓글