Skip to content

hasune/spirng-web-mvc-heavy-upload

Repository files navigation

대용량 파일 업로드 진행률 표시 POC

License: MIT Spring Boot Java Kafka Docker

대용량 파일 업로드 시 실시간 진행률을 표시하는 Spring Boot + Kafka 기반 POC(Proof of Concept) 프로젝트입니다.

프로젝트 개요

이 프로젝트는 Spring Boot를 사용하여 대용량 파일을 업로드하면서 사용자에게 실시간으로 업로드 진행 상황을 보여주는 기능을 구현한 POC입니다. Kafka를 활용하여 업로드 진행 상황을 클라이언트에 전달하며, Server-Sent Events(SSE)를 통해 실시간으로 진행률을 표시합니다.

기능 요약

  • 대용량 파일(최대 5GB) 업로드 지원
  • Kafka를 통한 실시간 업로드 진행률 전송
  • SSE를 활용한 클라이언트 진행률 표시
  • 다양한 파일 업로드 방식 비교 및 성능 테스트

주요 기술 스택

  • Backend: Spring Boot 3.4.5, Java 17
  • Frontend: HTML, CSS, JavaScript
  • 메시징: Apache Kafka
  • 템플릿 엔진: Mustache
  • 컨테이너화: Docker, Docker Compose

업로드 방식 비교

이 프로젝트는 네 가지 다른 방식의 파일 업로드 방법을 구현하여 성능을 비교했습니다:

  1. 기본 MultipartFile 방식 (/upload): 전체 파일을 메모리/디스크에 한 번에 로드하는 방식

    • 10명 동시에 4.8GB 파일 요청 시 모든 요청이 5분 이상 소요됨
    • 동시 요청이 몰릴 경우 서버에 부담이 크고 성능이 저하됨
  2. InputStream 청크 단위 처리 방식 (/upload/stream): 스트림을 바로 읽어서 청크 단위로 디스크에 저장

    • 기본 방식보다 약간 개선되어 4분 40초 정도로 단축됨
    • 메모리 사용량 측면에서 1번 방식보다 효율적
  3. Java NIO 기반 스트리밍 방식 (/upload/nio): NIO Channel을 활용한 스트리밍 저장

    • 비동기 WebFlux를 사용하지 않거나, 프론트에서 짤라보내지 않는 방식으로 했을 경우 대안
    • DirectBuffer를 활용하여 네이티브 I/O 성능 개선
  4. 클라이언트 청크 분할 업로드 방식 (/upload/chunk): 파일을 클라이언트에서 작은 청크로 나누어 순차적으로 업로드

    • 10MB 크기의 청크로 분할하여 순차적으로 전송
    • 각 청크 별로 진행 상황을 추적하고 최종적으로 서버에서 병합
    • 대용량 파일 처리에 가장 효율적이며 네트워크 오류 발생 시 복원 가능성 향상

실시간 진행률 처리 아키텍처

  1. 클라이언트가 파일과 고유 업로드 ID를 서버에 전송
  2. 서버는 파일을 저장하면서 진행률을 계산하여 Kafka에 메시지 전송
  3. Kafka 컨슈머가 진행률 메시지를 구독
  4. SSE를 통해 클라이언트에 실시간으로 진행률 전달
  5. 클라이언트는 진행률을 프로그레스 바로 시각화

병목 현상 및 성능 고려 사항

대용량 파일 업로드 시 발생하는 주요 병목 현상과 이를 해결하기 위한 방법:

주요 병목 지점

  1. MultipartResolver 처리 지연:

    • Spring의 MultipartResolver는 요청을 받으면 먼저 전체 파일을 임시 디렉토리에 저장한 후 컨트롤러로 전달합니다.
    • 대용량 파일(5GB)이나 여러 파일이 동시에 들어오면 임시 파일 생성 과정에서 디스크 I/O 경합이 발생합니다.
    • 이 과정에서는 아직 컨트롤러 코드가 실행되지 않기 때문에 로그가 찍히지 않고 서버가 응답하지 않는 것처럼 보입니다.
  2. Tomcat의 요청 처리 큐:

    • 기본 Tomcat 설정에서는 동시 처리 가능한 요청 수가 제한되어 있습니다.
    • 다수의 대용량 파일 업로드 요청이 들어오면 일부는 큐에서 대기하게 됩니다.
  3. 디스크 I/O 병목:

    • 대용량 파일을 디스크에 쓰는 과정에서 디스크 I/O가 병목 지점이 될 수 있습니다.
    • 특히 여러 파일을 동시에 처리할 때 성능 저하가 두드러집니다.

