분명 파일을 같이 보냈는데 요청 파일을 찾을 수 없다
최근에 받은 프로젝트에 대해서 전체적으로 테스트하고 결과를 기록하던 중에 이미지를 업로드하는 API에서 요청 파일을 MultipartFile로 받지 못하는 문제가 발생했다.
@PostMapping("/image-upload")
public ResponseEntity<Void> imageUpload(@RequestPart(value="file") MultipartFile multipartFile) {
//... 이미지 업로드 로직
}
해당 API endpoint에 대해 작성되어있는 테스트 코드는 통과했기 때문에 애플리케이션 구동 시 request part 'file' is not present 라는 에러가 뜨는 게 당황스러웠다. 해당 버그를 해결하기 위해 많은 삽질과 검색을 했던 기록을 남기고자 한다.
문제의 원인
처음에는 컨트롤러 자체에 문제가 있다고 생각해서 작성된 컨트롤러 코드를 살펴봤지만 당연히 문제가 없었다. 다음으로 테스트 코드도 확인해봤으나 테스트 코드도 문제는 없었다. 그러다 테스트 코드가 1) @WebMvcTest로 작성되어 있고, 2) 테스트 코드에서는 해당 로직이 통과하고, 3) 실제 애플리케이션 구동 후 테스트할 때는 에러가 발생한다는 것을 근거로 컨트롤러 단 밖의 코드에 문제가 있을 것으로 판단했다.
두번째로는 애플리케이션을 구동해서 해당 요청을 디버깅했다. 실제로 요청을 처리하는 DispatchServlet doDispatch에서 MultipartFile 용 Request 객체인 StandardMultipartHttpServletRequest 로 request 객체를 받고 있지만 안의 multipart 관련 내용들은 전부 빈 값으로 받는 것을 알 수 있었다.


해당 디버깅 과정을 통해 프로젝트 내에서 HTTP 요청을 커스터마이징 하는 필터들 중 하나가 파일 요청을 임의로 변경하고 있다고 판단했다. 프로젝트에서 필터를 추가하는 부분은 크게 Spring Security 관련 부분과 HTTP 요청/응답 로깅 부분이 있었다. 처음에는 스프링 시큐리티의 필터들이 MultipartFile 관련 필터보다 먼저 적용되기 때문에 문제가 생기는 것으로 생각하여 스프링 시큐리티의 필터 적용 순서를 미루기도 해봤으나 다른 에러만 발생하고 문제가 해결되지 않았다.
결국 해당 버그가 발생하는 원인은 HTTP 요청/응답을 로깅하기 위해 재정의해서 등록한 logback-access 라이브러리의 TeeFilter 와 MultipartFile의 충돌 때문이었다. 먼저 TeeFilter는 원본의 HTTP 요청의 inputStream을 감싸주게 된다. 그 후에 MultipartFile에서 원본 요청의 inputStream을 사용하려고 시도한다. 해당 과정에서 TeeFilter가 감싼 inputStream과 충돌하게 되고 그로 인해 요청 데이터가 손실되는 버그가 발생하게 되는 것이었다.
해결 방안
해결방안은 다음과 같았다.
- TeeFilter에 파일 업로드 관련 url 패턴을 제외하는 규칙을 추가해준다.
- TeeFilter를 사용하지 않는다.
먼저 TeeFilter에 파일 업로드와 관련된 url 패턴을 제외하는 규칙을 추가해주는 방법은 설정 파일에 설정값을 추가해주는 방법과 직접 TeeFilter를 등록할 때 규칙을 추가해주는 방법이 있었다. 먼저 설정 파일에 설정값을 추가해주는 방법은 application.yml 기준으로 다음과 같이 추가해주면 된다.
logging:
filter:
tee:
exclude-path-patterns: /file-upload/**
TeeFilter를 직접 추가해주는 방법은 다음과 같다.
@Configuration
public class TeeFilterConfiguration {
@Bean
public FilterRegistrationBean<TeeFilter> teeFilter() {
FilterRegistrationBean<TeeFilter> registrationBean = new FilterRegistrationBean<>();
TeeFilter teeFilter = new TeeFilter();
registrationBean.setFilter(teeFilter);
registrationBean.addUrlPatterns("/*");
registrationBean.addInitParameter("excludePathPatterns", "/image-upload/**");
return registrationBean;
}
}
위의 두 방법 중 하나를 선택해 MultipartFile을 사용하는 요청에서는 TeeFilter을 적용하지 않도록 해주면 된다.
TeeFilter를 사용하지 않고 직접 HTTP 요청과 응답을 로깅하는 필터나 인터셉터를 등록하는 방법도 있다. TeeFilter가 편해보이긴 하지만 공식문서에서도 운영 환경에서의 적용을 추천하지 않고 있기 때문에, HTTP 요청과 응답을 로깅해주는 필터나 인터셉터를 따로 만들어 등록하는 것도 괜찮을 방법이 될 것 같다.
TL;DR
- MultipartFile을 사용하는 요청 URL는 TeeFilter와 충돌이 발생한다.
- TeeFilter를 사용하지 않는 것도 좋다.
참고 문헌
https://logback.qos.ch/access.html
Logback-access
HTTP-access logs with logback-access, Jetty and Tomcat Authors: Ceki Gülcü, Sébastien Pennec --> Introduction The logback-access module, part of the standard logback distribution, integrates with Servlet containers such as Jetty or Tomcat to provide ric
logback.qos.ch
https://github.com/akkinoc/logback-access-spring-boot-starter
GitHub - akkinoc/logback-access-spring-boot-starter: Spring Boot Starter for Logback-access.
Spring Boot Starter for Logback-access. Contribute to akkinoc/logback-access-spring-boot-starter development by creating an account on GitHub.
github.com
'Java > Spring Boot' 카테고리의 다른 글
| Datadog Agentlesss Logging 시 service 이름 추가하기 (0) | 2023.04.10 |
|---|---|
| Spring Boot로 만드는 API 서버 템플릿 (2) - 맛있는 Refresh Token을 위한 여정 (0) | 2023.03.21 |
| Spring Boot로 만드는 API 서버 템플릿 (1) - 시작 (0) | 2023.03.21 |