Spring Boot

Presigned Url을 통한 파일 업로드 최적화

최-코드 2024. 11. 8. 16:06

필요성 

  • 기존의 방식으로 서버에서 s3로 업로드하는 방식을 사용하면 서버에서 이미지 파일을 받고 이를 s3에 업로드했어야 했다. 이 과정에서 서버 자원이 많이 소모되기에 presigned url을 통해 서버에서 직접 파일을 처리하지 않게 되어 서버에 부담이 많이 줄어든다.
  • 또한 aws에서 presigned url을 통해 파일을 업로드할 시에 처리 속도가 더 빠르다는 장점이 있다.

기본 로직

  1. 클라이언트에서 이미지 이름, 타입(옵션)을 보낸다.
  2. 서버는 이를 토대로 s3에게 presigned url을 요청한다.
  3. s3는 서버에 presigned url을 응답한다.
  4. 서버는 이를 클라이언트에 응답한다.
  5. 클라이언트는 presigned url에 직접 파일을 업로드하여 bucket에 업로드한다.

의존성

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'

 

설정파일

cloud:
  aws:
    s3:
      bucket: chois3
    stack:
      auto: false
    region:
      static: ap-northeast-2
    credentials:
      access-key: 
      secret-key:

 

ConfigClass

@Configuration
public class S3Config {

    @Value("${cloud.aws.key.access-key}")
    private String accessKey;

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

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

    @Bean
    public AmazonS3Client amazonS3Client() {
        BasicAWSCredentials basicAWSCredentials = new BasicAWSCredentials(accessKey, secretKey);
        return (AmazonS3Client) AmazonS3ClientBuilder.standard()
            .withRegion(region)
            .withCredentials(new AWSStaticCredentialsProvider(basicAWSCredentials))
            .build();
    }
}

 

Controller

@RestController
@RequiredArgsConstructor
@RequestMapping("/images")
public class S3ImageController {
    private final S3ImageService s3ImageService;

    @GetMapping
    public ResponseDto getPreSignedUrl(@RequestParam("imageName") String imageName) {
        PreSignedUrlResponseDto responseDto = s3ImageService.generatePreSignedUrl(imageName);

        return BodyResponseDto.onSuccess("PreSignedUrl 생성 및 조회 성공", responseDto);
    }
}

 

Service

@Service
@RequiredArgsConstructor
public class ImageUploadService {

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

    public PreSignedUrlResponseDto generatePreSignedUrl(String imageName) {
        GeneratePresignedUrlRequest generatePresignedUrlRequest = getGeneratePreSignedUrlRequest(imageName);

        String preSignedUrl = amazonS3.generatePresignedUrl(generatePresignedUrlRequest).toString();
        String imageSavedUrl = amazonS3.getUrl(bucket, generatePresignedUrlRequest.getKey()).toString();

        return PreSignedUrlResponseDto.of(preSignedUrl, imageSavedUrl);
    }
    
    private GeneratePresignedUrlRequest getGeneratePreSignedUrlRequest(String fileName) {
        String extension = extractExtension(fileName);
        System.out.println(extension);
        String savedFileName = UUID.randomUUID() + "." + extension;
        GeneratePresignedUrlRequest generatePresignedUrlRequest = new GeneratePresignedUrlRequest(bucket, savedFileName)
                .withMethod(HttpMethod.PUT) // put으로 요청 보내야 함
                .withExpiration(new Date(System.currentTimeMillis() + 180000)) // presigned url 만료 설정
                .withContentType(MimeTypeUtil.getMimeType(extension)); // content-type 설정 -> 다른 형식의 파일 방지
		
        // 익명 사용자도 업로드된 파일을 읽을 수 있도록 상태 설정
        generatePresignedUrlRequest.addRequestParameter(
                Headers.S3_CANNED_ACL,
                CannedAccessControlList.PublicRead.toString()
        );

        return generatePresignedUrlRequest;
    }

    public void deleteFile(String fileUrl) {
        String key = fileUrl.substring(fileUrl.indexOf("image/"));
        amazonS3Client.deleteObject(bucket, key);
    }

    public void deleteFiles(List<String> fileUrls) {
        if(fileUrls !=null && !fileUrls.isEmpty()) {
            List<KeyVersion> keyVersions = fileUrls.stream()
                .map(url -> url.substring(url.lastIndexOf("image/")))
                .map(KeyVersion::new).toList();
            DeleteObjectsRequest deleteObjectsRequest = new DeleteObjectsRequest(bucket)
                .withKeys(keyVersions);
            amazonS3Client.deleteObjects(deleteObjectsRequest);
        }
    }
}
  • 정적 url(s3에 저장된 업로드된 파일을 볼 수 있는 url)을 같이 보내줌으로써 프론트에서 presigned url을 가공하는 과정 없앤다.
  • 이를 통해 가공 과정에서의 오류나 문제를 걱정할 필요 없이 정확한 경로를 받을 수 있도록 할 수 있다.