업데이트:

업무를 하던 도중 파일 업로드 과정에서 java.nio.file.NoSuchFileException이 발생했고, 일반적인 원인이 아니어서 이 원인과 해결방법을 포스팅해보겠습니다. (저희는 참고로 Linux 환경과 Undertow를 사용하고 있습니다.)

NoSuchFileException?


NoSuchFileExceptionException 명에서 알 수 있듯이 해당 파일을 못찾았을때 발생하는 Exception입니다.

파일 뿐만 아니라 디렉토리가 존재하지 않을때도 발생합니다.

원인 확인


하지만 이상했습니다. Exception이 발생한 프로세스는 서버내에 파일을 업로드 하는 것이 아니고 AWS S3에 업로드 하는 것이었기 때문에 NoSuchFileException이 발생할 일이 없었기 때문입니다. 그래서 해당 Exception의 메시지를 구글링해보았습니다.

Request processing failed; nested exception is org.springframework.web.multipart.MultipartException: 
Failed to parse multipart servlet request; nested exception is java.lang.RuntimeException: java.nio.file.NoSuchFileException:

구글링 해본 결과 다음 링크에서 해답을 찾을 수 있었습니다.

링크로 이동하여 2. Cause investigation의 한 부분을 보시면 다음과 같은 문구가 있습니다.

Originally, when launching the spring boot application through java - jar in the linux operating system, 
a temporary directory will be created by default in the /tmp directory 
...
However, if the files in the /tmp directory are not used for more than 10 days, 
they will be automatically cleaned up by the system.

Linux 환경에 Spring Boot 어플리케이션을 구동할 때는 default로 /tmp 폴더 안에 임시 폴더를 구성하게 되는데, undertow 포맷 형태로 하위 폴더를 구성하게 됩니다.

그런데, /tmp 하위 디렉토리에서 10일 이상 사용하지 않는다면 System이 자동으로 클린징을 진행합니다.

❓Temporary Directory 파일 업로드 시 임시 파일 경로에 임시 파일을 저장해놓고 이 임시 파일을 다시 읽어서 작업을 진행하는 형태를 취합니다.

따라서 이 클린징에 의해 경로가 제거되어 NoSuchFileException이 발생한 것이었습니다.

로컬 확인

실제로 임시 파일 경로가 생성되는지와 임시 파일이 존재하는지 로컬에서 확인해보았습니다.

먼저 Controller에서 Break-point를 찍어 파일에 대해 확인해본 결과는 다음과 같았습니다. (참고로 제 로컬은 Mac이어서 /tmp 경로가 아닌 다음 경로에 생성되었습니다.)

file-path

그리고 실제로 위의 경로가 생성됐는지와 임시 파일 존재 유무를 확인해보았습니다.

directory

위와 같이 경로와 임시 파일이 생성된 것을 확인할 수 있었습니다.

문제 해결 방법

링크에선 다음과 같이 총 3가지 해결 방법을 제시하고 있습니다.

수동 경로 생성

💡Manually create the temporary directory (not recommended)

mkdir -p /tmp/undertow.8099.1610110889708131476/undertow1171565914461190366upload

Linux 시스템 구성을 변경

💡Modify Linux system configuration (not recommended)

vim /usr/lib/tmpfiles.d/tmp.con
# Add at the end of the file, which means that the folder at the beginning of undertow will not be cleaned up
x /tmp/undertow*

Spring Boot Config 변경

💡Modify spring boot configuration file (recommended)

spring:
  servlet:
    multipart:
      # Specify a custom upload directory
      location: /mnt/tmp

솔루션에서 추천하듯이 저 역시 위의 2가지 방법처럼 System쪽에서 문제를 해결하는 것이 아니라 Application에서 해결하기 위해 Spring Boot Config를 수정하는 방법으로 선택했습니다.

💡또 다른 방식으로는 Application을 다시 구동하는 방식이 있습니다. Application이 구동될때 임시 파일 경로를 생성하기 때문인데요. 매번 NoSuchFileException이 일어날 때 마다 Application을 새로 구동한다는 것은 얼토당토하지 않는 얘기이기 때문에 바로 무시했습니다.

하지만, 추가 고민거리가 있었습니다. 특정 경로로 지정한다 하더라도 누군가의 실수로 경로가 지워진다면 결국 같은 에러가 다시 발생할 것

