Spring

[Spring Boot] PresignedUrl 활용한 AWS S3 버킷에 이미지 업로드 구현

도전하는 린치핀 2024. 8. 28. 16:34

0. PresignedUrl 사용 배경

보통 이미지의 업로드/다운로드의 과정은 어떻게 진행할까?

  • 이미지 자체는 S3에 업로드하고, S3의 url을 백엔드 서버에 저장
  • 클라이언트가 이미지를 요청하면 서버는 저장된 url을 반환한다.

즉, 크게 나눈다면 "S3에 이미지 업로드/다운로드", "S3에 접근하는 url 저장" 두가지라고 생각할 수 있다.

하지만 S3에 이미지를 업로드/다운로드를 하는 과정은 어떻게 진행되었을까?

 

기존의 방식

  1. 서버가 클라이언트에게 이미지 자체를 받는다.
  2. 서버가 보안절차를 거친다.(AWS sdk secret key를 활용해 s3접속)
  3. 서버가 버킷에 이미지를 올린다.

이렇게 구현된 이유는 보안이슈 때문이다. 아무나 이미지를 업로드하게 하면 안되기에, 서버를 거쳐 이미지 업로드를 구현했었다. 

그렇다면 위의 방식에서 가장 큰 문제가 무엇일까?

 

바로 이미지의 용량이 커지고, 이미지 업로드가 빈번하게 일어나면 서버 부하가 심해진다는 것이다.

 

즉, 모든 이미지의 업로드 과정을 서버가 부하한다면 메모리 관리가 힘들 수 있다. 

해당 문제에 대한 해결 방법으로 Presigned-url을 사용한다고 한다.

 

1. PresignedUrl 이란?

모든 객체는 기본적으로 비공개이며, 객체 소유자만 객체에 액세스 할 수 있다.
객체 소유자는 필요할 경우 자신의 보안 자격 증명을 사용하여 일정기간 동안 객체 업로드/다운로드를 허가하는 미리 서명된 URL 을 만들어 다른 사용자와 객체를 공유할 수 있다. 

 

 

 

 

이 Presigned-Url을 통해 클라이언트는 백엔드 서버를 거치지 않고, 바로 S3로 업로드가 가능해진다.

위의 기존 방식에서 2번절차를 서버가 하고, 3번을 클라이언트가 하는 방식이다.

백엔드는 presignedUrl을 생성해줘 보안절차 작업을 진행해주고, Client는 이를 받아 AWS S3로 바로 업로드가 가능해진다.

 

PresignedUrl 활용 이미지 업로드 로직

  1. 사용자가 이미지 업로드를 하려고 한다.
  2. 클라이언트는 서버에 filename을 보내며 Presigned Url 요청을 보낸다.
  3. 서버는 AWS에 보안절차를 통해 Presigned url을 생성한다.
  4. 서버는 응답값으로 생성된 PresignedUrl을 반환한다.
  5. 클라이언트는 PresignedUrl에 Put 메소드로 file을 함께 넘겨주면 업로드가 완료된다.
  6. 업로드 받은 이미지 경로를 String 형태로 서버에 전달한다.

2. PresignedUrl 구현을 위한 AWS 설정

2-1. S3 버킷 생성

 

  1. 기본적으로 S3 버킷의 서비스는 모든 사용자가 업로드한 파일을 Public하게 접근할 수 있어야한다.
  2. 또한, 버킷을 생성할때 퍼블릭 엑세스 차단 설정을 해제함.
  3. 모든 나머지 설정은 기본 값으로 두면 된다.

위의 사진처럼 설정하고 나면 버킷 생성완료!

 

 

2-2. S3 버킷 정책 설정

1. 버킷 > 권한탭의 정책 편집을 클릭

 

2. 버킷 정책 -> 편집 버튼 클릭

 

3. 버킷 ARN 복사 후, 정책 생성기 클릭

 

3. 정책 생성을 위한 값 입력

(좌 : 정책 생성기 값 입력 화면, 우: 생성된 정책)

 

