Friday, December 7, 2018

실전 Spring Boot 대용량 파일 업로드 처리: 설정, 예외 처리, 그리고 성능 최적화

현대 웹 애플리케이션에서 파일 업로드는 단순한 이미지 첨부를 넘어, 동영상, 대용량 데이터셋, 소프트웨어 배포판 등 그 범위와 규모가 날로 확장되고 있습니다. 사용자는 몇 기가바이트(GB)에 달하는 파일을 손쉽게 업로드하기를 기대하지만, 개발자에게 이는 결코 간단한 문제가 아닙니다. Spring Boot는 이러한 대용량 파일 처리를 위한 강력하고 유연한 기능을 제공하지만, 그 내부 동작 원리와 올바른 설정 없이는 예상치 못한 오류와 성능 저하에 직면하기 쉽습니다. 이 글에서는 Spring Boot 환경에서 대용량 파일 업로드를 완벽하게 다루기 위한 실전 지식을 총망라합니다. 단순한 설정 변경부터 고급 예외 처리, 성능 최적화를 위한 스트리밍 기법까지, 안정적이고 효율적인 파일 업로드 시스템을 구축하는 데 필요한 모든 것을 상세히 다룹니다.

Spring Boot의 기본 파일 업로드 제한 이해하기

Spring Boot 애플리케이션에서 대용량 파일 업로드가 실패하는 가장 흔한 원인은 바로 내장된 파일 크기 제한입니다. 많은 개발자들이 이를 버그로 오해하지만, 사실 이는 시스템 자원을 보호하기 위한 필수적인 안전장치입니다. 왜 이러한 제한이 존재하며, 어떻게 동작하는지 이해하는 것이 문제 해결의 첫걸음입니다.

모든 것은 보안과 안정성에서 시작됩니다

만약 파일 업로드 크기에 아무런 제한이 없다면 어떻게 될까요? 악의적인 사용자가 수십, 수백 기가바이트의 거대한 파일을 서버로 무차별적으로 전송하는 디도스(DDoS) 공격을 시도할 수 있습니다. 이러한 공격은 서버의 디스크 공간을 순식간에 고갈시키거나, 메모리 부족(Out of Memory) 오류를 유발하여 애플리케이션 전체를 마비시킬 수 있습니다. Spring Boot는 이러한 위협으로부터 애플리케이션을 보호하기 위해 기본적으로 멀티파트 요청(파일 업로드를 포함하는 요청)의 크기를 합리적인 수준으로 제한합니다.

  • 기본 최대 파일 크기 (spring.servlet.multipart.max-file-size): 1MB
  • 기본 최대 요청 크기 (spring.servlet.multipart.max-request-size): 10MB

즉, 별도의 설정이 없다면 1MB를 초과하는 단일 파일이나, 총 크기가 10MB를 초과하는 여러 파일을 업로드하려고 시도하면 즉시 오류가 발생합니다. 이것이 바로 우리가 가장 먼저 마주하는 '업로드 제한'의 실체입니다.

두 가지 핵심 설정: max-file-size와 max-request-size

이 문제를 해결하기 위해 Spring Boot는 두 가지 핵심 프로퍼티를 제공합니다. 이 둘의 차이점을 명확히 이해하는 것이 중요합니다.

  • spring.servlet.multipart.max-file-size: 이 설정은 개별 파일 하나의 최대 허용 크기를 정의합니다. 예를 들어 이 값을 '100MB'로 설정하면, 사용자는 100MB 미만의 파일은 업로드할 수 있지만 100MB를 초과하는 단일 파일은 업로드할 수 없습니다.
  • spring.servlet.multipart.max-request-size: 이 설정은 하나의 HTTP 요청에 포함될 수 있는 모든 데이터(모든 파일 + 텍스트 데이터)의 총 최대 크기를 정의합니다. 예를 들어, 한 번의 요청으로 여러 파일을 동시에 업로드하는 경우, 각 파일의 크기는 max-file-size를 넘지 않아야 하고, 모든 파일 크기의 합과 다른 폼 데이터의 크기를 더한 총합이 max-request-size를 넘지 않아야 합니다.

따라서, 일반적으로 max-request-sizemax-file-size보다 크거나 같게 설정해야 합니다. 만약 max-file-sizemax-request-size보다 크다면, 그 설정은 사실상 의미가 없어집니다. 왜냐하면 파일이 max-file-size에 도달하기 전에 이미 max-request-size 제한에 걸리기 때문입니다.


