S3 이미지 업로드 성능 트러블슈팅 ‐ Presigned URL 도입 - fitpassTeam/fitpass GitHub Wiki

S3 이미지 업로드 성능 트러블슈팅 - Presigned URL 도입

문제 상황

발생한 문제

유저 이미지를 업로드 및 체육관 이미지 업로드하는 로직에서 이미지 업로드 성능 저하서버 리소스 비효율성 문제가 발생

증상 및 현상

  • 업로드 속도 저하: 클라이언트 → 서버 → S3의 이중 전송으로 인한 지연
  • 서버 리소스 과다 사용: CPU, 메모리, 네트워크 대역폭의 불필요한 소모
  • 확장성 문제: 동시 업로드 시 서버 부하 급증
  • 병목 현상: 서버 인스턴스의 대역폭이 업로드 속도 제한 요소로 작용

발생 시점

사용자 증가와 함께 이미지 업로드 빈도가 높아지면서 성능 이슈가 점진적으로 드러남

원인 분석

기존 아키텍처의 문제점

image

주요 원인:

  1. 불필요한 중계 구조: 서버가 단순히 이미지 데이터를 S3로 전달하는 역할만 수행
  2. 이중 네트워크 전송: 클라이언트→서버, 서버→S3 두 번의 전송 과정
  3. 서버 리소스 낭비: 파일 버퍼링과 전송을 위한 CPU, 메모리 사용
  4. 동시성 한계: 서버의 처리 능력에 따른 업로드 수 제한

성능 측정 결과 (문제 상황)

  • 서버 처리 시간: 210ms (파일 수신 + S3 업로드)
  • HTTP 요청 시간: 126ms
  • 서버 부하: 높음 (CPU, 메모리, 네트워크 대역폭)

해결 과정

1단계: 문제 정의 및 가설 수립

가설: "S3 Presigned URL을 사용하면 서버 부하를 줄이면서 업로드 성능을 개선할 수 있을 것이다"

2단계: 해결 방안 검토

선택한 기술: S3 Presigned URL

선택 이유:

  • AWS Native Solution으로 추가 인프라 불필요
  • 임시 URL을 통한 보안적 업로드
  • 클라이언트의 AWS 글로벌 네트워크 직접 접근
  • 기존 시스템과의 호환성 유지

3단계: 구현

아키텍처 변경: image

핵심 구현 코드:

S3Service 확장

// 새로운 Presigned URL 방식 추가
public String generatePresignedUrl(String originalFilename, String contentType) {
    // 파일 형식 검증
    if (!isAllowedFileType(originalFilename)) {
        throw new BaseException(ExceptionCode.INVALID_FILE_TYPE);
    }

    String fileName = UUID.randomUUID().toString() + getExtension(originalFilename);
    Date expiration = new Date(System.currentTimeMillis() + 5 * 60 * 1000); // 5분

    GeneratePresignedUrlRequest request = new GeneratePresignedUrlRequest(bucketName, fileName)
        .withMethod(HttpMethod.PUT)
        .withExpiration(expiration)
        .withContentType(contentType);

    return amazonS3.generatePresignedUrl(request).toString();
}

private boolean isAllowedFileType(String filename) {
    String[] allowedExtensions = {".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp"};
    return Arrays.stream(allowedExtensions)
            .anyMatch(filename.toLowerCase()::endsWith);
}

Controller API 추가

@GetMapping("/presigned-url")
public ResponseEntity<ResponseMessage<Map<String, String>>> getPresignedUrl(
    @RequestParam("filename") String filename,
    @RequestParam("contentType") String contentType
) {
    String presignedUrl = s3Service.generatePresignedUrl(filename, contentType);
    String fileName = s3Service.extractFileNameFromUrl(presignedUrl);

    Map<String, String> response = new HashMap<>();
    response.put("presignedUrl", presignedUrl);
    response.put("fileName", fileName);
    response.put("contentType", contentType);
    response.put("expiresIn", "300");

    return ResponseEntity.status(SuccessCode.S3_PRESIGNED_URL_GENERATED.getHttpStatus())
        .body(ResponseMessage.success(SuccessCode.S3_PRESIGNED_URL_GENERATED, response));
}

