개발에 AtoZ까지

[Spring Boot 3] 파일 업로드가 예전 코드 그대로면 막히는 이유 — 1MB 한도와 새로 생긴 part 개수 제한 본문

백엔드/Spring

[Spring Boot 3] 파일 업로드가 예전 코드 그대로면 막히는 이유 — 1MB 한도와 새로 생긴 part 개수 제한

AtoZ 개발자 2026. 8. 10. 19:30
반응형

몇 년 전에 만들어 잘 돌던 다중 파일 업로드 화면이, 프레임워크만 올리고 나면 갑자기 안 되는 일이 있습니다. 로컬에서는 되는데 운영에서만 실패하거나, 파일 3개까지는 되는데 5개부터 조용히 실패하는 식이죠.

원인이 하나가 아니라서 헷갈립니다. 업로드 요청은 여러 층의 한도를 차례로 통과해야 하는데, 그중 어디서 걸렸는지에 따라 에러 메시지가 완전히 달라집니다. 게다가 2025년에 새 한도가 하나 더 생겼습니다.

이 글을 다 읽으면 세 가지가 정리됩니다. 첫째, 업로드 요청이 통과하는 한도 계층 세 개를 구분할 수 있습니다. 둘째, 에러 메시지만 보고 어느 설정을 고쳐야 하는지 바로 찾을 수 있습니다. 셋째, 예전 코드에서 그대로 옮겨 오면 안 되는 부분을 확인할 수 있습니다.

이 글은 Spring Boot·Spring Framework·Apache Tomcat 공식 문서와 공개된 설정 기본값을 기준으로 정리했습니다. 버전에 따라 값이 다를 수 있으니 본인 프로젝트 버전으로 확인해 주세요.

업로드 요청은 커넥터 한도 → 서블릿 멀티파트 한도 → 애플리케이션 검증을 차례로 통과합니다

먼저 사라진 것부터: CommonsMultipartResolver

오래된 예제를 그대로 복사하면 첫 줄부터 컴파일이 안 됩니다. Apache Commons FileUpload 기반의 CommonsMultipartResolverSpring Framework 6.0에서 제거됐기 때문입니다. Servlet 5.0 이상을 기준으로 잡으면서 정리된 것이죠.

지금은 서블릿 컨테이너의 멀티파트 파서를 그대로 쓰는 StandardServletMultipartResolver가 표준입니다. Spring Framework 공식 문서도 이 구현을 제공한다고 안내하고 있습니다.

Spring Boot를 쓰신다면 별도 빈 등록도 필요 없습니다. 자동 설정이 멀티파트를 켜 주기 때문에 컨트롤러만 있으면 됩니다.

@RestController
public class UploadController {

    @PostMapping("/upload")
    public String single(@RequestParam("file") MultipartFile file) {
        if (file.isEmpty()) {
            throw new IllegalArgumentException("빈 파일입니다.");
        }
        return file.getOriginalFilename() + " / " + file.getSize() + " bytes";
    }

    @PostMapping("/upload/multi")
    public int multi(@RequestParam("files") List<MultipartFile> files) {
        return files.size();
    }
}

pom.xml이나 build.gradlecommons-fileupload 의존성이 남아 있다면 지우셔도 됩니다. 지금은 쓰이지 않습니다.

Spring Framework 공식 문서의 Multipart Resolver 안내 (출처: docs.spring.io, 확인일 2026-08-08)

첫 번째 한도: 1MB와 10MB

가장 자주 걸리는 곳입니다. Spring Boot의 멀티파트 기본값은 생각보다 작습니다.

프로퍼티 기본값 의미
spring.servlet.multipart.enabled true 멀티파트 지원 사용 여부
spring.servlet.multipart.max-file-size 1MB 파일 한 개의 최대 크기
spring.servlet.multipart.max-request-size 10MB 요청 전체의 최대 크기
spring.servlet.multipart.file-size-threshold 0B 이 크기를 넘으면 디스크에 씀
spring.servlet.multipart.location 없음 업로드 임시 저장 위치
spring.servlet.multipart.resolve-lazily false 파일·파라미터 접근 시점에 파싱할지

여기서 두 값의 차이가 중요합니다. 3MB짜리 파일 한 개를 올리면 max-file-size에서 걸리고, 800KB짜리 파일 20개를 한 번에 올리면 파일별로는 통과하지만 합계 16MB라 max-request-size에서 걸립니다. 에러 메시지가 같아 보여도 고쳐야 할 값이 다릅니다.