가장 쉬운 방법: application.properties/yml 설정

대부분의 경우, 이 파일 업로드 제한은 application.properties 또는 application.yml 파일에 몇 줄의 코드를 추가하는 것만으로 간단하게 해결할 수 있습니다. 이는 가장 직관적이고 널리 사용되는 방법입니다.

application.properties를 이용한 설정

src/main/resources/application.properties 파일에 다음과 같이 추가합니다. 아래 예제는 개별 파일의 최대 크기를 200MB로, 전체 요청의 최대 크기를 200MB로 설정하는 경우입니다.

# ===================================================================
# 파일 업로드 관련 설정 (Multipart Configuration)
# ===================================================================
# 멀티파트 활성화 여부 (기본값: true)
spring.servlet.multipart.enabled=true

# 단일 파일의 최대 크기 설정 (기본값: 1MB)
spring.servlet.multipart.max-file-size=200MB

# 전체 요청의 최대 크기 설정 (기본값: 10MB)
spring.servlet.multipart.max-request-size=200MB

application.yml을 이용한 설정

YAML 형식을 선호하는 경우, src/main/resources/application.yml 파일에 다음과 같이 계층적으로 설정할 수 있습니다. 기능은 .properties와 동일합니다.

spring:
  servlet:
    multipart:
      enabled: true
      max-file-size: 200MB
      max-request-size: 200MB

용량 단위 설정과 무제한(-1)의 함정

Spring Boot는 용량 단위를 매우 유연하게 인식합니다. 숫자 뒤에 특정 단위를 붙이지 않으면 기본적으로 바이트(Bytes) 단위로 인식됩니다. 가독성과 편의성을 위해 다음과 같은 단위를 사용할 수 있습니다.

  • KB: 킬로바이트
  • MB: 메가바이트
  • GB: 기가바이트
  • TB: 테라바이트

예를 들어 10485760 대신 10MB라고 명시적으로 작성하는 것이 훨씬 좋습니다.

또한, 두 설정값을 모두 -1로 설정하면 기술적으로 파일 크기 제한이 사라집니다(무제한).

spring:
  servlet:
    multipart:
      max-file-size: -1 # 무제한
      max-request-size: -1 # 무제한
개발 환경에서 테스트 목적으로는 편리할 수 있지만, 운영 환경에서는 절대 권장되지 않습니다. 앞서 설명한 디도스 공격에 매우 취약해지기 때문입니다. 항상 서비스의 요구사항과 서버의 물리적 자원(디스크, 메모리)을 고려하여 '충분히 크지만 합리적인' 수준의 제한을 설정하는 것이 보안과 안정성 측면에서 올바른 접근 방식입니다.


프로그래밍 방식의 동적 설정: MultipartConfigElement Bean 등록

application.properties.yml을 사용하는 정적 설정은 간단하지만, 때로는 애플리케이션 실행 중에 동적으로 설정을 변경하거나, 특정 프로필에 따라 다른 설정을 적용하고 싶을 수 있습니다. 이럴 때는 Java 코드를 통해 직접 MultipartConfigElement Bean을 등록하는 방식을 사용할 수 있습니다.

왜 Java 코드로 설정해야 할까?

  • 동적 설정: 외부 설정 소스(예: 데이터베이스, 원격 설정 서버)로부터 파일 크기 제한 값을 읽어와 동적으로 적용할 수 있습니다.
  • 조건부 설정: Spring의 @Profile 애너테이션 등과 결합하여, '개발(dev)', '테스트(test)', '운영(prod)' 환경에 따라 각각 다른 파일 업로드 정책을 적용할 수 있습니다.
  • 고급 옵션 제어: application.properties에서는 설정하기 어려운 fileSizeThresholdlocation 같은 세부적인 옵션을 제어할 수 있습니다.

MultipartConfigElement Bean 설정 예제

다음은 @Configuration 클래스를 생성하여 MultipartResolverMultipartConfigElement를 직접 설정하는 예제입니다.

import org.springframework.boot.web.servlet.MultipartConfigFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.unit.DataSize;
import org.springframework.web.multipart.MultipartResolver;
import org.springframework.web.multipart.support.StandardServletMultipartResolver;

import javax.servlet.MultipartConfigElement;

@Configuration
public class MultipartConfig {

    @Bean
    public MultipartResolver multipartResolver() {
        return new StandardServletMultipartResolver();
    }