S3 CORS 설정

[{
    "AllowedHeaders": ["*"],
    "AllowedMethods": ["GET", "PUT", "POST", "DELETE", "HEAD"],
    "AllowedOrigins": ["*"],
    "ExposeHeaders": ["ETag"]
}]

4단계: 테스트 및 검증

  • HTML 테스트 페이지를 통한 직접 비교 테스트
  • 브라우저 네트워크 탭과 서버 로그를 통한 성능 측정
  • 기존 API와의 호환성 확인

해결 결과

성능 개선 결과

측정 관점 기존 방식 Presigned URL 개선율
서버 처리 시간 210ms 3ms 70배 빠름
HTTP 요청 시간 126ms 12ms 10배 빠름
서버 부하 높음 매우 낮음 대폭 감소

실제 서버 로그

  • 기존 방식: 210ms (파일 수신 + S3 업로드 포함)
  • Presigned URL 생성: 3ms (URL 생성만)

실제 네트워크 로그

주요 개선 효과

  1. 성능 향상

    • 서버 처리 시간: 210ms → 3ms
    • 클라이언트가 AWS 글로벌 네트워크로 직접 업로드
  2. 서버 리소스 절약

    • CPU: 이미지 처리 부하 제거
    • 메모리: 파일 버퍼링 불필요
    • 네트워크: 서버 대역폭 사용량 최소화
  3. 확장성 개선

    • 동시 업로드 수 제한 해제
    • 서버 확장 없이도 더 많은 업로드 처리 가능
  4. 시스템 안정성

    • 이미지 업로드가 다른 API에 미치는 영향 최소화
    • 전체 시스템 응답성 개선

트러블슈팅 과정에서 겪은 어려움

발생한 이슈들

  1. CORS 설정 문제

    • 문제: S3 CORS 설정에서 PUT 메서드 누락으로 업로드 실패
    • 해결: CORS 설정에 PUT 메서드 추가 및 적절한 헤더 설정
  2. 파일 형식 검증 누락

    • 문제: 초기 구현에서 파일 형식 검증 부재
    • 해결: isAllowedFileType() 메서드로 허용된 이미지 확장자만 업로드 가능하도록 제한
  3. Presigned URL 만료 시간 설정

    • 문제: 적절한 만료 시간 설정의 고민
    • 해결: 5분으로 설정하여 보안과 사용성의 균형 확보

추가 개선 방안

현재 미흡한 부분

에러 처리 강화

  • 네트워크 실패, S3 장애 등 예외 상황 대응 로직 보완 필요
  • 클라이언트 측 업로드 실패 시 재시도 메커니즘 구현

핵심 학습 포인트

문제 해결 접근법

  1. 아키텍처 관점에서의 접근: 단순 튜닝보다 시스템 구조 개선이 더 효과적
  2. 점진적 도입 전략: 기존 API 유지하며 새 방식 병행 적용으로 안정성 확보
  3. AWS 관리형 서비스 활용: 기존 인프라 내에서 새로운 기능 발견 및 활용

성능 최적화 원칙

  1. 불필요한 중계 제거: "이 작업을 정말 서버가 해야 할까?" 라는 근본적 질문
  2. 측정 기반 개선: 체감이 아닌 실제 수치로 개선 효과 검증
  3. 호환성 고려: 기존 시스템을 건드리지 않는 안전한 개선 방식 선택

향후 적용 가능한 패턴

  • 다른 파일 업로드 기능에도 동일한 패턴 적용 검토
  • 대용량 파일 처리 시 서버 부하 분산 방안으로 활용
  • 마이크로서비스 아키텍처에서 각 서비스의 책임 분리 원칙 적용