체험일지 이미지 등록 성능 개선 – Presigned URL (1)

·2025년 5월 13일
0
post-thumbnail

이번 글에서는 체험일지 이미지 업로드 기능의 성능을 개선하기 위해 S3 Presigned URL을 도입한 과정을 소개합니다.

현재 상황

저희 숨비소리 서비스에서는 사용자가 해녀 체험 후 사진과 후기를 남기는 '체험일지' 기능을 운영 중입니다.
최대 10장의 이미지를 업로드할 수 있으며, 작성 화면은 다음과 같습니다.

스마트폰 카메라 성능의 발전으로 인해 고화질 이미지는 보통 3MB ~ 5MB 이상의 크기를 가지게 되었습니다.

즉, 한 체험 일지에 포함되는 이미지의 최대 용량은 30MB ~ 50MB 이상이 될 수 있어 업로드 처리에 큰 부담이 발생합니다.

이렇게 고용량 이미지 파일들을 여러 장 업로드하는 경우, 사용자는 모든 파일이 업로드될 때까지 기다려야 하기 때문에 보다 빠르고 안정적인 업로드 방식이 필요해졌습니다.


이미지 업로드 방식

MultiPartFile

기존에는 체험일지 등록 API에서 사용자가 선택한 이미지 파일들을 multipart/form-data 형식으로 서버에 한번에 전송하고, 서버에서 이를 AWS S3에 업로드하는 방식이었습니다.

기존 이미지 업로드 시 서버 처리 과정

Spring에서 이미지 업로드는 주로 MultipartFile 인터페이스를 통해 처리됩니다.
공식 JavaDoc에서는 다음과 같은 설명이 포함되어 있습니다.

“The file contents are either stored in memory or temporarily on disk.
The temporary storage will be cleared at the end of request processing.”

즉, 파일 크기에 따라 서버는 업로드된 이미지 파일을 서버의 메모리 또는 임시 디스크에 저장을 합니다.

흐름
1. 클라이언트가 이미지 파일을 multipart/form-data 형식으로 서버에 전송
2. 서버가 MultipartFile로 파싱하여 메모리 또는 임시 디스크에 저장
3. 서버가 해당 파일들을 AWS S3에 업로드


📦 1GB × 4 파일 동시 업로드 테스트 결과

K6를 이용해 1GB 크기의 파일 4개를 multipart/form-data 방식으로 동시에 업로드 요청하였고, Prometheus + Grafana 기반 모니터링 환경에서 다음과 같은 성능 지표를 수집하였습니다.

1. 임시 디스크 사용량 증가

업로드 전과 업로드 중의 루트 디스크(/tmp 포함) 사용량을 비교한 결과, 7.5G -> 15G 7.5GB의 디스크 공간이 추가로 사용된 것을 확인할 수 있었습니다.
이는 MultipartFile 처리 과정에서 업로드된 파일이 임시 디스크에 저장되기 때문이며, 대용량 파일 업로드 시 디스크 사용량 증가에 주의가 필요합니다.

업로드 전

업로드 중

💡 Tip: 디스크 공간이 부족해지면 IOException: No space left on device 예외가 발생하며 업로드가 실패할 수 있습니다. 디스크 모니터링 또는 용량 설정 조정이 필수입니다.

2. CPU 사용률

  • Prometheus 메트릭: process_cpu_usage, system_cpu_usage
  • 평균 70~80% 수준까지 상승

3. GC 일시 정지 시간

  • Prometheus 메트릭: jvm_gc_pause_seconds_max
  • 업로드 중 최대 1.4초의 GC 일시 정지 발생

4. GC 발생 빈도

  • Prometheus 메트릭: rate(jvm_gc_pause_seconds_count)
  • 최대 0.2 ops (초당 발생 횟수) 기록

5. 응답 시간

  • K6 테스트 기준
  • 평균 2분 36초, 최대 2분 46초

Multipart 방식 단점

  • 서버 부하 증가
    클라이언트가 전송한 대용량 파일을 서버가 직접 수신하고 저장하기 때문에, CPU 사용률 상승 및 GC 일시 정지 등 서버 자원이 급격히 소모될 수 있습니다.
  • 임시 디스크 공간 사용 리스크
    업로드된 파일은 처리 전 메모리에 저장되며, 파일 크기가 크거나 메모리가 부족한 경우 임시 디스크(기본적으로 /tmp)로 저장 위치가 변경됩니다. 이때 동시에 다수의 대용량 파일이 업로드될 경우 디스크 공간이 부족해져 업로드가 실패할 수 있습니다.
  • 응답 시간 지연
    서버가 파일 저장 및 외부 저장소(S3 등) 업로드까지 모두 수행하기 때문에, 클라이언트 응답까지의 시간이 길어질 수 있습니다.