    @Bean
    public MultipartConfigElement multipartConfigElement() {
        MultipartConfigFactory factory = new MultipartConfigFactory();
        // 개별 파일 사이즈 500MB
        factory.setMaxFileSize(DataSize.ofMegabytes(500L));
        // 총 업로드 데이터 사이즈 500MB
        factory.setMaxRequestSize(DataSize.ofMegabytes(500L));
        
        return factory.createMultipartConfig();
    }
}

주의: 이와 같이 Java Bean으로 직접 설정을 구성하면 application.properties.yml 파일에 있는 spring.servlet.multipart.* 설정은 무시됩니다. Bean 설정이 항상 더 높은 우선순위를 갖기 때문입니다.

고급 옵션 파헤치기: fileSizeThreshold와 location

Java 설정을 사용하면 두 가지 유용한 고급 옵션을 추가로 제어할 수 있습니다.

  • fileSizeThreshold: 업로드된 파일의 데이터가 특정 크기에 도달할 때까지 메모리에 보관하고, 그 크기를 초과하면 디스크의 임시 파일로 저장하도록 하는 임계값입니다. 작은 파일은 디스크 I/O 없이 메모리에서 빠르게 처리하고, 큰 파일은 메모리 오버플로우를 방지하기 위해 디스크를 사용하도록 하는 효율적인 방식입니다. 기본값은 0이며, 이는 모든 파일 데이터를 일단 디스크에 쓴다는 의미입니다.
    // 30MB 까지는 메모리에 저장, 그 이상은 디스크에 임시 저장
    factory.setFileSizeThreshold(DataSize.ofMegabytes(30L));
    
  • location: 멀티파트 데이터가 임시로 저장될 디스크의 경로를 지정합니다. 기본적으로 서버(Tomcat 등)가 제공하는 임시 디렉토리를 사용합니다. 별도의 고성능 디스크(SSD 등)에 임시 파일 경로를 지정하여 성능을 향상시키거나, 특정 파티션에 공간을 할당하여 루트 파티션이 가득 차는 것을 방지할 수 있습니다.
    // 임시 파일 저장 경로 지정
    factory.setLocation("/path/to/your/temp/directory");
    

예외는 피할 수 없다면, 우아하게 처리하라: MaxUploadSizeExceededException 핸들링

파일 크기 제한을 설정했다고 해서 모든 것이 끝난 것은 아닙니다. 사용자가 설정된 크기를 초과하는 파일을 업로드하면 어떤 일이 발생할까요? Spring은 MaxUploadSizeExceededException이라는 예외를 발생시킵니다. 이 예외를 제대로 처리하지 않으면, 사용자는 아무런 설명 없는 서버 오류 페이지(Whitelabel Error Page 등)를 보게 되어 좋지 않은 사용자 경험을 하게 됩니다.

무엇이 문제인가: 갑작스러운 에러 페이지

기본적으로 MaxUploadSizeExceededException은 Spring의 DispatcherServlet이 요청을 처리하기 전에, 즉 컨트롤러의 메소드가 호출되기 전에 발생합니다. 이 때문에 일반적인 @RestController 내의 @ExceptionHandler로는 이 예외를 잡아낼 수 없습니다. 이 문제를 해결하기 위해 전역 예외 처리기인 @ControllerAdvice 또는 @RestControllerAdvice를 사용해야 합니다.

@ControllerAdvice를 이용한 전역 예외 처리

@RestControllerAdvice 애너테이션을 사용하여 모든 컨트롤러에서 발생하는 예외를 한 곳에서 처리하는 클래스를 만들 수 있습니다. 다음은 MaxUploadSizeExceededException을 잡아내어 사용자에게 친절한 JSON 응답을 보내는 예제입니다.

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.multipart.MaxUploadSizeExceededException;

import java.util.HashMap;
import java.util.Map;

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(MaxUploadSizeExceededException.class)
    public ResponseEntity<Map<String, Object>> handleMaxUploadSizeExceededException(
            MaxUploadSizeExceededException ex) {
        
        Map<String, Object> response = new HashMap<>();
        response.put("status", "error");
        response.put("message", "파일 업로드 크기가 제한을 초과했습니다.");
        
        // application.yml/properties 에서 설정한 파일 크기 제한 정보를 가져와서 보여줄 수도 있습니다.
        // ex.getPermittedSize() 등을 활용 가능.
        
        return new ResponseEntity<>(response, HttpStatus.PAYLOAD_TOO_LARGE); // 413 Payload Too Large
    }
    
    // 다른 전역 예외 처리...
}

