[Spring] AWS S3를 사용한 이미지 업로드

JunWoo An·2023년 12월 19일
0

스파르타코딩클럽

목록 보기
39/46
post-thumbnail

이번 챌린지 과제는 이미지 업로드 기능 구현으로 S3를 활용한 이미지 업로드 기능을 구현해보자.

AWS의 S3 버킷생성과 IAM 엑세스,시크릿키 발급에 관해선 구글검색을 통해 쉽게 접할수 있으니 본 게시글에서는 다루지 않고 버킷생성과 키 발급이 완료된 후 부터 시작해보자.

사용 빌드 버전: Spring 3.1.6 , Java 17

우선 S3와 Spring web의 의존성을 추가해준다.

implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
implementation 'org.springframework.boot:spring-boot-starter-web'

그다음 application.yaml을 통해 발급받은 버킷과 키값을 설정해준다.

cloud:
  aws:
    credentials:
      accessKey: {발급받은 accesskey}
      secretKey: {발급받은 secretKey}
    region:
      static: {region 정보}
    stack:
      auto: false

application:
  bucket:
    name: {발급받은 bucket이름}

다음으로 설정을 적용하기 위해 StorageConfig 클래스를 생성해 S3에 적용해준다.

다음으로 이미지 업로드 요청을 받고 수행할 ControllerService을 만들어준다.

컨트롤러는 해당 유저 id와 이미지 파일을 받아 서비스단으로 전달해준다.

해당 서비스단에서는 컨트롤러로 부터 받아온 파라미터인 MultipartFile의 크기와 contentType을 검증한 후 S3에 중복저장을 막기 위한 고유의 이름으로 변경 후 저장한다.

Resolved [com.amazonaws.SdkClientException: Unable to calculate MD5 hash: 202312191806131735671348cat.jpg (지정된 파일을 찾을 수 없습니다)]

이때 로컬에 저장을 하지않고 S3에 저장을 할경우 위와 같은 오류메세지가 뜨게된다. 로컬에 저장하기 위해 convert메서드에 로컬저장기능을 추가하면 아래와같다.

포스트맨과 S3 결과는 아래와같다.

S3로 이미지파일을 업로드하는 방법에 대해 공부하던 도중 업로드 방법에 대한 선택에 대해 생각해보자.

추가로 스프링부트에서 S3로 파일을 업로드하는 방법에 대해 얘기해보자.
해당 블로그에서는 S3업로드 방식에 대해선 세부적으로 여러가지있지만 기술적으로 크게 나눈다면 3가지로 나뉜다고한다.

  1. Stream 업로드
  2. MultipartFile 업로드
  3. AWS Multipart 업로드

Stream 업로드

HttpServletRequest의 InputStream을 이용하여 AWS S3에 다이렉트로 파일을 전송하는 방식이다. 이 방법의 특징은 업로드할 파일의 바이너리 전체를 SpringBoot 애플리케이션을 실행하고 있는 서버의 디스크나 힙 메모리에 저장하지않는다는 점이다. (로컬에 잠시 저장 후 업로드가 끝나면 삭제하기때문에 약간의 메모리는 사용함.)

모든 바이너리를 로드를 하지않는다는 뜻은 전처리(이미지리사이징 등)이 불가능하다는 의미이기도하다. 또한 해당 업로드 방식은 1회 API호출 당 1개의 파일만 업로드만 가능하다. (HttpServletRequest에 담긴 바이너리에서 여러개 파일 데이터를 구분할수 있는 기준이 없기 때문)

이러한 Stream 업로드 방식은 서버 메모리에 영향이 적어 대용량 업로드에 적합하게 보일수 있으나 치명적인 문제가있다. 바로 속도이다. 대용량 파일을 업로드할때 중요한점이 바로 업로드 속도이다. 해당 방식은 서버의 한정적 리소스, 저용량 업로드에는 적합할지모르나 대용량업로드의 경우 고려해야할점이 있다.

  • 클라이언트 네트워크 환경, 클라우드 인스턴스 유형에 따라 업로드 속도 편차가 크기 때문에 운영환경과 동일한 인프라로 충분한 속도 테스트가 필요하다.
  • Stream 업로드 방식은 구조적으로 클라이언트에게 업로드 현황을 제공할 수 없습니다. 따라서 클라이언트가 기다릴 수 있을만큼 적당한 파일사이즈 제한이 필요하다.
  • 대용량 파일을 업로드 중 오류가 발생했을 때 전체 파일을 처음부터 다시 업로드해야하기 때문에 시간과 대역폭이 낭비될 수 있다.

MultipartFile 업로드

MultpartFile 업로드는 Spring에서 제공하는 MultiparFile 인터페이스를 이용하여 파일을 업로드하는 방식으로 내가 사용한 방식이다. 해당 인터페이스는 파일의 이름, 크기, 내용과 같은
업로드된 파일의 내용에 엑세스방식을 제공한다.

엑세스 방식을 제공함으로써 Stream업로드보다 더 높은 수준의 추상화와 편의성을 제공하고있다.
이 방법을 사용시 해당 설정을 추가해서 사용하면 작업 및 관리 편의성이 증가할수있다

spring:
  servlet:
    multipart:
      enabled: true # 멀티파트 업로드 지원여부 (default: true)
      file-size-threshold: 0B # 파일을 디스크에 저장하지 않고 메모리에 저장하는 최소 크기 (default: 0B)
      location: /users/charming/temp # 업로드된 파일이 임시로 저장되는 디스크 위치 (default: WAS가 결정)
      max-file-size: 100MB # 한개 파일의 최대 사이즈 (default: 1MB)
      max-request-size: 100MB # 한개 요청의 최대 사이즈 (default: 10MB)