spring.servlet.multipart.max-file-size=20MB
spring.servlet.multipart.max-request-size=100MB

값을 올릴 때는 max-request-sizemax-file-size보다 반드시 크게 잡으셔야 합니다. 파일 하나만 올려도 요청에는 경계 문자열과 헤더가 함께 들어가기 때문입니다.

두 번째 한도: 2025년에 새로 생긴 part 개수 제한

이게 최근에 사람들을 당황시킨 부분입니다. 예전에는 멀티파트 요청의 part 개수에 별도 제한이 없었는데, Apache Tomcat 10.1.42(그리고 11.0.8)에서 두 속성이 새로 생겼습니다. part 수가 지나치게 많은 요청으로 메모리를 고갈시키는 공격을 막기 위한 조치입니다.

Tomcat 공식 커넥터 문서의 설명을 옮기면 이렇습니다.

속성 현재 기본값 설명
maxPartCount 50 multipart/form-data 요청에서 허용하는 최대 part 수. 이 제한은 maxParameterCount추가로 적용됨
maxPartHeaderSize 512 part 하나당 허용하는 최대 헤더 바이트 수. 초과하면 요청 거부
maxParameterCount 10000 쿼리 스트링과 요청 본문에서 파싱하는 파라미터 총 개수

문서는 이 값을 왜 무한정 키우면 안 되는지도 함께 설명합니다. 멀티파트 처리에 필요한 메모리는 대략 maxPartHeaderSize × maxPartCount × maxConnections × 2이고, 기본값 기준으로 계산하면 512 × 50 × 8192 × 2, 즉 400MB가 됩니다. part 개수를 열 배로 늘리면 이 계산도 열 배가 된다는 뜻이죠.

주의할 점은 part가 파일만을 뜻하지 않는다는 것입니다. 멀티파트 규격에서는 함께 보내는 텍스트 필드도 각각 하나의 part입니다. 파일 10개에 메타데이터 입력 필드 45개를 같이 보내면 55개가 되어 한도를 넘습니다.

이 값의 기본이 처음 도입될 때는 10이었고 이후 50으로 상향됐습니다. 그래서 어느 패치 버전에 있느냐에 따라 체감이 다릅니다. "특정 시점에 업그레이드한 뒤부터 다중 업로드가 깨졌다"는 이야기의 배경이 대체로 이겁니다.

Apache Tomcat 10.1 커넥터 문서의 maxPartCount·maxPartHeaderSize 설명 (출처: tomcat.apache.org, 확인일 2026-08-08)

Spring Boot 3.5부터는 프로퍼티로 노출됩니다.

# 파일 10개 + 텍스트 필드가 많은 폼이라면 넉넉히
server.tomcat.max-part-count=200
server.tomcat.max-part-header-size=1KB

그보다 낮은 버전이라면 프로퍼티가 없으니 커스터마이저로 직접 지정하셔야 합니다.

@Bean
WebServerFactoryCustomizer<TomcatServletWebServerFactory> partCountCustomizer() {
    return factory -> factory.addConnectorCustomizers(connector -> {
        connector.setProperty("maxPartCount", "200");
        connector.setProperty("maxPartHeaderSize", "1024");
    });
}

세 번째 한도: 그 앞에 있는 것들

여기까지 왔는데도 막힌다면 앞단을 보셔야 합니다.

  • server.tomcat.max-http-form-post-size — 기본 2MB. 폼 본문 크기 제한입니다.
  • server.tomcat.max-swallow-size — 기본 2MB. 거절한 요청의 남은 본문을 얼마나 읽고 버릴지입니다.
  • 앞단의 Nginx client_max_body_size, 로드밸런서·API 게이트웨이의 본문 제한.

max-swallow-size는 증상이 특이해서 알아 둘 만합니다. 큰 파일이 한도에 걸려 거절될 때 서버가 남은 본문을 다 읽지 않고 커넥션을 끊어 버리면, 클라이언트는 우리가 정성껏 만든 413 응답 대신 연결이 끊겼다는 네트워크 에러를 봅니다. "서버 로그에는 예외가 찍히는데 브라우저에는 우리 에러 메시지가 안 뜬다"면 이 값을 의심해 보세요.

에러 메시지로 원인 찾기

메시지별로 고쳐야 하는 설정이 다릅니다