이제 사용자가 제한 크기를 초과하는 파일을 업로드하면, 무미건조한 500 에러 페이지 대신 다음과 같은 명확한 JSON 응답과 함께 HTTP 413 Payload Too Large 상태 코드를 받게 됩니다. 프론트엔드에서는 이 응답을 파싱하여 사용자에게 "파일 크기는 200MB를 초과할 수 없습니다."와 같은 구체적인 안내 메시지를 보여줄 수 있습니다.


{
  "status": "error",
  "message": "파일 업로드 크기가 제한을 초과했습니다."
}

한 걸음 더: 성능과 메모리 최적화를 위한 스트리밍 업로드

지금까지의 방법은 파일 업로드가 완료되면 Spring이 해당 파일을 서버의 메모리나 임시 디스크 공간에 완전히 저장한 후, 컨트롤러로 MultipartFile 객체를 넘겨주는 방식입니다. 이 방식은 수백 MB 정도의 파일에는 효과적이지만, 수십 GB를 넘어가는 진정한 의미의 '초 대용량' 파일을 다루기에는 부적합합니다. 파일 전체를 메모리나 디스크에 한 번에 올리는 과정에서 심각한 성능 부하와 메모리 부족(OutOfMemoryError)을 유발할 수 있기 때문입니다.

MultipartFile의 한계와 OutOfMemoryError

MultipartFile을 사용하는 표준 방식의 데이터 흐름은 다음과 같습니다.
`Client` -> `Servlet Container(Tomcat)` -> `메모리/임시 디스크` -> `MultipartFile` -> `Controller`
여기서 병목은 메모리/임시 디스크 단계입니다. 아무리 디스크에 쓴다고 해도, 처리 과정에서 많은 리소스가 소모되며 여러 사용자가 동시에 대용량 파일을 업로드하면 시스템 전체가 불안정해질 수 있습니다.

스트림 직접 다루기: HttpServletRequest의 InputStream 활용

이러한 한계를 극복하기 위한 방법은 바로 **스트리밍(Streaming)** 처리입니다. 즉, 업로드되는 파일 데이터를 큰 덩어리로 한 번에 받는 것이 아니라, 들어오는 대로 잘게 나누어진 '스트림' 형태로 즉시 읽어서 처리하는 것입니다. 이 방법을 사용하면 파일 전체를 서버에 저장하지 않고도 데이터 처리가 가능해져 메모리와 디스크 사용량을 획기적으로 줄일 수 있습니다.

스트리밍 업로드를 구현하려면, Spring의 자동 멀티파트 처리를 비활성화하고, `HttpServletRequest`에서 직접 `InputStream`을 얻어와야 합니다.

  1. 멀티파트 처리 비활성화: application.yml에서 `spring.servlet.multipart.enabled`를 `false`로 설정합니다. 이렇게 하면 Spring이 파일 데이터를 가로채서 `MultipartFile`로 변환하는 과정을 건너뜁니다.
    spring:
      servlet:
        multipart:
          enabled: false # 스트리밍 처리를 위해 비활성화
    
  2. Controller에서 InputStream 직접 읽기:
    import org.springframework.http.ResponseEntity;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.PostMapping;
    
    import javax.servlet.http.HttpServletRequest;
    import java.io.InputStream;
    import java.io.OutputStream;
    import java.nio.file.Files;
    import java.nio.file.Paths;
    import java.nio.file.StandardCopyOption;
    
    @Controller
    public class StreamingUploadController {
    
        @PostMapping("/upload/stream")
        public ResponseEntity<String> handleStreamingUpload(HttpServletRequest request) {
            String destinationPath = "/data/uploads/large-file.dat";
    
            try (InputStream inputStream = request.getInputStream()) {
                // 파일을 원하는 위치에 저장합니다.
                // Files.copy는 내부적으로 버퍼를 사용하여 효율적으로 스트림을 복사합니다.
                Files.copy(inputStream, Paths.get(destinationPath), StandardCopyOption.REPLACE_EXISTING);
                return ResponseEntity.ok("파일 스트리밍 업로드 성공!");
    
            } catch (Exception e) {
                // 스트리밍 중 오류 처리
                e.printStackTrace();
                return ResponseEntity.internalServerError().body("스트리밍 업로드 실패: " + e.getMessage());
            }
        }
    }
    

이 방식은 Amazon S3와 같은 객체 스토리지로 파일을 직접 스트리밍할 때 특히 유용합니다. 서버의 디스크를 거치지 않고 `Client -> Application Server -> S3`로 데이터가 파이프라이닝되어 매우 효율적인 아키텍처를 구성할 수 있습니다.

