[Mingle] Project

Presigned URL 이용하여 파일 전송 최적화 하기

jiihyunn 2025. 3. 5. 23:39

Presigned URL 이란?

Presigned URL은 Amazon S3 객체에 대해 임시로 제한된 시간 동안 액세스 권한을 부여하는 서명된 URL입니다.

이를 통해 사용자는 AWS 보안 자격 증명이나 권한 없이도 지정된 시간 동안 객체를 다운로드하거나 업로드할 수 있습니다.

 

즉, 클라이언트는 서버를 거치지 않고 S3 버킷에 직접 접근하여 파일을 업로드하거나 다운로드할 수 있습니다.

 

Presigned URL 적용 이유

 

아래와 같이 서버에서 S3 버킷에 직접 파일을 업로드 하는 방식은 간단하게 구현할 수 있지만 몇 가지 단점이 존재합니다.

현재 저희 프로젝트는 파일을 빈번히 업로드 해야 하기 때문에(예: 프로필 이미지 업로드, 게시글 업로드 시 여러 개의 파일 업로드 허용) 아래의 단점들을 극복하고자 Presigned URL 방식을 도입하였습니다. 

  public FileMetadata upload(MultipartFile file) {
        String randomName = FileNameHandler.createFileName(file.getOriginalFilename());
        String fileName = DIR_NAME + DIR_DELIMITER + randomName;
        putObject(file, fileName);

        GetUrlRequest request = getGetUrlRequest(fileName);
        String path = s3Client.utilities().getUrl(request).toString();

        BufferedImage image = extractImageMetadata(file);
        return createFileMetadata(file, image, path);
    }

 

단점1 - 네트워크 병목, 지연 시간 증가

  • 파일이 클라이언트에서 서버를 거쳐 S3로 전송되므로, 네트워크 트래픽이 두 배로 발생합니다.
    • 클라이언트 → 서버
    • 서버 → S3
  • 이로 인해 네트워크 대역폭이 낭비되며, 업로드 속도가 저하됩니다.

단점2 - 비용 증가

  • 서버가 파일 전송 과정에서 중간자로 작동하기 때문에, 트래픽 비용이 더 많이 발생합니다.

단점3 - 유지보수 복잡성 증가

  • 업로드 중단이나 실패에 대한 처리를 서버가 모두 담당해야 합니다.
    -> 추가적인 로직(예: 업로드 재시도, 업로드 상태 확인)을 구현함에 따른 유지보수 복잡성이 증가됩니다.
  • 모든 업로드 데이터가 서버를 통해 전달되므로, 민감한 데이터가 서버에 노출될 위험이 있습니다.
    -> 서버에서 인증, 암호화 등 보안 처리를 신경 써야 하는 범위가 늘어나며, 관리 부담이 증가합니다.

단점4 - 서버 부하 증가

  • 대용량 파일 업로드나 다수의 동시 요청이 있는 경우, 서버 리소스(네트워크 대역폭, CPU, 메모리 등)의 사용량이 크게 증가합니다.
    -> 서버 리소스는 한정되어 있기 때문에, 동시에 실행 중인 다른 요청(예: API 응답, 데이터베이스 쿼리 처리 등)의 처리 속도가 느려질 수 있습니다.

 

Presigned URL 동작 방식

 

1. 클라이언트가 파일 업로드를 위해 서버에 presigned url을 요청합니다.

2. 서버는 s3에 presigned url 생성을 요청합니다.

3. s3는 권한을 확인한 후, presigned url을 생성하여 서버에 전달합니다.

4. 서버는 발급받은 presigned url을 클라이언트에 전달합니다.

5. 클라이언트는 발급받은 presigned url을 통해 파일을 업로드합니다.

 

Presigned URL 구현 방법

✔️ 의존성 추가

먼저 aws를 쉽게 사용할 수 있도록 build.gradle에 AWS SDK 의존성을 주입해 줍니다.

 

s3와 관련해서는 버전이 2개 존재합니다.

그 중 AWS SDK v1은 2024년 7월 31일부터 유지 관리 모드에 들어갔으며 2025년 12월 31일에 지원이 종료될 예정이기 때문에, 필자는 AWS SDK v2를 사용하였습니다.

implementation platform('software.amazon.awssdk:bom:2.20.29')
implementation 'software.amazon.awssdk:s3'

 

✔️ application.yaml 에 환경변수 등록

Amazon S3에서 버킷 생성 후, AWS IAM을 통해 S3에 접근 권한을 가진 사용자를 생성합니다.

이후 버킷 이름과 생성 시 사용한 region, 그리고 사용자로 엑세스 하기 위해 생성한 access, secrey key를 yaml파일에 입력해 줍니다.