보이는 것 원인 고칠 곳
MaxUploadSizeExceededException / Maximum upload size exceeded 파일 또는 요청 전체 크기 초과 max-file-size, max-request-size
MissingServletRequestPartException: Required part 'file' is not present 폼의 name@RequestParam 이름 불일치, 또는 멀티파트가 아닌 요청 프런트 name 속성 확인
part가 일부만 도착하거나 요청 자체가 거부됨 part 개수·헤더 크기 초과 server.tomcat.max-part-count, max-part-header-size
413 응답이 프록시 단계에서 발생 웹서버 본문 제한 Nginx client_max_body_size
응답 없이 연결 끊김 거절 후 본문 폐기 한도 server.tomcat.max-swallow-size
CommonsMultipartResolver 관련 컴파일 오류 Spring 6에서 제거됨 StandardServletMultipartResolver로 전환

크기 초과는 예외를 잡아 사용자에게 친절한 응답으로 바꿔 주는 편이 좋습니다.

@RestControllerAdvice
public class UploadExceptionHandler {

    @ExceptionHandler(MaxUploadSizeExceededException.class)
    public ProblemDetail handleTooLarge(MaxUploadSizeExceededException e) {
        ProblemDetail body = ProblemDetail.forStatusAndDetail(
                HttpStatus.PAYLOAD_TOO_LARGE,
                "업로드 가능한 최대 크기를 초과했습니다.");
        body.setTitle("파일 크기 초과");
        return body;
    }
}

에러 응답 형식을 프로젝트 전체에서 통일하는 방법은 ProblemDetail로 에러 응답 표준화 글에 정리해 두었습니다.

올리기 전에 확인할 목록

  • [ ] 업로드할 가장 큰 파일 한 개의 크기를 기준으로 max-file-size를 정한다
  • [ ] 동시에 올릴 수 있는 최대 합계를 기준으로 max-request-size를 정한다(항상 더 크게)
  • [ ] 폼에 들어가는 파일 수 + 텍스트 필드 수를 세어 part 개수 한도와 비교한다
  • [ ] Tomcat 버전이 10.1.42 이상인지 확인한다(이상이면 part 한도가 존재한다)
  • [ ] Nginx·로드밸런서 등 앞단의 본문 크기 제한을 함께 올린다
  • [ ] MaxUploadSizeExceededException 핸들러로 사용자에게 보일 응답을 만든다
  • [ ] 임시 파일이 쌓이는 경로(spring.servlet.multipart.location)의 디스크 여유를 확인한다

복사해서 바로 쓰실 수 있는 설정 묶음입니다.

# 업로드 한도
spring.servlet.multipart.max-file-size=20MB
spring.servlet.multipart.max-request-size=200MB
spring.servlet.multipart.file-size-threshold=1MB

# 멀티파트 part 관련 (Spring Boot 3.5+)
server.tomcat.max-part-count=200
server.tomcat.max-part-header-size=1KB

# 거절 응답이 클라이언트에 닿도록
server.tomcat.max-swallow-size=20MB

file-size-threshold를 올려 두면 작은 파일은 메모리에서 처리하고 큰 파일만 디스크로 내려갑니다. 트래픽이 많은 서비스라면 메모리와 디스크 사이에서 균형을 잡아 보실 만합니다.

다음에 볼 글

이 글은 예전에 정리했던 Spring 파일 단일·다중 업로드 글을 지금 기준으로 다시 본 것입니다. 그때는 없던 한도가 생겼고 리졸버 하나가 사라졌습니다. 요청 파라미터를 받는 방식이 버전에 따라 어떻게 달라졌는지는 전송 방식별 파라미터 받기Spring Boot 3.2 이후 파라미터 이름 에러에 이어서 정리해 두었습니다.

잘못된 내용이나 보충이 필요한 부분이 있으면 댓글로 알려 주시면 반영하겠습니다.

참고한 자료

  • Spring Framework Reference — Multipart Resolver, StandardServletMultipartResolver (확인일 2026-08-08)
  • Spring Boot MultipartProperties 기본값 — max-file-size 1MB, max-request-size 10MB (확인일 2026-08-08)
  • Spring Boot ServerProperties Tomcat 기본값 — maxPartCount 50, maxPartHeaderSize 512B, maxSwallowSize 2MB (확인일 2026-08-08)
  • Apache Tomcat 10.1 Configuration Reference — HTTP Connector maxPartCount, maxPartHeaderSize, maxParameterCount (확인일 2026-08-08)
  • Spring Framework 6 업그레이드 안내 — Apache Commons FileUpload 기반 리졸버 제거 (확인일 2026-08-08)
반응형
Comments