이미지 최적화 - 3. Presigned URL

신예찬·2024년 12월 29일

AWS 이미지 최적화

목록 보기
3/3

앞선 두 포스팅은 이미지를 업로드하는데에 포커스를 맞춰뒀다

사실 뭐 대부분의 서비스가 쓰기작업보다도 읽기 작업이 훨씬 많을수밖에 없기 때문에 업로드에는 크게 신경써도 되지 않을줄 알았다

하지만 기존에 업로드 방식에 문제점을 발견해서 이를 해결하기 위해 업로드 방식에도 변화가 필요하다고 생각해 알아봤다

이미지 업로드 방식

이미지를 서버에 업로드할때는 두가지 요소를 잘 따져봐야한다

  1. 이미지를 어디에 저장할지

    • DB, 애플리케이션 서버, Cloud Storage에 저장 가능하다
  2. 이미지를 누가 저장할지

    • WAS에서 저장할수도 있고 Client단에서 직접 저장할수도 있다

이미지 업로드 방식을 먼저 다이어그램으로 살펴보자

1. 애플리케이션 서버에 직접 저장

가장 간단한 방법이다
당연하게도 업로드하게되면 디스크의 공간을 차지게 된다

2. Blob 변환 후 DB 저장


이미지를 Blob로 변환한 후 데이터베이스에 저장하는 방식이다

RDBMS를 활용할수 있다는 장점아닌 장점이 있지만 데이터베이스에 긴 문자열을 넣는다는것 자체가 엄청나게 큰 비용이기 때문에 썩 좋지않은 방법이다

3. WAS에서 Cloud Storage에 저장

기존에 사용하던 방식이다

이미지를 Client단에서 multipart로 만들어 업로드하고, 이를 Spring Boot WAS에서 AWS의 S3에 저장하는 방식을 사용했다

Spring WebMVC에서 multipart를 받으면 MultipartFile이라는 객체를 받아오게 되는데 이것이 문제가 됐다

A representation of an uploaded file received in a multipart request.

The file contents are either stored in memory or temporarily on disk. In either case, the user is responsible for copying file contents to a session-level or persistent store as and if desired. The temporary storage will be cleared at the end of request processing.

멀티파트 요청으로 수신된 업로드된 파일의 표현입니다.

파일 내용은 메모리에 저장되거나 임시로 디스크에 저장됩니다. 두 경우 모두 사용자는 원하는 경우 파일 콘텐츠를 세션 수준 또는 영구 저장소에 복사할 책임이 있습니다. 요청 처리가 끝나면 임시 저장소가 지워집니다.

이미지를 가져오면 메모리나 디스크에 임시로 저장된다는 문제다

일시적이긴 하지만 이미지 업로드 요청이 쇄도하는 경우에 자칫 OOME같은 문제가 발생할수도 있을거 같다

그래서 다음 방식을 최종적으로 채택하게 됐다

4. Presigned URL을 사용해 Client단에서 업로드

이미지를 Client단에서 Cloud Storage에 올리는방식이다

업로드할 컨텐츠에 대해 미리 서명된 URL을 사용자에게(정확히는 브라우저에게) 제공하여 사용자가 직접 컨텐츠를 업로드 가능하게 권한을 제공해주는 컨텐츠 업로드 서명 방식이다

미리 서명된 URL을 사용하는 이유는 아무나 Cloud Storage에 접근할 수 없도록 하기 위함이다

발급받은 URL을 통해 Cloud Storage에 업로드할 이미지를 PUT 또는 POST 요청으로 직접 업로드 할 수 있다

물론 해당 URL은 만료시간이 지나게되면 더이상 사용 불가능하다(정확히는 만료된 URL을 사용하면 403 에러가 발생)



매개변수설명
X-Amz-Algorithm서명 버전과 알고리즘을 식별하고, 서명을 계산하는데 사용. 서명 버전 4를 위해서 “AWS4-HMAC-SHA256” 로 설정
X-Amz-Credential액세스 키 ID와 범위 정보(요청 날짜, 사용하는 리전, 서비스 명). 리전 명은 리전 및 엔드포인트에서 확인 가능
X-Amz-Date날짜는 ISO 8601형식. 예: 20160115T000000Z
X-Amz-Expires미리 선언된 URL이 유효한 시간 주기. 초단위. 정수 값. 최소 1에서 최대 604800 (7일) 예: 86400 (24시간)
X-Amz-SignedHeaders서명을 계산하기 위해 사용되어지는 헤더 목록. HTTP host 헤더가 요구됨
X-Amz-Signature요청을 인증하기 위한 서명

Presigned URL은 헤더에 위와 같은 정보들을 가져 사용자가 이미지를 S3에 직접 올리려 할때 직접 서명 및 기타 메타정보들을 확인한다

구현 With Java

AWS는 워낙 공식 문서가 잘 되어 있기 때문에 찾아보면 여러가지 방법들이 나온다

그중 최대한 간략한 방법을 찾아 Presigned URL을 발급받도록 했다

1. URL 발급 서비스 구현

의존성 추가

	implementation 'com.amazonaws:aws-java-sdk-s3'
	package org.chzz.market.common.config.aws;

import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AWSConfig {
    @Value("${cloud.aws.credentials.access-key}")
    private String accessKey;

    @Value("${cloud.aws.credentials.secret-key}")
    private String secretKey;

    @Value("${cloud.aws.region.static}")
    private String region;
    
    @Bean
    public AmazonS3 amazonS3Client() {
        BasicAWSCredentials awsCreds = new BasicAWSCredentials(accessKey, secretKey);
        return AmazonS3ClientBuilder.standard()
                .withRegion(region)
                .withCredentials(new AWSStaticCredentialsProvider(awsCreds))
                .build();
    }
}

aws s3 접근을 위한 bean 설정

package org.chzz.market.domain.image.service;  
  
import com.amazonaws.HttpMethod;  
import com.amazonaws.services.s3.AmazonS3;  
import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest;  
import java.net.URL;  
import java.time.Instant;  
import java.util.Date;  
import java.util.List;  
import java.util.UUID;  
import lombok.RequiredArgsConstructor;  
import org.chzz.market.common.config.aws.BucketPrefix;  
import org.chzz.market.domain.image.dto.response.CreatePresignedUrlResponse;  
import org.springframework.stereotype.Service;  
  
@Service  
@RequiredArgsConstructor  
public class ImageUploadService {  
    private static final int DURATION_MILLIS = 1000 * 60 * 2;  
  
    private final AmazonS3 amazonS3;  
    private final String s3BucketName;  
  
    public CreatePresignedUrlResponse createPresignedUrl(BucketPrefix bucketPrefix, String fileName) {  
        Date expiration = getPreSignedUrlExpiration();  
        String objectKey = bucketPrefix.createPath(fileName);  
        GeneratePresignedUrlRequest request = getGeneratePreSignedUrlRequest(objectKey, expiration);  
        URL url = amazonS3.generatePresignedUrl(request);  
        return CreatePresignedUrlResponse.of(objectKey, url.toString(), expiration);  
    }
  
    private GeneratePresignedUrlRequest getGeneratePreSignedUrlRequest(final String fileName, final Date expiration) {  
        return new GeneratePresignedUrlRequest(s3BucketName, fileName)  
                .withMethod(HttpMethod.PUT)  
                .withExpiration(expiration);  
    }  
  
    private Date getPreSignedUrlExpiration() {  
        return Date.from(Instant.now().plusMillis(DURATION_MILLIS));  
    }  
}

그리고 URL 발급 서비스다

파일 이름을 적절히 hash code + UUID를 조합해 S3의 객체 이름을 만들었다

참고로 파일 이름에 /가 있으면 prefix(S3 내부에서 일종의 파일)로 인식된다

2. 발급된 URL을 Client단에 전달


응답으로는 Object Key와 업로드 가능한 URL이 필요하다

upload URL을 통해 이미지를 요청한다

코드부분에서 보이듯이 해당 URL을 PUT 요청을 통해 이미지 업로드가 가능하다

Object Key는 이어지는 작업에서 사용된다

3. 업로드 성공 후 DB에 메타정보 저장

마지막으로 PUT 요청이 성공하면 성공한 URL을 WAS에 전송해 영속화하여 조회시 사용한다

중요한것은 요청이 완전히 성공하고 난 후에 데이터베이스에 저장을 해야한다는 것이다
그렇지 않으면 S3에 없는 데이터를 조회 요청할때 사용할 수 있기 때문이다

AWS Multipart

AWS Multipart는 Presigned URL을 사용하되 이미지를 S3에 업로드를 한번에 하느것이 아니라 쪼개서 하는 방법이다

필자의 경우 업로드하는 이미지의 크기가 그리 크지 않기 때문에 이미지 업로드를 쪼개서 할 필요성까지는 못느껴서 한번에 업로드 하는 방식을 사용했지만 안정성이 필요한 경우에는 AWS Multipart Upload 방식을 고민해보는것도 좋다

백엔드에서 이미지 업로드는 어떻게 하면 좋을까?
우아한 기술 블로그 Spring Boot에서 S3에 파일을 업로드하는 세 가지 방법
AWS API 호출하기 (2) – Amazon S3 객체에 대한 미리 선언된(pre-signed) URL 생성하기
aws doc sdk examples Github

0개의 댓글