스프링 부트로 파일 업로드를 구현하다 보면, 별다른 설정 없이도 MultipartFile 객체를 통해 파일을 받아낼 수 있다.
@PostMapping("/upload")
public String upload(@RequestParam MultipartFile file) {
// 그냥 된다. 신기할 정도로...
return file.getOriginalFilename();
}
그런데 설정에 spring.servlet.multipart.enabled라는 옵션이 존재한다. 기본값은 true라지만, 문득 이런 의문이 든다.
"아니, 멀티파트 요청이 들어오면 당연히 처리해줘야 하는거 아님? 이걸 왜 굳이 끄고 켤 수 있게 만든 거지?"
오늘은 당연하게 생각했던 이 스위치 뒤에 숨겨진 이유를 정리해 보았다.
HTTP 요청이 들어올 때 이게 일반 데이터인지, 대용량 파일이 섞인 Multipart 요청인지 판단하고 파싱(Parsing)하는 과정은 서버 자원을 꽤 잡아먹는 작업이다.
true): 스프링의 MultipartResolver가 모든 요청을 일단 감시한다. 멀티파트 요청이면 HttpServletRequest를 MultipartHttpServletRequest로 변환해 우리가 쓰기 편하게 '세팅'해준다.false): 이런 변환 과정을 통째로 건너뛴다. 서버는 멀티파트 데이터를 그냥 읽을 수 없는 바이너리 덩어리로 취급한다.스프링이 "알아서 다 해줄게!"라고 하지 않고 선택권을 준 이유는 크게 두 가지다.
모든 서비스가 파일 업로드를 사용하는 건 아니다. 만약 업로드 기능이 없는 서버인데, 누군가 악의적으로 10GB짜리 멀티파트 데이터를 계속 던진다고 가정해보자. 서버가 매번 이걸 파싱하려고 시도하는 것 자체가 CPU와 메모리 낭비이며, 서비스 장애(DDoS)로 이어질 수 있다. 이럴 땐 옵션을 꺼서 문을 아예 닫아버리는 게 상책이다.
스프링은 특정 라이브러리를 강제하지 않는다. 표준 서블릿을 쓸지, Apache Commons 같은 외부 라이브러리를 쓸지 개발자가 선택할 수 있게 하려고 "기본 자동 설정을 끌 수 있는 스위치"를 남겨준 것이다.
실제로 이 편리한 옵션을 false로 설정하는 건 아주 특수한 경우라고 한다. 하지만 "스프링의 친절한 자동 처리가 오히려 방해가 되는 상황"에서는 이 선택권이 필수적이라는 점을 알게 되었다.
스프링의 기본 멀티파트 처리는 파일을 받을 때 내부적으로 임시 디렉토리에 저장하거나 메모리에 올린다. 하지만 10GB 이상의 초고용량 영상을 처리해야 한다면? 스프링이 이걸 중간에 가로채서 저장하려다 서버 메모리가 버티지 못할 수 있다.
이런 경우 옵션을 끄고, 개발자가 서블릿 입력 스트림(InputStream)을 직접 열어 데이터가 들어오는 대로 즉시 스토리지로 쏴버리는 방식을 택한다고 한다.
스프링이 파싱을 시작하기 전(Filter 단계)에 HTTP 헤더만 보고 "허용되지 않은 파일이니 파싱조차 안 하겠다"라고 더 앞단에서 차단하고 싶을 때, 자동 설정을 끄고 직접 제어 로직을 구현하기도 한다.
결국 spring.servlet.multipart.enabled 옵션이 존재하는 이유는 "자동화의 편리함(True)과 수동 제어의 세밀함(False) 사이의 선택권"을 주기 위해서였다.
MultipartResolver의 편리함을 누리면 된다."Multipart 처리를 꺼야 하는 이유가 있나?"라는 단순한 의문에서 시작했지만, 공부를 마칠 때쯤엔 스프링이 개발자의 자유도를 얼마나 깊게 고려하고 있는지 체감할 수 있었다. 역시 모든 설정에는 다 이유가 있고, 그 이면을 이해할 때 비로소 프레임워크를 제대로 다룰 수 있게 되는 것 같다.