936 단어
5 분
AWS S3 파일 업로드, 다운로드
2025-04-12
2025-04-12

S3 Mapping Properties#

@Data  
@Component  
@ConfigurationProperties(prefix = "discodeit.storage.s3")  
public class S3Properties {  
  
  private String accessKey;  
  private String secretKey;  
  private String region;  
  private String bucket;  
  private Integer presignedUrlExpiration;  
}

위와 같은 Properties 클래스를 따로 만들었다. @ConfigurationProperties 이 부분을 보면 알겠지만 application.yml의 프로퍼티를 가져와 자동으로 매핑 시켜준다.

AWS S3 Config#

@Configuration  
@RequiredArgsConstructor  
@ConditionalOnProperty(name = "discodeit.storage.type", havingValue = "s3")  
public class S3Config {  
  
  private final S3Properties properties;  
  
  private static final long MB = 1024 * 1024;  
  
  @Bean  
  public S3AsyncClient s3AsyncClient() {  
  
    AwsBasicCredentials awsBasicCredentials = AwsBasicCredentials.create(  
        properties.getAccessKey(),  
        properties.getSecretKey());  
  
    return S3AsyncClient.crtBuilder()  
        .credentialsProvider(StaticCredentialsProvider.create(awsBasicCredentials))  
        .region(Region.of(properties.getRegion()))  
        .targetThroughputInGbps(0.1)  
        .minimumPartSizeInBytes(8 * MB)  
        .build();  
  }  
  
  @Bean  
  public S3TransferManager transferManager(S3AsyncClient s3AsyncClient) {  
    return S3TransferManager.builder()  
        .s3Client(s3AsyncClient)  
        .build();  
  }  
}

AWS에 파일을 업로드 하고 다운로드 하는 방식은 여럿 있지만, 그 중 AWS CRT 기반의 S3 Transfer Manager, S3AsyncClient를 사용하기로 했다.

credentialsProvider()#

credentialsProvider(StaticCredentialsProvider.create(awsBasicCredentials))은 AWS 서비스에 접근하기 위한 자격 증명을 세팅한다. 공식 레퍼런스에서는 DefaultCredentialsProvier.create()를 이용하고 있다. 이러한 방식은 ~/.aws/credentials 에 프로파일 설정 정보를 저장해서 사용한다. 여기서는 .env에 환경 변수를 저장하고 Properties를 이용해 불러올 것이므로 고정된 access key와 secret key를 사용하기 위해 StaticCredentialsProvider를 사용했다.

region()#

말 그대로 aws 서비스를 사용할 지역을 지정한다.

targetThroughputInGbps()#

S3 클라이언트의 목표 처리량을 0.1Gbps(100Mbps)로 설정했다. 레퍼런스에는 기본으로 20으로 되어있던데 이건 너무 높고 프리티어에서 사용 자체가 안될 것 같아 최소 속도라고 생각한 100Mbps로 설정했다.

minimumPartSizeInBytes()#

Multipart 데이터 업로드할 때 최소 크기를 8MB로 지정했다. 만약 대용량의 파일을 보내개 된다면 여러개의 파일들로 쪼개져서 나가는데 이때 쪼개져서 나가는 파일들의 최소 크기이다.

파일 다운로드#

@Override  
public ResponseEntity<?> download(BinaryContentDto file) {  
  try {  
    String presingedUrl = generatePresingedUrl(file.id().toString(), file.contentType());  
  
    HttpHeaders headers = new HttpHeaders();  
    headers.setLocation(URI.create(presingedUrl));  
  
    log.info("파일 다운로드: {}", file.fileName());  
    return new ResponseEntity<>(headers, HttpStatus.FOUND);  
  } catch (Exception e) {  
    log.error("파일 다운로드 실패: {}", file.fileName());  
    throw new AWSException(Instant.now(), ErrorCode.AWS_ERROR,  
        Map.of(file.fileName(), ErrorCode.AWS_ERROR.getMessage()));  
  }  
}