이 방식은 해당 파일이 업로드되면 WAS(Tomcat)에 의해 해당파일을 힙 메모리가 아닌 Servlet Container Disk(컨테이너가 실행되고 있는 서버의 디스크)에 저장된다. 요청 처리가 끝나면 해당 파일을 삭제하지만 업로드 중에 배포된다거나 장애가 발생하는등 문제가 발생할 경우 삭제가 되지않고 남아있는 경우가 있을수 있기때문에 별도 경로를 지정한 후 주기적으로 삭제가 이뤄질수있게 만들어야한다.

또한 Spring은 임시 디렉토리에 저장된 파일을 Multipart 변수에 매핑함으로써 업로드된 파일의 바이너리를 힙메모리에 할당하지 않고 해당 콘텐츠의 메타데이터에 바로 엑세스할수있다. 즉 업로드된 파일의 크기가 file-size-threshold값 이하면 WAS가 임시파일을 생성하지않고 바로 파일 바이너리를 메모리에 다이렉트로 할당한다. 이 경우 파일 처리 속도는 빠르지만 스레드가 작업을 수행하는 동안에 부담이 될수 있기 때문에 충분한 검토가 필요하다. 값 이상이라면 파일은 지정경로에 저장되고 Spring에서 필요할때 해당 파일을 읽어 작업할수있다.

MultipartFile 업로드 방식은 단일 API호출로 List형식으로 파일을 맵핑받아 여러개의 파일을 동시에 업로드가 가능하다는 특징이있다.

Stream 업로드와 MultipartFile 업로드 방식 모두 다수의 사용자가 동시에 요청이 올때 서버의 스레드가 빠르게 소진될 위험이 있기때문에 스레드 풀 설정이 적절하게 이뤄져 스레드 고갈로 인한 타임아웃을 방지해야한다. 다른 방법은 bulkhead 패턴을 활용하여 작업의 최대 수를 제한하고 별도의 스레드 풀을 사용하거나 파일을 업로드하는 서버를 별도로 분리하는 방법 등이 있다.

AWS Multipart 업로드


해당 방법은 AWS S3에서 제공하는 파일 업로드 방식으로 업로드할 파일을 작은 part로 나누어 각 부분을 개별적으로 업로드 후 모든 part가 업로드되면 AWS에서 하나의 객체로 조립하여 저장한다.

이 방법은 파일의 바이너리가 SpringBoot를 거치지않고 AWS S3에 다이렉트로 업로드되기 때문에 서버의 부하를 고려하지 않아도 되는 점이있으며 해당 파트의 업로드 현황을 사용자에게 제공할수있다는 장점이있다.

해당 과정을 요약해 보자면

  1. 업로드 시작 : 서버에서는 멀티파트 업로드에 대한 고유 식별자 upload ID를 요청에 대한 응답으로 보내는데 해당 아이디는 부분 업로드, 완료, 중단 요청 등에 사용되기 때문에 중요성이 높다.

  2. PresignedURL 발급 : 업로드 하기위해 AWS의 서명된 URL을 발급받는 요청으로 upload IDPartNumber값을 함께 요청해야한다. PartNumber는 AWS에서 사용되는 숫자로 1부터 10000까지 지정되는 숫자로 AWS에서 해당 숫자를 통해 객체의 각 부분과 위치를 고유하게 식별한다. 이 정보를 토대로 이전에 업로드한 부분과 동일한 부분 번호로 새 부분을 업로드 할 경우 이전에 업로드한 부분을 덮어쓰게 된다.

  3. PresignedURL part 업로드 : 발급받은 URL에 Put메소드로 파트이 바이너리를 실어서 요청한다. 이때 파트의 용량은 클라이언트에서 정하는데 분할된 파트는 5MB~5GB의 크기만 가능하다. 단 마지막 파트는 5MB 이하 여도 가능하고 파일의 총크기가 5MB인경우도 마지막파트로 취급하여 파일 업로드가 가능하다. 파트는 개당 5GB씩 최대 10,000개 가능함으로 이론상 5TB크기 까지 업로드 가능하다. 해당 업로드 이후 받는 응답 헤더에는 MD5 Checksum 값인 ETag(Entity Tag)가 포함되있다. 클라이언트 각 파일 업로드 이후 PartNumberETag값을 매칭하여 보관해야한다. 이후 멀티파트 업로드 완료 요청시 해당 값을 포함해야한다.

이때 PartNumber와 ETag를 통해 클라이언트에게 현재 전송 상황을 퍼센테이지로 보여줄수있다.

  1. 업로드 완료 : 완료 요청에는 upload Id, 각 PartNumber와 매칭되는 ETag값이 배열로 포함되어 있어야한다. 그 이유는 AWS는 해당 정보를 토대로 전송받은 정보를 기준으로 객체를 조립한다.

해당 블로그에서는 무조건 정답인 방법은 없으므로 해당 설계 상황에 맞는 방법을 취사선택하여 기능을 구현한다고 말하고있다. 내가 구현한 이미지 업로드기능의 경우 파일크기가 크지않고 게시글의 경우 여러개의 이미지가 유저 프로필사진의 경우 한개의 이미지가 들어가고 Spring에서 지원하는 MultipartFile 인터페이스를 사용한 방법을 사용하였다.

참조 :우테코 : Spring Boot에서 S3에 파일을 업로드하는 세 가지 방법

profile
도전하는 사람

0개의 댓글