밑에서 참고 자료로 다시 말씀 드리겠지만 Tomcat의 경우는 임시 파일 경로가 없을 경우 새로 생성되도록 하는 방법으로 처리 되어있지만, Undertow는 아직 그런 로직이 존재하지 않는다고 합니다. (실제 밑에 코드를 보시면 그렇게 되어있습니다.)

따라서 저는 좀 더 Exception 로그를 좀 더 분석해보기로 했습니다.

추가 로그 분석


MultiPartParserDefinition

첫 번째 캡처를 보시면 io.undertow.server.handlers.form.MultiPartParserDefinition에 관한 로그가 남겨져 있는걸 알 수 있고,

beginPart

두 번째 캡처를 보시면 임시 파일을 생성하는 것을 확인할 수 있습니다. (tempFileLocationapplication.yml에 정의한 spring.servlet.multipart.location을 뜻합니다.)

dispatcherServlet

그리고 해당 메소드를 호출하기까지를 추적해보니 DispatcherServlet으로부터 호출되는 것을 알 수 있습니다.

그렇다면 DispatcherServlet이전에 임시 파일 경로의 존재 유무를 파악하고 생성하면 되겠다는 생각을 하여 Filter를 사용하기로 했습니다.

Filter 생성 및 추가


@Component
public class MultipartFileFilter extends OncePerRequestFilter {

    @Value("${spring.servlet.multipart.location}") // (1)
    private String tempDirectoryPath; 

    @Override
		protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {

        vaildAndCreateTemporaryDirectory(request);

        chain.doFilter(request, response);
    }

    private void vaildAndCreateTemporaryDirectory(HttpServletRequest request) {

        if (!isMultipartFormData(request)) { // (2)
            return;
        }

        Path path = Paths.get(tempDirectoryPath);
        if (isNotTemporaryDirExist(path)) { // (3)
            try {
                Files.createDirectories(path); // (4)
            } catch (IOException e) {
                log.error("Exception occur at create temporary directory : {}", e.getMessage());
            }
        }
    }

    private boolean isNotTemporaryDirExist(Path path) {
        return !Files.isDirectory(path);
    }

    private boolean isMultipartFormData(HttpServletRequest request) {
        return request.getContentType() != null && request.getContentType().startsWith(MediaType.MULTIPART_FORM_DATA_VALUE);
    }
}

(1) “${spring.servlet.multipart.location}”

YAML 파일에 지정한 임시 파일 경로를 tempDirectoryPath에 바인딩 합니다.

(2) isMultipartFormData()

넘어온 Request의 ContentTypemultipart/form-data인지 검증합니다.

(3) isNotTemporaryDirExist()

지정한 경로가 존재하는지 파악합니다.

(4) Files.createDirectories(path)

경로가 존재하지 않으면 경로를 새로 만듭니다.

위와 같은 Filter를 추가해줌으로써 매 요청마다 multipart/form-data일 때 임시 파일 경로에 대한 문제를 방지할 수 있게 되었습니다.

검증


먼저 YAML 파일에 경로를 지정해주도록 하겠습니다.

yaml

업로드 요청 전 /tmp 하위를 보시면 /multipart 디렉토리가 존재하지 않는 것을 볼 수 있습니다.

before

그리고 요청을 해보면

after1

after2

위와 같이 /multipart 경로가 생성되었고, 임시 파일로 잘 적재되는 것을 확인할 수 있습니다.

마무리


오늘은 유저의 multipart/form-data의 요청 마다 임시 파일 업로드 경로의 생성 여부를 검사하고 생성하는 Filter를 추가하는 과정을 포스팅해봤습니다.

참고 자료로 밑에 링크를 남겨놓겠습니다만 제가 확인한 바로는 Tomcat에서는 해당 로직을 지원하고 있으나, Undertow는 아직 지원하지 않고 있다고 합니다.

혹시나 저와 달리 Tomcat을 사용하시는 분들은 밑에 링크를 참고하시면 도움이 되실겁니다.

해당 이슈를 구글링 해봤을땐 많은 같은 문제를 겪는 사람들은 많으나 해결 방법은 spring.servlet.mulipart.location을 추가하는 것 말고는 의외로 레퍼런스가 많이 없더군요.

제 포스트가 많은 도움이 되셨길 바랍니다.

오늘도 긴 글을 봐주셔서 감사합니다.😊

📌참고 링크


https://github.com/spring-projects/spring-boot/commit/70eee612ff2a2b1e58cbcb18a4d46e464895c18a

https://github.com/spring-projects/spring-boot/issues/9616

댓글남기기