파일 다운로드에 대핸 코드는 그렇게 복잡하지 않다. AWS S3의 PresingedUrl을 얻어 리다이렉션을 시키면 된다. 그렇다면 generatePresingedUrl 메서드를 봐야 알 것 같다는 생각이 든다.

generatePresingedUrl()#

private String generatePresingedUrl(String key, String contentType) {  
  String bucketName = PropertiesUtils.getBucket();  
  
  try (S3Presigner presigner = S3Presigner.create()) {  
    GetObjectRequest objectRequest = GetObjectRequest.builder()  
        .bucket(bucketName)  
        .key(key)  
        .responseContentType(contentType)  
        .responseContentDisposition("attachment; filename=\"" + key + "\"")  
        .build();  
  
    GetObjectPresignRequest presignedRequest = GetObjectPresignRequest.builder()  
        .signatureDuration(Duration.ofMinutes(10)) // 10분동안 유효  
        .getObjectRequest(objectRequest)  
        .build();  
  
    PresignedGetObjectRequest presignedResult = presigner.presignGetObject(presignedRequest);  
    log.info("Presigned URL: [{}]", presignedResult.url().toString());  
    log.info("HTTP method: [{}]", presignedResult.httpRequest().method());  
  
    return presignedResult.url().toExternalForm();  
  } catch (Exception e) {  
    log.error("Presigned URL 생성 중 에러 발생 {}", e.getMessage());  
    throw new AWSException(Instant.now(), ErrorCode.AWS_ERROR,  
        Map.of(key, ErrorCode.AWS_ERROR.getMessage()));  
  }  
}

GetObjectRequest를 버킷과 키를 지정하고 파일을 다운로드 방식(attachment;)으로 제공하는 객체를 가져온다. 이 객체를 통해 GetObjectPresignRequest를 가져오게 되는데, 이때signatureDuration(Duration.ofMinutes(10))은 URL의 유효 기간을 10분으로 지정하겠다는 것이다. 마지막으로 presigner.presignGetObject(presignedRequest);를 통해 실제 Presigned URL를 얻는다.

파일 업로드#

@Override  
public UUID put(UUID fileId, byte[] bytes) {  
  String bucketName = PropertiesUtils.getBucket();  
  
  try {  
    Path tempFile = Files.createTempFile("s3-" + UUID.randomUUID(), ".tmp");  
    Files.write(tempFile, bytes);  
  
    UploadFileRequest request = UploadFileRequest.builder()  
        .putObjectRequest(b -> b.bucket(bucketName).key(fileId.toString()))  
        .source(tempFile)  
        .build();  
  
    FileUpload fileUpload = s3TransferManager.uploadFile(request);  
    CompletedFileUpload uploadResult = fileUpload.completionFuture().join();  
  
    Files.delete(tempFile);  
    log.info("파일 업로드: {}", uploadResult.response().eTag());  
  
    return fileId;  
  } catch (Exception e) {  
    log.error("파일 업로드 실패: {}", fileId);  
    throw new AWSException(Instant.now(), ErrorCode.AWS_ERROR,  
        Map.of(fileId.toString(), ErrorCode.AWS_ERROR.getMessage()));  
  }  
}

UploadFileRequest를 만들기 위해서는 반드시 Files 객체가 필요하다. 그런데 파라미터를 보면 알겠지만 파일이 넘어오는 것이 아니라 byte[]로 넘어온다. 그렇기 때문에 Files.createTempFile()로 임시 파일을 만들어 실제 업로드를 수행했다. 그런 다음 S3TransferManger에게 객체를 넘기고 completionFuture()메서드를 호출함으로써 파일 업로드가 완료된다.


레퍼런스#

https://docs.aws.amazon.com/ko_kr/sdk-for-java/latest/developer-guide/transfer-manager.html

AWS S3 파일 업로드, 다운로드
https://realits.me/posts/awss3fileuploadanddownload/
저자
realitsyourman
게시일
2025-04-12
라이선스
CC BY-NC-SA 4.0