spring: 
    cloud:
      aws:
        credentials:
          access-key: ${AWS_ACCESS_KEY}
          secret-key: ${AWS_SECRET_KEY}
        s3:
          bucket: ${BUCKET_NAME}
        region:
          static: ${AWS_REGION}

 

✔️ S3Config 설정

S3Presigner 객체를 통해 Amazon S3 SdkRequest에 서명함으로써, 
호출자에 대한 인증을 요구하지 않고 객체를 실행할 수 있습니다.
- amazon 공식문서
@Configuration
public class S3Config {

    @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 S3Presigner s3Presigner() {  // s3로 부터 presign url 가져올 때 사용
        AwsBasicCredentials credentials = AwsBasicCredentials.create(accessKey, secretKey);

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

    @Bean
    public S3Client s3Client() { // s3에 저장되어 있는 파일 삭제 시 사용
        AwsBasicCredentials credentials = AwsBasicCredentials.create(accessKey, secretKey);

        return S3Client.builder()
                .region(Region.of(region))
                .credentialsProvider(StaticCredentialsProvider.create(credentials))
                .build();
    }
}

 

✔️ S3Service 작성

 

📌 imageKey는 파일 이름을 의미합니다.

📌 필자는 image가 저장되는 directory를 구분하기 위해 imageType라는 객체를 만들어 활용하였으니,

이 글을 참고하여 구현하시는 분들은 fileName만 인자로 활용하면 될 것 같습니다!

@Service
@RequiredArgsConstructor
public class S3Service {

    private static final String DIR_DELIMITER = "/";
    private static final int URL_DURATION_MINUTES = 2;  // url 유효시간

    private final S3Presigner s3Presigner;
    private final S3Client s3Client;

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

	// s3로 부터 presignedUrl 얻어오는 메서드
    public String getPreSignedUrl(ImageType imageType, String fileName) {
        String imageKey = getImageKey(imageType, fileName);
        PutObjectRequest putObjectRequest = getPutObjectRequest(imageKey);
        PresignedPutObjectRequest preSignedRequest = getPresignedPutObjectRequest(putObjectRequest);
        return preSignedRequest.url()
                .toString();
    }

    public String getImageKey(final ImageType imageType, final String fileName) {
        return imageType.getType() + DIR_DELIMITER + fileName;
    }

	//s3 내 이미지 삭제하는 메서드
    public void deleteOrgImage(String imageKey) {
        DeleteObjectRequest deleteObjectRequest = DeleteObjectRequest.builder()
                .bucket(bucket)
                .key(imageKey)
                .build();

        s3Client.deleteObject(deleteObjectRequest);
    }

    private PutObjectRequest getPutObjectRequest(final String imageKey) {
        return PutObjectRequest.builder()
                .bucket(bucket)
                .key(imageKey)
                .build();
    }

    private PresignedPutObjectRequest getPresignedPutObjectRequest(final PutObjectRequest putObjectRequest) {
        return s3Presigner.presignPutObject(builder -> builder
                .signatureDuration(Duration.ofMinutes(URL_DURATION_MINUTES))
                .putObjectRequest(putObjectRequest)
        );
    }
}

 

테스트

1. presigned url을 발급받기 위해 저장할 파일 위치와 파일 이름을 서버에 request로 보내주면,
서버는 url을 응답으로 건네줍니다.

 

 

2.사진처럼 발급받은 preSignedUrl을 이용하여, PUT 그리고 binary 을 선택하여 이미지를 업로드 하면 정상적으로 200 응답을 받을 수 있습니다.

 

3. 이후 s3 버킷을 확인해보면 사진이 정상적으로 업로드 된 것을 확인해 볼 수 있습니다!

 

마무리

Presigned URL은 파일 업로드 및 다운로드 시 보안과 효율성을 동시에 만족시킬 수 있는 강력한 도구입니다.

이를 통해 서버의 부담을 줄이고, 사용자 경험을 개선하며, 안전한 접근 권한 관리를 실현할 수 있었습니다.

 

이번 글에서는 Presigned URL의 개념부터 적용 이유, 동작 방식, 그리고 구현 방법까지 살펴보았습니다. 

질문 혹은 수정해야할 부분이나, 제안하고싶은 부분이 있다면 댓글 부탁드립니다!


Reference

https://docs.aws.amazon.com/ko_kr/AmazonS3/latest/userguide/using-presigned-url.html

https://pgmjun.tistory.com/163

https://ipekogosu.tistory.com/59

https://solution-is-here.tistory.com/209#3.%20S3Presigner-1