입력해야 할 값

  • Select Type of Policy : S3 Bucket Policy
  • Effect: Allow
  • AWS Service: Amazon S3
  • ARN : arn:aws:s3:::{bucket_name}/* → {복사한 ARN}/*
  • Actions: Get*, Put*
  • Principal: *

생성된 정책

{
    "Version": "2012-10-17",
    "Id": xxxxxxxxxxxxx,
    "Statement": [
        {
            "Sid": xxxxxxxxxx,
            "Effect": "Allow",
            "Principal": "*",
            "Action": [
                "s3:GetObject",
                "s3:PutObject"
            ],
            "Resource": "arn:aws:s3:::test-presignedurl-bucket11/*"
        }
    ]
}

 

 

4. 3번에서 생성한 정책 붙여넣기

 

 

2-3. CORS 설정

 

입력한 CORS 정책

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

 

추가로, 예제에서 AllowedOrigins를 *로 모두 허용했지만 보통 자신이 만드는 웹페이지의 origin에 대해서만 허용해야한다.

 

2-4. IAM 권한 생성

  • S3버킷에 접근할 수 있는 사용자를 통해 API 내에서 파일을 버킷에 업로드할 수 있도록 IAM 서비스에 접근 가능한 사용자를 추가해야한다.
  • 사용자 이름 입력 > 직접 정책 연결 옵션 선택 AmazonS3FullAccess권한을 추가하고 사용자를 추가한다.
  • 엑세스키를 생성하고 <엑세스키, 시크릿키>는 반드시 따로 저장해야 한다.

 

 

3. Spring Boot 내 설정 및 구현

 

3-1. 의존성 추가

// build.gradle 파일
// aws
implementation 'software.amazon.awssdk:s3:2.17.285'

 

3-2. application.yml 추가

  # aws
  cloud:
    aws:
      s3:
        credentials:
          accessKey: ${AWS_S3_ACCESSKEY}
          secretKey: ${AWS_S3_SECRETKEY}
        bucket: ${AWS_S3_BUCKET}
      region:
        static: ${AWS_REGION}
      stack:
        auto: false

 

  • 위의 accessKey, secretKey 와 같은 값들은 직접 입력하면 AWS의 무서운 과금을 느낄 수 있기 때문에 반드시 따로 저장해야 한다.
  • 해당 예제에서는 Spring 내 환경 변수로 설정하였다.

환경 변수 설정

  • 위의 사진에서 Edit 버튼을 클린한다.
  • Modify Options > Environment variablesName, Value 값 입력

3-3. Config 파일 생성

package T2F2.SPOT.config;


import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.AwsCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;

@Configuration
public class S3Config {

    @Value("${AWS_S3_ACCESSKEY}")
    private String accessKey;
    @Value("${AWS_S3_SECRETKEY}")
    private String secretKey;
    @Value("${AWS_REGION}")
    private String region;

    @Bean
    public AwsCredentials basicAWSCredentials() {
        return AwsBasicCredentials.create(accessKey, secretKey);
    }

    @Bean
    public S3Presigner s3Presigner(AwsCredentials awsCredentials) {
        return S3Presigner.builder()
                .region(Region.of(region))
                .credentialsProvider(StaticCredentialsProvider.create(awsCredentials))
                .build();
    }

    @Bean
    public S3Client s3Client (AwsCredentials awsCredentials) {
        return S3Client.builder()
                .region(Region.of(region))
                .credentialsProvider(StaticCredentialsProvider.create(awsCredentials))
                .build();
    }

}

 

3-4. Controller, Service 로직 생성

 

AWS Controller

package T2F2.SPOT.util.AWS;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;

@Slf4j
@RestController
@RequestMapping("/aws")
@RequiredArgsConstructor
public class AWSController {

    private final AWSService awsService;

    @GetMapping("file/{filename}")
    public ResponseEntity<String> getFile(@PathVariable(value = "filename") String filename) throws IOException {

        String url = awsService.getPresignUrl(filename);

        return new ResponseEntity<>(url, HttpStatus.OK);
    }
}

 

 

AWS Service

package T2F2.SPOT.util.AWS;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest;
import software.amazon.awssdk.services.s3.presigner.model.PresignedGetObjectRequest;
import software.amazon.awssdk.services.s3.presigner.model.PresignedPutObjectRequest;
import software.amazon.awssdk.services.s3.presigner.model.PutObjectPresignRequest;

import java.time.Duration;

@Slf4j
@Service
@RequiredArgsConstructor
public class AWSService {

    @Value("${AWS_S3_BUCKET}")
    private String bucketName;

    private final S3Client s3Client;
    private final S3Presigner presigner;

    public String getPresignUrl(String filename){
        if(filename == null || filename.equals("")) {
            return null;
        }
        PutObjectRequest putObjectRequest = PutObjectRequest.builder()
                .bucket(bucketName)
                .key(filename)
                .build();

        log.info(putObjectRequest.toString());

        PutObjectPresignRequest putObjectPresignRequest = PutObjectPresignRequest.builder()
                .signatureDuration(Duration.ofMinutes(5)) // presignedURL 5분간 접근 허용
                .putObjectRequest(putObjectRequest)
                .build();

        log.info(putObjectPresignRequest.toString());


        PresignedPutObjectRequest presignedPutObjectRequest = presigner
                .presignPutObject(putObjectPresignRequest);

        String url = presignedPutObjectRequest.url().toString();
        log.info(url);
        presigner.close(); // presigner를 닫고 획득한 모든 리소스를 해제
        return url;
    }

}

 

4. 결과 확인

4-1. Presigned url 요청

 

  • aa.png 라는 파일을 업로드 할꺼라고 서버에게 알려주면 서버는 aa.png 파일을 업로드 할 수 있는 presigned url을 반환해준다.

4-2. 반환 받은 Presigned url로 파일 업로드 하기

  • 반환 받은 Presigned url을 입력하고 PUT 메소드Body에서 binary로 선택한 뒤 "aa.png" 라는 파일을 함께 담아 보낸다.

4-3. 업로드 후 S3 및 url로 조회하기

 

S3 버킷에서 확인하면 aa.png 파일이 성공적으로 업로드 된 것을 확인할 수 있다.

 

 

또한, S3에 업로드 후 파일의 url을 통해 PostMan으로 확인하면 이미지가 잘 불러오는 것을 확인할 수 있다.

 

더보기