고려사항: 복잡성과의 트레이드오프

스트리밍 업로드는 강력하지만, 구현의 복잡성이 증가하는 단점이 있습니다. MultipartFile이 제공하는 파일 이름, 콘텐츠 타입, 크기 등의 메타데이터를 직접 파싱해야 할 수도 있으며, 오류 처리 로직도 더 정교해져야 합니다. 따라서 모든 파일 업로드에 스트리밍을 적용하기보다는, 서비스가 다루는 파일의 평균 및 최대 크기를 고려하여 '일반 업로드(MultipartFile)'와 '대용량 스트리밍 업로드'를 위한 API를 분리하여 제공하는 것이 현명한 전략일 수 있습니다.


서버와 클라이언트, 함께 고려해야 할 것들

임베디드 서버별 특징

Spring Boot는 기본적으로 Tomcat을 내장 서버로 사용합니다. 대부분의 경우 `spring.servlet.multipart.*` 설정만으로 충분하지만, 극단적인 경우 내장 서버의 고유한 설정을 알아두면 도움이 될 수 있습니다. 예를 들어 Tomcat에는 max-swallow-size라는 프로퍼티가 있는데, 이는 업로드 크기 제한을 초과하는 요청의 나머지 부분을 얼마나 더 읽어들일지를 결정합니다. 클라이언트가 연결을 갑자기 끊는 것을 방지하기 위함인데, Spring Boot 2.x에서는 max-request-size에 맞춰 이 값을 자동으로 조정해주므로 대부분 신경 쓸 필요가 없습니다.

프론트엔드: 청크 업로드와 진행 상태 표시

대용량 파일 업로드의 사용자 경험을 극대화하려면 서버 측 개선만으로는 부족합니다. 클라이언트 측(프론트엔드)에서의 노력이 함께 필요합니다.

  • 청크 업로드(Chunked Uploads): 수 GB짜리 파일을 한 번에 전송하다가 네트워크가 불안정하여 중간에 끊기면 처음부터 다시 업로드해야 합니다. 이는 매우 끔찍한 경험입니다. 이를 해결하기 위해 파일을 클라이언트에서 잘게 조각(Chunk)내어 여러 번의 요청으로 나누어 보내는 '청크 업로드' 방식이 있습니다. 서버는 이 조각들을 받아 순서대로 합쳐서 원본 파일을 복원합니다. 이 방식을 사용하면 업로드 실패 시 실패한 조각부터 재전송이 가능하며, 이어 올리기도 구현할 수 있습니다. tus.io와 같은 오픈소스 프로토콜이나 직접 JavaScript로 구현할 수 있습니다.
  • 진행 상태 표시(Progress Bar): 대용량 파일 업로드는 시간이 오래 걸리므로, 사용자에게 현재 얼마나 진행되었는지 시각적으로 보여주는 것이 매우 중요합니다. `XMLHttpRequest`의 `progress` 이벤트를 사용하여 실시간으로 업로드 진행률을 계산하고 UI에 프로그레스 바로 표시할 수 있습니다. 이는 사용자의 불안감을 해소하고 이탈률을 줄이는 데 큰 도움이 됩니다.

보안: 용량 제한은 최고의 방어 수단이다

다시 한번 강조하지만, 파일 업로드 크기 제한은 단순히 기능적인 제약을 넘어 핵심적인 보안 장치입니다. `무제한(-1)` 설정의 유혹을 뿌리치고, 서비스의 목적에 맞는 합리적인 제한을 반드시 설정해야 합니다. 추가적으로 다음과 같은 보안 조치를 고려해야 합니다.

  • 파일 타입 검증: 사용자가 업로드한 파일의 확장자만 믿어서는 안 됩니다. 악의적인 사용자는 .exe 파일을 .jpg로 이름만 바꿔서 업로드할 수 있습니다. 파일의 실제 콘텐츠를 분석하여 MIME 타입을 확인하는 라이브러리(e.g., Apache Tika)를 사용하여 허용된 파일 형식(이미지, 문서 등)만 저장하도록 강제해야 합니다.
  • 악성코드 검사: 업로드된 파일을 서버에 저장하기 전에 바이러스나 악성코드가 있는지 검사하는 것이 안전합니다. 클라우드 기반의 바이러스 스캐닝 서비스를 연동하거나, 서버에 ClamAV와 같은 오픈소스 백신 엔진을 설치하여 검사하는 파이프라인을 구축할 수 있습니다.
  • 저장 경로 분리: 업로드된 파일은 웹 루트(사용자가 URL로 직접 접근할 수 있는 경로) 밖에 저장하여, 혹시 모를 실행 파일이 업로드되더라도 웹을 통해 직접 실행되는 것을 원천 차단해야 합니다. 파일에 접근할 때는 인증/인가를 거친 컨트롤러를 통해서만 스트리밍 방식으로 제공하는 것이 안전합니다.

