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