S3 Presigned URL을 활용한 이미지 업로드: Spring Boot & CloudFront 적용기

hyouzl·2025년 1월 29일
0

이미지를 업로드할 때, 일반적으로 서버를 통해 이미지를 받아서 S3에 전달하는 방식을 사용한다.
하지만 이런 방식은 서버에 부하를 주고, 불필요한 네트워크 비용을 발생시킨다.
Presigned URL을 활용하면 클라이언트가 직접 S3에 업로드할 수 있어 서버 부하를 줄이고 속도를 개선할 수 있다.

Presigned URL 이란?

클라이언트가 직접 S3에 파일을 업로드 하려면 AWS 리소스인 S3에 접근하기 위해서는 엑세스 키처럼 권한이 필요한데, 이 키를 프론트엔드가 직접 사용하는 것은 적절하지 않다. 프론트엔드는 사용자가 접근 가능한 영역이므로, 프론트엔드에 키를 줄 경우 유출 문제가 생길 수 있기 때문이다.

이를 해결하기 위해 사용하는 것이 Presigned url 이다. presigned url은 서버에서 가지고 있는 권한을 사용해 클라이언트에게 임시적인 권한을 발급해주는 것을 의미한다.
s3 버킷으로 접근할 수 있는 endPoint 라고 볼 수 있다.

결국 서버에서 presigned url 을 생성해주고 반환해주면 클라이언트는 s3에 직접 이미지를 업로드 할 수 있다.

구현 해보기

🧚 의존성 추가

일단 스프링부트 프로젝트에 sdk 의존성을 추가해준다.

    implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
    implementation 'com.amazonaws:aws-java-sdk-s3:1.12.767'
    implementation 'software.amazon.awssdk:s3:2.27.3'
    implementation 'software.amazon.awssdk:s3control:2.27.3'
    implementation 'software.amazon.awssdk:s3outposts:2.27.3'

🧚 application.yml 설정

그런 다음 생성해놓은 s3 버킷에 대한 정보를 설정 파일에 담아주어야 한다. 위에서 만든 버킷 이름, 버킷이 위치한 리전, 그리고 엑세스 키와 시크릿 키 정보를 넣어준다. 이때 엑세스 키와 시크릿 키는 무슨 일이 있어도 github에 올라가서는 안되므로 로컬 상에 환경변수로 등록해서 사용하도록 하자!

 cloud:
  aws:
    s3:
      bucket: ${AWS_BUCKET:default}
      path:
        projects: projects
    region:
      static: ${AWS_REGION:default}
    stack:
      auto: false
    credentials:
      accessKey: ${AWS_ACCESS_KEY_ID:default}
      secretKey: ${AWS_SECRET_ACCESS_KEY:default}

🧚 S3 설정

s3Config

S3Presigner는 S3에 접근하기 위한 Presigned URL을 생성하는 데 사용된다.

package com.partnerd.config;


import com.amazonaws.auth.*;
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;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;


/**
 *
 * S3Config를 통해 AmazonS3라는 클라이언트를 빈에 등록
 * 이 클라이언트가 있어야 AWS 리소스와 연결하고 정보를 가져오는 등의 일을 할 수 있다.
 *
 **/
@Configuration
public class S3Config {

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

    @Bean
    public S3Presigner s3Presigner() {

        StaticCredentialsProvider credentials = StaticCredentialsProvider.create(
                AwsBasicCredentials.create(accessKey, secretKey)
        );

        return S3Presigner.builder()
                .region(Region.of(region))
                .credentialsProvider(credentials)
                .build();
    }

}

s3 Presigned url 생성을 위해 권한 정책 세팅은
https://developer-been.tistory.com/36 해당 블로그를 참고했다!

s3Controller

  @PostMapping("/preSignedUrl")
  @Operation(summary = "PUT 메서드용 S3 preSignedUrl 요청 ", description = "PUT 메서드용 S3 preSignedUrl를 요청하는 API입니다.")
  @ApiResponses({
          @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON200",description = "OK, 성공"),
  })
  public ApiResponse<S3ResponseDTO.S3ResponseDTOforPut> getPreSignedUrlForPut(@RequestBody S3RequestDTO.S3RequestDTOForPut s3RequestDTO) {

       return ApiResponse.onSuccess(s3Service.getPresignedUrlForPut(s3RequestDTO));

  }