최종 종합 예제: 실제 프로젝트처럼 구성하기

지금까지 논의된 내용을 종합하여, 실제 프로젝트에 바로 적용할 수 있는 파일 업로드 시스템의 전체 코드를 살펴봅니다.

1. Controller: 파일 업로드 API

import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

@RestController
@RequestMapping("/api/files")
@RequiredArgsConstructor
public class FileUploadController {

    private final FileStorageService fileStorageService;

    @PostMapping("/upload")
    public ResponseEntity<String> uploadFile(@RequestParam("file") MultipartFile file) {
        try {
            String storedFileName = fileStorageService.storeFile(file);
            // 여기서는 저장된 파일 이름을 반환하지만, 파일 접근 URL을 만들어서 반환할 수도 있다.
            return ResponseEntity.ok().body("파일 업로드 성공: " + storedFileName);
        } catch (Exception e) {
            return ResponseEntity.badRequest().body("파일 업로드 실패: " + e.getMessage());
        }
    }
}

2. Service: 파일 저장 로직

import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.UUID;

@Service
public class FileStorageService {

    private final Path fileStorageLocation;
    
    // 파일 저장 경로 설정
    public FileStorageService() {
        // 실제 운영 환경에서는 application.properties 등에서 경로를 읽어오는 것이 좋습니다.
        this.fileStorageLocation = Paths.get("./uploads").toAbsolutePath().normalize();

        try {
            Files.createDirectories(this.fileStorageLocation);
        } catch (Exception ex) {
            throw new RuntimeException("파일을 업로드할 디렉토리를 생성하지 못했습니다.", ex);
        }
    }

    public String storeFile(MultipartFile file) {
        String originalFileName = StringUtils.cleanPath(file.getOriginalFilename());
        String extension = StringUtils.getFilenameExtension(originalFileName);
        String storedFileName = UUID.randomUUID().toString().replace("-", "") + "." + extension;

        try {
            if (originalFileName.contains("..")) {
                throw new RuntimeException("파일명에 부적합한 문자가 포함되어 있습니다 " + originalFileName);
            }

            Path targetLocation = this.fileStorageLocation.resolve(storedFileName);
            Files.copy(file.getInputStream(), targetLocation, StandardCopyOption.REPLACE_EXISTING);

            return storedFileName;
        } catch (IOException ex) {
            throw new RuntimeException("파일 " + originalFileName + "을 저장하지 못했습니다. 다시 시도해 주세요.", ex);
        }
    }
}

3. Configuration: application.yml

spring:
  servlet:
    multipart:
      enabled: true
      max-file-size: 200MB # 개별 파일 최대 200MB
      max-request-size: 210MB # 요청 전체 최대 210MB (파일 외 다른 데이터 고려)

# 서버 포트 등 기타 설정
server:
  port: 8080

4. Exception Handler: 전역 예외 처리기

(앞서 소개한 `GlobalExceptionHandler` 클래스를 프로젝트에 포함시킵니다.)

마치며: 안정적인 파일 서비스를 향하여

Spring Boot에서 대용량 파일 업로드를 처리하는 것은 단순히 설정값 몇 개를 바꾸는 것 이상의 의미를 가집니다. 이는 애플리케이션의 성능, 안정성, 보안, 그리고 사용자 경험과 직결되는 중요한 문제입니다. 오늘 다룬 내용들—기본적인 크기 설정부터 프로그래밍 방식의 동적 제어, 우아한 예외 처리, 그리고 극한의 성능을 위한 스트리밍 기법까지—을 깊이 이해하고 프로젝트의 요구사항에 맞게 적절히 조합하여 사용한다면, 사용자가 어떤 크기의 파일을 업로드하더라도 흔들림 없는 안정적인 파일 서비스를 구축할 수 있을 것입니다. 기억하십시오. 최고의 시스템은 모든 예외적인 상황을 미리 예측하고 대비하는 것에서부터 시작됩니다.


0 개의 댓글:

Post a Comment