들어가기 전

이번 포스팅에서는 Spring, S3 preSignedUrl를 활용하여 S3에 업로드된 파일을 다운로드하는 방법에 대해서 알아보겠습니다.

S3의 개념 및 버킷 생성하는 방법을 모르시는 분은 아래 포스팅을 참고하고 이 글을 읽으시는 것을 추천드리겠습니다.

 

 

https://hoestory.tistory.com/72

 

[Spring Boot] Spring Boot + Kotlin + AWS S3를 이용한 이미지 다루는 방법

들어가기 전 이번 포스팅에서는 Spring Boot와 Kotlin, AWS S3를 이용하여 이미지를 업로드, 조회, 삭제하는 방법에 대해 알아보겠습니다. AWS S3란? S3(Simple Storage Service)는 AWS에서 제공하는 클라우드 스

hoestory.tistory.com

 

예제에 들어가기 앞서 예제에서 사용되는 공통 설정에 대해 알아보겠습니다.

 

 

build.gradle

 

// S3
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'

 

 

application.yml

 

cloud:
  aws:
    s3:
      bucket: 버킷이름
    region:
      static: 버킷생성한 region
    credentials:
      access-key: 엑세스키 //IAM에서 생성한 엑세스키
      secret-key: 시크릿키 //IAM에서 생성한 시크릿키

 

 

S3Config

 

import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
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;

@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 AmazonS3 amazonS3Client() {
        return  AmazonS3ClientBuilder.standard()
            .withCredentials(new AWSStaticCredentialsProvider(
                                new BasicAWSCredentials(accessKey,secretKey)))
            .withRegion(region).build();
    }
}

 

 

Request

 

public record FileDownloadRequest(
    String objectKey
) {}

 

 

이제부터 Spring, S3 각각 사용하여 S3에 업로드된 파일을 다운로드하는 방법에 대해서 알아보겠습니다.

(참고 : 예제에서는 S3에 이미 이미지가 업로드된 것으로 간주하고 진행할 것입니다.)

 

 

 

 

 

Spring을 활용한 파일 다운로드

 

 

 

먼저 클라이언트가 objectKey(S3 key), 버킷 이름으로 서버에게 요청을 보내면 서버는 클라이언트로부터 들어온 objectKey와 버킷을 활용하여 S3에서 파일을 가져옵니다.

S3에서 파일을 가져오고 서버가 클라이언트에게 응답을 보낼 때 헤더에 *"Content-Disposition"을 설정을 한 뒤 클라이언트에게 응답을 하면 클라이언트는 "spring.png" 파일을 다운로드할 수 있습니다.

 

*Content-Disposition : 컨텐츠를 브라우저 내부에서 볼 것인지, 다운로드를 할 것인지 설정하는 헤더
1. inline - 기본값으로 브라우저 내부에서 보임.
2. attachment - 서버로부터 응답온 파일을 다운로드할 수 있음.
3. attachment; filename="파일명"  - "파일명"으로 파일을 다운로드할 수 있음

 

@RestController
@RequestMapping("/api/spring-file")
@RequiredArgsConstructor
public class SpringFileDownloadController {

    private final SpringFileDownloadService springFileDownloadService;

    @PostMapping
    public Resource downloadFile(@RequestBody FileDownloadRequest request,
                                   HttpServletResponse response){
        return springFileDownloadService.getFileResource(request.objectKey(),
                                                            response);
    }
}

 

  • Resource는 파일에 접근하는 인터페이스입니다. 클라이언트에게 응답을 할 때 바이너리 값을 응답합니다.

 

@Service
@RequiredArgsConstructor
public class SpringFileDownloadService {
    private final S3Processor s3Processor;
    
    @Value("${cloud.aws.s3.bucket}")
    private String bucketName;

    public Resource getFileResource(String objectKey, HttpServletResponse response) {
        byte[] objectBytes = s3Processor.getObjectBytes(bucketName, objectKey);
        InputStream inputStream = new ByteArrayInputStream(objectBytes);
        try {
            String originFileName = URLDecoder.decode(objectKey, "UTF-8");
            response.setHeader("Content-Disposition", String.format("attachment; filename=\"%s\"", originFileName));
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
        response.setContentType("image/png");
        return new InputStreamResource(inputStream);
    }
}

 

  • contentType : 다운로드할 파일의 타입으로 설정

 

@Component
@RequiredArgsConstructor
@Slf4j
public class S3Processor {

    private final AmazonS3 amazonS3;

    public byte[] getObjectBytes(String bucketName, String objectKey) {
        S3Object s3Object = amazonS3.getObject(bucketName, objectKey);
        S3ObjectInputStream s3ObjectInputStream = s3Object.getObjectContent();
        try {
            byte[] bytes = s3ObjectInputStream.readAllBytes();
            s3ObjectInputStream.close();
            return bytes;
        }catch (IOException e) {
            log.error("파일 가져오기 실패");
            return null;
        }
    }
}

 