성능 개선 방안

  1. 클라이언트 측 청크 분할 업로드:

    • 파일을 작은 청크로 나누어 순차적으로 업로드하면 메모리 사용량 감소
    • 각 청크별 진행 상황 추적 및 실패 시 해당 청크만 재시도 가능
  2. 서버 설정 최적화:

    server:
      tomcat:
        max-threads: 50                  # 동시 처리 스레드 수 증가
        max-connections: 100             # 최대 연결 수 증가
        accept-count: 20                 # 처리 대기 큐 크기
        connection-timeout: 20000        # 연결 타임아웃(ms)
        max-swallow-size: -1             # 요청 본문이 너무 클 경우 처리 방식
        max-http-form-post-size: 5242880000  # 최대 POST 크기 (5GB)
  3. 비동기 처리 적용:

    • Spring의 비동기 처리 기능을 사용하여 요청 처리 성능 향상
    @PostMapping("/upload/nio")
    public CompletableFuture<ResponseEntity<?>> uploadWithNio(
            @RequestParam("file") MultipartFile file,
            @RequestParam("uploadId") String uploadId) {
        return CompletableFuture.supplyAsync(() -> {
            fileUploadService.saveFileWithNioStreaming(file, uploadId);
            return ResponseEntity.ok().build();
        });
    }
  4. 로깅 개선:

    • 요청 처리 단계별 로깅으로 병목 지점 파악
    logging:
      level:
        org.springframework.web: DEBUG
        org.apache.tomcat.util.http.fileupload: DEBUG
  5. 업로드 인터셉터 추가:

    • MultipartResolver 단계에서 로그를 확인하기 위한 인터셉터 구현
    @Component
    public class FileUploadInterceptor implements HandlerInterceptor {
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
            if (request.getContentType() != null && request.getContentType().contains("multipart/form-data")) {
                System.out.println("⏱️ 파일 업로드 요청 수신 시작: " + request.getRequestURI());
            }
            return true;
        }
        
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
            if (request.getContentType() != null && request.getContentType().contains("multipart/form-data")) {
                System.out.println("⏱️ 파일 업로드 요청 처리 완료: " + request.getRequestURI());
            }
        }
    }

부하 테스트 방법 및 결과

테스트 환경 구성

  • 테스트 파일: fallocate -l 5G test_5gb_file.bin 명령어로 5GB 더미 파일 생성
  • 병렬 업로드 테스트를 위한 bash 스크립트 사용

테스트 스크립트 (bulk_upload_test.sh)

#!/bin/bash
# === 설정 ===
FILE="./test_5gb_file.bin"        # 업로드할 파일 경로
USERS=10                          # 동시 사용자 수
UPLOAD_URL="http://localhost:8080/upload"
# === 동시 업로드 함수 ===
upload_file() {
  local id=$(uuidgen)
  echo "🚀 [$id] 업로드 시작"
  
  curl -s -o /dev/null -w "✅ [$id] %{http_code} (%{time_total}s)\n" \
    -F "file=@${FILE}" \
    -F "uploadId=${id}" \
    "${UPLOAD_URL}"
}
# === 병렬 실행 ===
echo "⚙️ ${USERS}명의 사용자로 병렬 업로드 시작..."
for i in $(seq 1 $USERS); do
  upload_file &
done
wait
echo "🎉 모든 업로드 요청 완료!"

병렬 업로드 성능 개선 스크립트

동시 업로드 요청 간에 지연을 추가하여 서버 부하 분산:

#!/bin/bash
FILE="./test_5gb_file.bin"
USERS=10
UPLOAD_URL="http://localhost:8080/upload/nio"  # NIO 방식 사용

echo "⚙️ ${USERS}명의 사용자로 병렬 업로드 시작 (지연 적용)..."

for ((i=1; i<=USERS; i++)); do
  (
    UUID=$(uuidgen)
    echo "🚀 [$UUID] 업로드 시작"
    
    # 약간의 지연 추가 (0-3초)
    DELAY=$(( RANDOM % 3 ))
    sleep $DELAY
    
    curl -s -o /dev/null -w "✅ [$UUID] %{http_code} (%{time_total}s)\n" \
      -F "file=@${FILE}" \
      -F "uploadId=${UUID}" \
      "${UPLOAD_URL}"
  ) &
  
  # 각 요청 간 짧은 간격 추가 (0.5초)
  sleep 0.5