현재 문제는 이미지가 서버의 임시 디스크에 저장되며, 서버 자원에 직접적인 부하를 준다는 점입니다.
그렇다면, "이미지를 굳이 임시 디스크에 저장하지 않으면 되지 않을까?” 라는 의문이 생겼고, 그 대안으로 Stream 방식을 고려해보았습니다.


Stream 업로드 방식

Stream 방식은 HttpServletRequest로부터 InputStream을 이용하여,바로 외부 저장소(S3 등)로 전송하는 방식입니다.
이 방식의 가장 큰 특징은 업로드된 파일의 바이너리 전체를 서버의 디스크나 힙 메모리에 저장하지 않는다는 점입니다.
(즉, 전체 파일을 한 번에 메모리에 올리지 않는다는 의미입니다.)

다만 업로드 과정에서 파일의 일부 청크는 메모리에 순차적으로 로드되기 때문에, 일정 수준의 메모리 사용은 발생합니다.
또한 메모리에 바이너리 전체를 로드하지 않는 이상, 이미지 리사이징과 같은 전처리 작업은 불가능합니다.

해당 방식은 Multipart 방식과 달리, 한 번의 요청에 하나의 파일만 업로드할 수 있습니다.
이는 요청 본문이 단순한 바이너리 스트림으로 구성되기 때문에, 여러 파일을 구분할 수 있는 명확한 경계(boundary)가 존재하지 않기 때문입니다.


장단점

장점

  • 서버 자원 사용 감소
    Multipart 방식과 달리 파일 전체를 서버의 자원에 저장하지 않기때문에, 서버의 자원 소모가 훨씬 적습니다. 디스크 부족이나 GC 일시 정지와 같은 문제를 피할 수 있습니다.

  • 구현 복잡성이나 업로드 복잡성이 낮음

단점

  • 클라이언트의 네트워크 상태에 따라 업로드 속도 편차가 큼
    업로드 속도가 클라이언트 환경에 크게 영향을 받으며, 느린 네트워크 환경에서는 업로드 시간이 매우 길어질 수 있습니다.

  • 서버에서 이미지 전처리 작업 불가
    전체 파일을 메모리에 로드하지 않기 때문에, 서버에서 리사이징 등의 전처리 작업을 수행할 수 없습니다.

  • 여러개의 파일 업로드 불가
    요청 본문이 단일 바이너리 스트림으로 구성되므로, 하나의 요청에서는 하나의 파일만 업로드할 수 있습니다.

  • 대용량 파일 업로드 시 사용자 대기 시간이 길고, 오류 발생 시 전체 재전송 필요
    업로드 중 오류가 발생하면 중단된 지점부터 이어받기가 불가능해 처음부터 다시 업로드해야 하므로, 시간과 대역폭이 낭비됩니다.

  • 클라이언트에게 업로드 진행 상황 제공 불가능
    업로드 완료 전까지 응답이 없어, 클라이언트가 업로드 진행 상황을 확인할 수 없습니다.


Multipart 및 Stream 방식의 공통적인 한계

Stream 방식과 Multipart 방식 모두 파일 업로드를 서버가 직접 처리하는 구조입니다.
이러한 구조에서는 요청이 들어오는 동안 서버 스레드가 점유되기 때문에, 다수의 사용자로부터 동시에 업로드 요청이 몰릴 경우, 서버의 스레드 풀이 빠르게 고갈될 수 있습니다.
이에 따라 스레드 풀 설정이 적절하지 않으면, 업로드 도중 타임아웃이 발생하거나 서버가 다른 요청을 처리하지 못하는 상황으로 이어질 수 있습니다.


앞서 살펴본 Multipart 방식과 Stream 방식 모두 공통적으로 서버가 직접 업로드를 처리해야 하기 때문에, 서버 자원 사용량 증가와 요청 처리 병목이라는 구조적인 한계를 가지고 있습니다.

이러한 문제를 해결하기 위해, 클라이언트가 직접 S3에 이미지를 업로드할 수 있도록 하는 AWS S3 Presigned URL 방식이 있습니다.

다음 글에서는 AWS S3 Presigned URL의 동작 원리와 함께, 실제로 프로젝트에서 어떻게 적용했는지, 그리고 이를 통해 업로드 구조를 어떻게 개선했는지를 다뤄보겠습니다.

0개의 댓글