  • bucketName과 objectKey를 활용하여 s3 버킷에 저장되어 있는 객체 조회

 

위 코드에서 S3로부터 받아온 객체의 용량이 작을 경우에는 문제가 없겠지만 나중에 용량이 큰 파일을 다운로드할 때 서버 메모리에 무리가 갈 수 있습니다.

서버 무리가 발생하는 이유는 "readAllBytes()"로 인해 메모리에 로드하기 때문입니다.

메모리를 효율적으로 사용하기 위해서는 스트림 기반으로 파일을 다운로드 받습니다.

스트림은 데이터를 작은 단위로 읽고 처리하면서 메모리에 모든 데이터를 로드하지 않습니다. 이를 통해 메모리 사용량을 줄이고 서버 리소스를 효율적으로 사용할 수 있습니다.

 

스트림

@Component
@RequiredArgsConstructor
@Slf4j
public class S3Processor {

    private final AmazonS3 amazonS3;
    
    public InputStream getObjectBytes(String bucketName, String objectKey) {
        return amazonS3.getObject(bucketName, objectKey)
            .getObjectContent()
            .getDelegateStream();
    }
}

 

 

@Service
@RequiredArgsConstructor
public class SpringFileDownloadService {
    private final S3Processor s3Processor;
    @Value("${cloud.aws.s3.bucket}")
    private String bucketName;

    public Resource getFileResource(String objectKey, HttpServletResponse response) {
        try {
            String originFileName = URLDecoder.decode(objectKey, "UTF-8");
            response.setHeader("Content-Disposition", String.format("attachment; filename=\"%s\"", originFileName));
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
        return new InputStreamResource(s3Processor.getObjectBytes(bucketName, objectKey));
    }
}

 

 

 

S3 preSignedURL을 활용한 파일 다운로드

 

 

클라이언트가 다운로드하고 싶은 파일(spring.png) 정보를 조회하기 위해 서버에 요청을 합니다.

서버는 S3에 클라이언트가 요청한 파일 정보에 대한 PresignedUrl 발급 요청 후 응답을 받습니다.

그리고 서버는 클라이언트에게 발급받은 Presinged URL을 응답해 줍니다.

 

 

 

 

클라이언트는 서버로부터 발급받은 PresignedURL을 통해서 직접 S3 접근하여 파일을 다운로드할 수 있습니다.

그리고 파일을 다운로드하기 위해서 서버와 통신하지 않고 S3에 직접 접근을 하여 서버 부하를 줄일 수 있습니다.

 

public record S3PresignedResponse(
    String presignedUrl
) {}

 

@RestController
@RequestMapping("/api/s3-presigned-url")
@RequiredArgsConstructor
public class S3PresignedController {
    private final S3PresignedService s3PresignedService;

    @GetMapping
    public S3PresignedResponse getPresignedUrl(FileDownloadRequest request) {
        return s3PresignedService.getPresignedUrl(request.objectKey());
    }
}

 

 

@Service
@RequiredArgsConstructor
public class S3PresignedService {

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

    public S3PresignedResponse getPresignedUrl(String objectKey) {
        return new S3PresignedResponse(s3Processor.getPresignedUrl(bucketName, objectKey));
    }
}

 

 

@Component
@RequiredArgsConstructor
@Slf4j
public class S3Processor {

    private final AmazonS3 amazonS3;

    public String getPresignedUrl(String bucketName, String objectKey) {
        Date date = new Date();
        long time = date.getTime() + 1000 * 60 * 5;
        date.setTime(time);

        URL presignedUrl = amazonS3.generatePresignedUrl(bucketName,
            s3Key,
            date
        );

        return presignedUrl.toString();
    }
}

 

amazonS3.generatePresignedUrl을 통해서 S3에 접근할 수 있는 presignedURL을 발급받을 수 있습니다.

generatePresignedUrl의 파라미터는 아래와 같이 구성되어 있습니다.

 

URL generatePresignedUrl(String var1, String var2, Date var3) throws SdkClientException;

 

  • var1 : bucketName
  • var2 : objectKey(s3 Key)
  • var3: presignedUrl 유효시간

유효시간을 설정하지 않으면 객체가 삭제되기 전까지 평생 접근이 가능합니다.

 

PresigendURL을 사용하면 서버 부하를 줄여준다는 큰 장점이 있지만 상황에 따라 주의하면서 사용해야 합니다.

 

※주의사항

PresignedURL은 누구나 URL을 알게 된다면 파일에 접근이 가능합니다. 

그래서 PresignedURL을 사용할 경우에는 특정 사용자만 봐야 하는 정보가 있는 파일에는 사용하는 것을 되도록 피해야 합니다.