done

wait
echo "🎉 모든 업로드 요청 완료!"

주요 테스트 결과

  1. 기본 방식 (/upload)

    • 10명 동시 접속 시: 모든 요청이 평균 5분 이상 소요
    • 메모리 사용량 급증으로 OOM 발생 가능성 있음
  2. 스트림 방식 (/upload/stream)

    • 10명 동시 접속 시: 평균 4분 40초로 소폭 개선
    • 메모리 사용량이 안정적이나 획기적인 성능 향상은 없음
  3. NIO 스트리밍 방식 (/upload/nio)

    • I/O 처리 효율성 개선으로 CPU 사용률 감소
    • 대용량 파일 처리 시 메모리 효율성 향상
  4. 클라이언트 청크 분할 방식 (/upload/chunk)

    • 작은 청크 단위로 전송하여 메모리 사용량 최소화
    • 업로드 진행 상황을 더 세밀하게 추적 가능
    • 각 청크별 독립적인 처리로 오류 복원력 향상

안전한 파일 저장 구현

파일 업로드 과정에서 발생할 수 있는 디스크 공간 부족이나 권한 문제를 방지하기 위한 안전 장치:

  1. 홈 디렉토리 기반 파일 저장:

    • 사용자 홈 디렉토리를 기준으로 일관된 경로에 파일 저장
    • 날짜 및 업로드 ID별 구조화된 디렉토리 구성으로 관리 용이성 향상
  2. 디스크 공간 사전 확인:

    • 업로드 전 필요한 디스크 공간 계산 및 확인
    • 공간 부족 시 명확한 오류 메시지 제공
  3. 권한 관리 자동화:

    • 디렉토리 생성 시 쓰기 권한 자동 확인 및 설정
    • POSIX 및 비-POSIX(Windows) 시스템 모두 지원
  4. 임시 파일 자동 정리:

    • 업로드 완료 후 Spring이 생성한 임시 파일 자동 정리
    • 디스크 공간 효율적 사용

개선 및 확장 방향

  1. WebFlux 및 리액티브 스트림 활용

    • Spring WebFlux를 도입하여 비동기적 파일 처리
    • 높은 동시성 처리를 위한 리액티브 프로그래밍 적용
  2. 클라이언트 청크 업로드 고도화

    • 병렬 청크 업로드 기능 추가
    • 업로드 중단/재개 기능 구현
  3. 분산 파일 저장소 연계

    • S3, MinIO 등의 객체 스토리지와 연동
    • 업로드 후처리 워크플로우 구현 (압축, 변환, 분석 등)
  4. 백그라운드 처리 개선

    • 파일 저장과 메타데이터 처리를 비동기적으로 분리
    • 멀티파트 업로드 완료 후 백그라운드에서 후처리 작업 수행

로컬 개발 환경 구성

사전 요구사항

  • Java 17
  • Docker 및 Docker Compose

실행 방법

  1. Kafka 환경 실행:
docker-compose up -d
  1. 애플리케이션 빌드 및 실행:
./gradlew bootRun
  1. 웹 브라우저에서 접속:
http://localhost:8080/

결론

이 POC 프로젝트는 대용량 파일 업로드 시 실시간 진행률을 제공하는 방법과 다양한 업로드 처리 방식의 성능을 비교했습니다. 각 방식은 장단점이 있으며, 대용량 파일 처리와 동시 접속 처리 관점에서 클라이언트 청크 분할 방식이 가장 효율적인 것으로 확인되었습니다.

또한 Spring의 MultipartResolver 처리 과정에서 발생하는 병목 현상을 발견하고, 이를 해결하기 위한 여러 방안을 제시했습니다. 대용량 파일 업로드 시스템 구축 시 고려해야 할 주요 요소로는 사용자 경험(실시간 진행률), 시스템 리소스 관리(메모리, 디스크 I/O), 안정성(오류 복구 메커니즘)이 있으며, 이 POC는 이러한 요소들을 종합적으로 고려한 참조 아키텍처를 제공합니다.

About

Spring web MVC - 대량파일 업로드의 여러가지 방식과 진행율 표현

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors