[AWS] Presigned URL을 사용하여 S3에 이미지 업로드

Donghoon Jeong·2024년 7월 24일
0

AWS

목록 보기
3/4
post-thumbnail

지난 포스팅에서 S3 버킷을 생성하고 스프링 부트 프로젝트와 연동하여 이미지를 업로드하는 방법에 대해 설명드렸습니다.

이번에 진행한 프로젝트에서 이미지를 대량으로 업로드하는 부분을 맡게 되어 해당 방법을 사용하여 다량의 이미지를 업로드하는 방식을 사용하려고 했지만 해당 방법을 사용할 경우, 단점이 존재한다는 것을 알게 되었습니다. 그래서 이번 포스팅에서는 해당 방식의 단점에 대해서 알아보고 단점을 해결하기 위한 방법인 Presigned URL에 대해서 알아보겠습니다.

기존 방식의 단점

저번 포스팅에서 알아본 방식은 비교적 간단하게 구현할 수 있지만 아래와 같이 몇 가지 단점이 존재합니다.

서버 부하 증가

이미지 업로드 시 클라이언트가 서버로 이미지를 전송하고, 서버는 이 이미지를 다시 S3로 전송합니다. 이는 서버에 불필요한 부하를 유발할 수 있습니다. 특히, 대용량 파일이나 많은 사용자가 동시에 이미지를 업로드할 경우 서버의 자원 소모가 급격히 증가할 수 있습니다.

서버 리소스 낭비

중간에 서버를 거쳐 가는 방식은 네트워크 대역폭을 낭비하고, 서버의 CPU와 메모리를 불필요하게 사용하게 됩니다. 이는 서버의 성능 저하로 이어질 수 있습니다.

위와 같은 단점들을 해결하기 위해 AWS S3에서 제공하는 presigned URL을 사용할 수 있습니다. Presigned URL을 사용하면 서버를 거치지 않고 클라이언트가 직접 S3 버킷에 파일을 업로드할 수 있어 서버의 부하를 줄이고, 보다 효율적으로 이미지를 업로드할 수 있습니다.


Presigned URL

Presigned URL은 클라우드 스토리지 서비스에서 제공하는 기능으로, 사용자가 생성한 URL을 통해 지정된 시간 동안 서버의 자원(파일 등)에 접근할 수 있게 해주는 임시 링크입니다. 이 URL은 서버에 저장된 자원에 대한 요청을 사전에 승인하며, 서명된 쿼리 파라미터를 포함하여 생성됩니다. URL은 생성 시 설정한 유효 기간 동안만 유효하며, 이 기간이 지나면 자동으로 접근이 불가능해집니다.

Presigned URL 동작과정

Presigned URL의 동작과정은 아래와 같습니다.

  1. 클라이언트가 이미지를 업로드를 위한 Presigned URL을 발급을 요청합니다.

  2. AWS는 파일을 올릴 때 사용할 수 있는 Presigned URL을 발급하여 서버를 거쳐 클라이언트에게 전달합니다.

  3. 클라이언트는 해당 URL에 PUT 요청으로 이미지를 업로드합니다.

  4. S3에 파일이 업로드됩니다.

Presigned URL 장점

서버 부하 감소

클라이언트가 서버를 거치지 않고 직접 S3로 이미지를 업로드하므로, 서버의 부하를 크게 줄일 수 있습니다. 서버는 단지 presigned URL을 생성해 클라이언트에 전달하는 역할만 하면 됩니다.

효율적인 리소스 사용

서버 리소스를 절약할 수 있으며, 네트워크 대역폭을 보다 효율적으로 사용할 수 있습니다. 파일 업로드와 관련된 리소스를 거의 사용하지 않게 되어, 다른 중요한 작업에 집중할 수 있습니다.

정리하자면, Presigned URL을 사용하면 서버를 거치지 않고 다량의 이미지를 직접 S3 버킷에 업로드할 수 있습니다.

이 방법은 클라이언트가 서버를 통해 이미지를 업로드하는 기존 방식과 달리, 서버가 이미지 데이터를 직접 처리하지 않아도 되므로 서버에 대한 부하를 크게 줄일 수 있습니다. 이는 서버의 리소스를 절약하고 성능을 개선하며, 동시에 업로드 과정의 효율성을 높일 수 있는 큰 장점이 있습니다.

Presigned URL을 활용하면 클라이언트가 지정된 기간 동안 S3 버킷에 안전하게 파일을 업로드할 수 있어 보안 측면에서도 유리합니다. 이러한 이유로 대량의 이미지를 효율적으로 처리해야 하는 상황에서 Presigned URL은 매우 유용한 솔루션이 될 수 있습니다.


Presigned URL 파라미터

https://example-bucket.s3.amazonaws.com/images/2d098b12-5cd7-4f00-835b-a6c998a13617%EB%B8%94%EB%A1%9C%EA%B7%B8%20%EC%8D%B8%EB%84%A4%EC%9D%BC.png
?x-amz-acl=public-read
&X-Amz-Algorithm=AWS4-HMAC-SHA256
&X-Amz-Date=20230903T144326Z
&X-Amz-SignedHeaders=host
&X-Amz-Expires=120
&X-Amz-Credential=<ACCESS_KEY>/20230903/ap-northeast-2/s3/aws4_request
&X-Amz-Signature=<SIGNATURE_VALUE>

x-amz-acl

Presigned URL을 활용하여 업로드하고 리소스에 대한 GET 요청 시 access denied가 발생합니다.

그러므로 Presigned URL을 생성할 때 public-read 권한을 설정해야 합니다.

x-amz-algorithm

서명 버전과 알고리즘을 식별하고, 서명을 계산하는데 사용합니다. 버전 4를 위해서 AWSS4-HMAC-SHA256 사용하였습니다.

x-amz-date

날짜는 ISO 8601 형식을 사용합니다.

ex) 20240118T000000Z

x-amz-signedheaders

서명을 계산하기 위해 사용되는 헤더 목록으로, HTTP host 헤더가 요구됩니다.

x-amz-expires

미리 선언된 URL이 유효한 시간 주기로, 초 단위이며 정수 값입니다.

최소 1에서 최대 604800(7일)까지 가능합니다.

x-amz-credential

Acces Key와 범위 정보(요청 날짜, 리전, 서비스 명)입니다.

x-amz-signature

요청을 인증하기 위한 서명입니다.


구현 방법

1. 의존성 추가

현재 진행하고있는 스프링부트 버전에 맞는 AWS관련 의존성을 추가해줍니다.

build.gradle

implementation platform("io.awspring.cloud:spring-cloud-aws-dependencies:3.0.0")
implementation 'io.awspring.cloud:spring-cloud-aws-starter-s3'
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'

2. S3 정보 설정

application.yml

S3 버킷 정보와 접근 권한(GetObject, PutObject)이 있는 User의 Access Key와 Secret Key를 추가해줍니다.

spring:
  cloud:
    aws:
      s3:
        bucket: [bucket name]
      credentials:
        accessKey: [access key]
        secretKey: [secret key]
      region:
        static: ap-northeast-2

3. S3Config 설정

S3Config.java

위에서 입력한 키에 대한 정보를 사용하여 S3 접근을 위해 필요한 인증 정보를 설정해 줍니다.

@Configuration
public class S3Config {

    @Value("${spring.cloud.aws.credentials.access-key}")
    private String accessKey;
    @Value("${spring.cloud.aws.credentials.secret-key}")
    private String secretKey;
    @Value("${spring.cloud.aws.region.static}")
    private String region;

    @Bean
    @Primary
    public BasicAWSCredentials awsCredentialsProvider() {
        return new BasicAWSCredentials(accessKey, secretKey);
    }

    @Bean
    public AmazonS3 amazonS3() {
        return AmazonS3ClientBuilder.standard()
                .withRegion(region)
                .withCredentials(new AWSStaticCredentialsProvider(awsCredentialsProvider()))
                .build();
    }
}

4. PhotoService 구현

PhotoService.java

@Service
@RequiredArgsConstructor
public class PhotoService {

    private final PhotoRepository photoRepository;
    private final MemberRepository memberRepository;
    private final S3Template s3Template;
    private final AmazonS3 amazonS3;

    @Value("${spring.cloud.aws.s3.bucket}")
    private String bucketName;

    @Value("${spring.cloud.aws.region.static}")
    private String region;

    @Transactional
    public PreSignedUrlResponse getPreSignedUrl(String prefix, String originalFilename) {
        String fileName = createPath(prefix, originalFilename);
        GeneratePresignedUrlRequest generatePresignedUrlRequest = getGeneratePreSignedUrlRequest(bucketName, fileName);
        URL presignedUrl = amazonS3.generatePresignedUrl(generatePresignedUrlRequest);

        photoRepository.save(photo);
        return PreSignedUrlResponse.toPreSignedUrlResponse(presignedUrl.toString());
    }

    private GeneratePresignedUrlRequest getGeneratePreSignedUrlRequest(String bucket, String fileName) {
        GeneratePresignedUrlRequest generatePresignedUrlRequest = new GeneratePresignedUrlRequest(bucket, fileName).withMethod(HttpMethod.PUT).withExpiration(getPreSignedUrlExpiration());

        generatePresignedUrlRequest.addRequestParameter(Headers.S3_CANNED_ACL, CannedAccessControlList.PublicRead.toString());

        return generatePresignedUrlRequest;
    }

    private Date getPreSignedUrlExpiration() {
        Date expiration = new Date();
        long expTimeMillis = expiration.getTime();
        expTimeMillis += 1000 * 60 * 2;
        expiration.setTime(expTimeMillis);
        return expiration;
    }

    private String createFileId() {
        return UUID.randomUUID().toString();
    }

    private String createPath(String prefix, String fileName) {
        String fileId = createFileId();
        return String.format("%s/%s", prefix, fileId + fileName);
    }

    private String generateFileAccessUrl(String fileName) {
        return String.format("https://%s.s3.%s.amazonaws.com/%s", bucketName, region, fileName);
    }
}

PhotoService 클래스는 Presigned URL을 발급 받는 로직입니다. 각각의 메서드의 설명을 통해 해당 로직에 대해서 자세하게 설명하겠습니다.

getPreSignedUrl

아래 4개의 메서드를 사용하여 Presigned URL을 발급하여 반환합니다.

getGeneratePresignedUrlRequest

파일 업로드용 Presigned URL을 생성하기 위한 GeneratePresignedUrlRequest 객체를 생성합니다. 이 객체는 다음을 포함합니다

getPresignedUrlExpiration

이 메서드는 Presigned URL의 유효 기간을 설정합니다. 현재 시각을 기준으로 2분 후로 설정됩니다.

createFileId

UUID를 사용하여 파일 고유 ID를 생성합니다.

createPath

파일의 전체 경로 생성한다. prefix와 fileName을 합쳐서 파일의 전체 경로를 생성합니다.
파일의 이름이 중복될 수 있으므로 createFileld 메서드를 사용하여 UUID를 추가합니다. 해당 값을 사용하여 이미지에 접근할 수 있습니다.

데이터베이스 저장 시점

이미지를 불러오기 위해 별도의 데이터베이스 저장이 필요합니다. 처음에는 Presigned URL을 호출하는 시점에 이미지를 데이터베이스에 저장하는 방식을 사용했습니다. 그러나 이미지 업로드가 완료되지 않은 상태에서 데이터베이스에 저장하는 것은 올바른 로직이 아니라고 판단했습니다.

그래서 클라이언트에서 이미지 업로드가 완료된 후, 서버에 이미지 이름과 부가적인 정보를 보내주면 서버가 이를 데이터베이스에 저장하는 방식으로 구현했습니다.

5. PhotoController 구현

PhotoController.java

@RequestMapping("/images")
@RestController
@RequiredArgsConstructor
public class PhotoController {

    private final PhotoService photoService;

    @PostMapping("/preSignedUrl")
    public ResponseEntity<List<PreSignedUrlResponse>> getPreSignedUrl(@RequestBody List<PreSignedUrlRequest> preSignedUrlRequestList) {
        List<PreSignedUrlResponse> preSignedUrlList = preSignedUrlRequestList.stream()
                .map(preSignedUrlRequest -> photoService.getPreSignedUrl(preSignedUrlRequest.getPrefix(), preSignedUrlRequest.getImageName()))
                .collect(Collectors.toList());

        return ResponseEntity.ok().body(preSignedUrlList);
    }
}

해당 컨트롤러는 요청으로 받은 preSignedUrlRequestList를 스트림으로 처리합니다.
PreSignedUrlRequest 객체에 대해 getPreSignedUrl 메서드를 호출하여 presigned URL을 생성하고 생성된 PreSignedUrlResponse 객체들을 리스트로 수집하여 반환합니다.


테스트

이제 위 코드를 사용하여 Presigned URL을 발급받고 이미지를 업로드해 보겠습니다.

클라이언트가 서버에게 Presigned URL 발급 요청

저장할 파일 위치와 저장할 이미지의 이름을 작성하여 Presigned URL을 요청합니다.

다음과 같이 정상적으로 Presigned URL이 발급된 것을 확인할 수 있습니다.

클라이언트가 발급 받은 Presigned URL을 통해 이미지 업로드

위에서 발급받은 Presigned URL을 사용하여 이미지 업로드를 해보겠습니다.

PUT 메서드를 선택하고 원하는 이미지를 선택하고 요청을 보내면 정상적으로 응답이 나온 것을 확인할 수 있습니다.

또한 S3 버킷에도 사진이 정상적으로 저장된 것을 확인할 수 있습니다.


Reference

https://techblog.woowahan.com/11392/

https://velog.io/@jmjmjmz732002/Spring-Boot-%EC%84%9C%EB%B2%84%EB%A6%AC%EC%8A%A4-%EA%B8%B0%EB%B0%98-S3-Presigned-URL-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0#dependency

profile
정신 🍒 !

0개의 댓글

관련 채용 정보