스프링 프레임워크와 파일 업로드 릴레이

🗓️

파일 업로드 다운로드를 개발하다 보면 내가 개발하고 있는 어플리케이션이 파일이 저장되는 종착지가 아닐때가 있다. 예를들어 하나의 백엔드 어플리케이션이 모든 도메인을 처리하고 있고 그중에 파일 관련된 핸들링도 포함되어있다면 그 백엔드가 파일 시스템 어딘가에 저장하거나 S3 스토리지로 보낸다. 그런데 마이크로서비스가 보편화된 시대에서 파일이 여러 서비스를 거쳐야 파일 핸들링을 하는 서비스에 도달하는 경우가 있을 수 있다. 그 역할을 클라우드 플랫폼에 포함된 API 게이트웨이가 아니라 Spring Framework로 만들어진 WAS가 해야 한다면 어떻게 해야할까?

Spring Framework에서 제공하는 Multipart 는 파일시스템에 저장하기 위해 InputStream에 대한 핸들링을 편리하게 할 수 있도록 도와준다. 그러나 릴레이를 하기에는 적합하지 않은데, 파일 전체를 먼저 수신해야 하는 블로킹 구조 때문이다. 파일 전체를 파일시스템에 저장한 후 다음 목적지로 송신하게 된다. 그래프에서도 나타나듯 1GB의 파일을 최종 목적지까지 업로드하는데 Multipart 방식으로 업로드 한다면 N+1만큼의 시간이 드는 샘이다. 그렇다고 프론트엔드 또는 UI 에서 최종 목적지까지 파일 업로드를 직접 하는 것은 위험천만한 일이다. 중간 파이프라인을 생략한다면 인증/인가와 로깅과 같은 설계적 문제가 생길 뿐만 아니라 내부 모듈이 외부에 노출된다는 취약점을 가지기 때문이다. ‘파일을 핸들링 하는 서비스에 인증 서비스와 협력하도록 하고 CQRS를 허용하면 되지 않냐’ 는 식의 접근 보다는 다른 차원의 문제라고 생각한다.

그래서 드는 생각은 Reactive를 도입하는 것이었다. 실제로 테스트 시 성능도 손실 없이 네트워크 성능을 거의 다 끌어다 사용했다. 때문에 매우 적절한 선택지로 보였다. 그러나 WebFlux 를 도입하는데는 배보다 배꼽이 더 크다는 문제가 있다. 개인적으로 함수형 프로그래밍에 큰 관심이 있고 늘 프로젝트에 도입하고 싶었다. 그러나 이 Reactive 프로그래밍의 문제점은 이런저런 이유를 다 빼고 절차형 프로그래밍에 비해 난이도가 너무 높다는데 있다. 나 혼자 개발을 한다면 상관없지만 다른 동료들과 함께 해야하고, 프로젝트 도중이라면 Spring MVC를 WebFlux로 엎어야만 하는데 불가능에 가까운 일이다.

더 좋은 방법이 없을까 고민하다 ’InputStream이 바이트 스트림이라면 chunking 방식으로 중계할 수 있지 않을까?’ 라는 생각에 도달했고 몇가지 알아보니 몰랐던 것들을 알게됐다.

  • WebFlux는 컨트롤러의 Exchange에서 body를 WebClient로 전달하면 자동으로 구독과 발행 파이프라인을 탄다. 별로 해줄게 없다.
  • Spring MVC에서 제공하는 RestTemplate도 오브젝트뿐만 아니라 바이트 배열을 사용할 수 있다.
  • Apache HttpClient는 바이트 배열에 대한 chunking을 자동으로 제공한다.

막연한 아이디어를 테스트해보기 위해 코드를 준비했고 다음과 같다.

  • 환경 1 기존 Multipart로 구현한 컨트롤러 (Spring MVC + Multipart)
  • 환경 2 ServerWebExchange에서 WebClient로 바로 릴레이 (Spring WebFlux + Flux)
  • 환경 3 InputStream에서 바이트를 따서 바로 Apache HttpClient로 릴레이 (Spring MVC + Byte array)
  • 환경 4 InputStream에서 바이트를 따서 바로 RestTemplate으로 릴레이 (Spring MVC + Byte array)

최종 정착지는 Spring MVC 로 REST API를 구현해 파일을 저장하도록 구현했다.

약 10GB 파일을 두고 테스트 결과는 아래와 같았다

  • 직접 업로드 : 7.7초
  • 환경 1 : 29.5초 (가장 느림)
  • 환경 2 : 9.5초 (가장 빠름)
  • 환경 3 : 16.3초
  • 환경 4 : 계측 실패 – Out of memory

환경에 따라 같은 크기의 파일을 릴레이 하더라도 차이는 더 큰 폭으로 늘어날 것이다.

환경 4의 경우는 수신되는 모든 바이트 배열을 메모리에 로드하려고 했기 때문에 OOM이 발생했다. WebFlux를 릴레이로 두는것은 최적의 선택사항이지만 채택이 불가능 하다. 결론적으로 파일 업로드 릴레이 구조에서 Spring MVC 기반의 기존 어플리케이션을 유지하며 성능을 최적화하려면 Apache HttpClient를 사용하는 것이 가장 현실적이고 효과적인 방안이다.

Apache HttpClient가 제공하는 chunk 방식은 현재 상황에서 아래와 같은 이유로 적합하다고 볼 수 있다.

  1. 스트리밍 처리 방식 : Apache HttpClient는 기본적으로 chunked transfer encoding을 지원하며, 데이터를 고정된 크기로 chunk 해 송신함으로써, InputStream의 모든 데이터를 메모리에 로딩할 필요가 없다. 이는 메모리 사용을 최소화하고, OOM 같은 문제를 방지한다.
  2. 높은 성능과 효율적인 네트워크 사용 : 테스트 결과에서도 나타났듯이 Apache HttpClient를 이용한 방법은 Multipart 방식 대비 절반 정도의 시간밖에 소요되지 않았다. 네트워크를 최대한 활용하면서도 시스템 자원을 효율적으로 관리할 수 있다.

요약하면, Apache HttpClient를 활용한 스트리밍 전송 방식은 Spring MVC 기반의 기존 어플리케이션 구조를 유지하면서 파일 업로드 릴레이 성능을 효율적으로 최적화할 수 있는 현실적인 방안이다. 실제로 이 아이디어를 도입해 계층별 파일 릴레이를 시간/공간 손실이 적은 방향으로 진행했으며 개발 테스트 결과 만족할만한 결과가 나왔다.