Put 메서드 용 presigned url 요청을 위한 API 를 구현했다.
이 API 를 통해 클라이언트는 저장하고 싶은 이미지에 대한 presigned url 을 전달 받을 수 있다.

s3Service

public S3ResponseDTO.S3ResponseDTOforPut getPresignedUrlForPut(S3RequestDTO.S3RequestDTOForPut s3RequestDTO) {


      String folderName = s3RequestDTO.getFolderName();
      String fileName = s3RequestDTO.getFilename();
      String type = "";


      if ( s3RequestDTO.getType() == 0 ) {
          type = String.valueOf(ImageType.BANNER);
      } else if (s3RequestDTO.getType() == 1){
          type = String.valueOf(ImageType.MAIN);
      } else {
          type = String.valueOf(ImageType.EVENT);
      }


      String uniqueFileName = UUID.randomUUID() + "-" + fileName;
      String keyName = folderName + "/" + type + "/" + uniqueFileName;
      String contentType = s3RequestDTO.getContentType();

      System.out.println(contentType);

      PutObjectRequest objectRequest = PutObjectRequest.builder()
              .bucket(bucket)
              .key(keyName)
              .contentType(contentType)
              .metadata(Map.of("Cache-Control", "max-age=864000")) // 캐싱 기간 10일
              .build();

      PutObjectPresignRequest presignRequest = PutObjectPresignRequest.builder()
              .signatureDuration(Duration.ofHours(2))  // 유효시간 2시간
              .putObjectRequest(objectRequest)
              .build();

      PresignedPutObjectRequest presignedRequest = s3Presigner.presignPutObject(presignRequest);
      String presignedUrl = presignedRequest.url().toString();

      s3Presigner.close();

      return S3ResponseDTO.S3ResponseDTOforPut.builder()
              .preSignedUrl(presignedUrl)
              .keyName(keyName)
              .build();

  }

url 을 생성할 때 keyName 설정해준 뒤 그에 해당하는 presigned url을 발급밥는다. 여기서 keyName은 s3 버킷에 저장되는 경로 라고 생각하면 된다.

진행하는 프로젝트에서는 서비스마다 이미지 등록할 때, banner 이미지/main 이미지/ event 이미지를 등록할 수 있어야 했기때문에 서비스명/이미지 유형(banner,main,event)/파일이름 형식으로 키네임을 설정해서 구분하였다.

저장할 이미지에 캐싱 설정, 컨텐츠타입, 키네임을 설정해준 뒤 해당 이미지를 업로드할 수 있는 presigned url 을 생성해 프론트에 url 과 keyname 을 전달해준다.

프론트는 전달받은 url로 PUT 메서드를 통해 s3에 이미지를 업로드하고, 전달받은 keyname은 서비스 최종 저장 API 에 요청값으로 설정하여 API에서 해당 서비스의 image keyname 으로 저장한다. 이렇게 되면, 서비스의 이미지를 불러올 때 keyname 을 통해 조회를 위한 url을 요청하여 해당 url을 통해 이미지를 조회할 수 있다.

📌 CloudFront를 활용한 최적화

나는 s3에 대한 cloud Front 배포 도메인을 설정하였다.
CloudFront를 사용하여 S3에 대한 직접적인 접근을 차단하고,
CloudFront는 캐싱을 적용하여 성능을 최적화하기 위해서이다.
CloudFront 배포 설정도 아래 블로그를 참고했다!
https://developer-been.tistory.com/38

추후에 CloudFront 에서 임시 도메인말고 진행하고 있는 프로젝트에 도메인으로 변경할 예정이다.

참고:
https://cn-c.tistory.com/119
https://developer-been.tistory.com/38

profile
기록의 중요성을 느끼고 행동으로 옮겨보려고 노력하지만, 쉽지 않아서 재미를 붙여보려고 하는 중 ... 입니다.

0개의 댓글