Spring Boot 대용량 파일 업로드와 스트리밍 처리
Spring Boot에서 메모리 과다 사용을 피하고 안정적으로 대용량 파일을 처리하는 방법과 설정, 서블릿 기반 스트리밍 구현 예제를 중심으로 한 설명
목차
개요
대용량 파일 업로드는 단순히 파일을 받아 저장하는 것보다 자원 관리와 안정성이 중요하다. 잘못된 설정은 메모리 과다 사용, 타임아웃, 임시파일 누적 등의 문제를 일으킨다. 본문에서는 spring boot 파일 업로드 대용량 상황에서 고려할 점과 spring boot multipart 스트리밍, spring boot file upload streaming 기법을 중심으로 실용적인 접근을 설명한다.
왜 스트리밍이 필요한가
문제점
Multipart 처리 시 기본 설정은 파일을 메모리 또는 임시 디스크에 올린 뒤 컨트롤러로 전달한다. 파일이 매우 클 때는 메모리 부족이나 임시 저장소의 빠른 소진이 발생할 수 있다. 또한 클라이언트 네트워크가 느릴 경우 전체 업로드가 끝날 때까지 요청을 점유하게 되어 서버 자원이 비효율적으로 사용된다.
목표
업로드 스트림을 읽어 즉시 처리하거나 디스크로 점진적으로 저장해 메모리 사용을 최소화한다. 필요 시 리버스 프록시나 nginx의 버퍼 설정, 타임아웃 조정도 병행한다.
기본 설정
application.properties 예
Spring Boot의 multipart 설정으로 최대 파일 크기와 임시 파일 위치를 지정한다. 이 설정만으로는 스트리밍을 완전 보장하지 않으므로 추가 조치가 필요하다.
spring.servlet.multipart.max-file-size=1GB
spring.servlet.multipart.max-request-size=1GB
spring.servlet.multipart.file-size-threshold=0
spring.servlet.multipart.location=/tmp/uploads
서블릿 기반 스트리밍 처리
가장 간단한 방식은 HttpServletRequest의 Part API를 사용해 InputStream을 직접 읽어 저장하거나 처리하는 것이다. 이 방법은 컨테이너가 요청 바디를 스트리밍으로 제공하면 메모리 점유를 줄일 수 있다.
컨트롤러 예제 (서블릿)
@PostMapping("/upload-stream")
public ResponseEntity<String> upload(HttpServletRequest request) throws Exception {
Collection<Part> parts = request.getParts();
for (Part part : parts) {
String filename = part.getSubmittedFileName();
if (filename == null) continue;
Path target = Paths.get("/data/uploads", filename);
try (InputStream in = part.getInputStream();
OutputStream out = Files.newOutputStream(target, StandardOpenOption.CREATE)) {
byte[] buffer = new byte[8192];
int len;
while ((len = in.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
}
}
return ResponseEntity.ok("ok");
}
중요한 점은 buffer 크기, 예외 처리, 파일 명 검증 등이다. 파일 이름을 그대로 쓰면 보안 이슈가 발생할 수 있으므로 별도 이름 규칙을 적용한다.
Reactive(WebFlux) 기반 스트리밍
리액티브 스택은 백프레셔를 지원하므로 대용량 업로드에 강하다. Flux<DataBuffer>를 받아 바로 파일로 저장하거나 다른 스트림으로 파이프할 수 있다.
WebFlux 예제
@PostMapping("/upload-reactive")
public Mono<ServerResponse> upload(ServerRequest request) {
return request.multipartData().flatMap(parts -> {
FilePart part = (FilePart) parts.toSingleValueMap().get("file");
Path path = Paths.get("/data/uploads", part.filename());
return part.transferTo(path).then(Mono.just(path.toString()));
}).flatMap(path -> ServerResponse.ok().bodyValue("saved:" + path));
}
WebFlux의 transferTo는 내부적으로 스트리밍으로 동작하며, 큰 파일도 안정적으로 처리한다. 단, 애플리케이션이 리액티브로 구성되어 있어야 한다.
운영에서의 고려 사항
- 임시 저장소 용량 확인과 주기적 정리 정책 수립.
- 타임아웃과 최대 동시 업로드 제한을 통해 자원 보호.
- 업로드 중인 파일의 무결성 검증(해시)이나 바이러스 검사 파이프라인 통합.
- 리버스 프록시(nginx) 설정에서 client_max_body_size 및 proxy_buffering 확인.
- 파일명 검증과 저장 경로 분리로 보안 강화.
테스트 및 모니터링
대용량 업로드는 실제 네트워크 환경과 디스크 I/O를 반영한 테스트가 필요하다. 부하 테스트 도구로 다수의 동시 업로드를 시뮬레이션하고, JVM 메모리, 디스크 I/O, 네트워크 사용량을 모니터링한다. 실패 모드는 재시도 정책과 부분 업로드(청크 업로드)를 도입하면 회복력을 높일 수 있다.
요약
spring boot 파일 업로드 대용량 환경에서는 스트리밍 접근이 핵심이다. 서블릿 Part API나 WebFlux의 Flux 스트림을 활용해 메모리 사용을 최소화하고, 적절한 multipart 설정과 인프라 조정을 병행하면 안정적인 파일 업로드 시스템을 만들 수 있다.