이번 포스팅에서는 Spring을 사용한 프로젝트에서 S3에 업로드하는 세 가지 방법을 비교하고, 각 방법에 장 단점에 대하여 알아보고자 합니다.
파일을 S3에 업로드하는 방법은 크게 세 가지가 존재합니다.
세 방식을 하나씩 알아보겠습니다.
Stream Upload는 HttpServletRequest의 InputStream을 사용하여 AWS S3에 다이렉트로 파일은 전송하는 방식입니다.
작동 방식은 다음과 같습니다.
특징은 다음과 같습니다.
장단점은 다음과 같습니다.
장점 | 단점 |
---|---|
S3에 단일 요청으로 업로드하므로 코드 구현이 간단함 | 파일 전체를 한 번에 업로드하므로 대용량 파일 처리에서는 비효율적 |
작은 파일의 경우 빠르게 업로드 가능 | AWS SDK 내부 버퍼링이 존재하므로 완전히 메모리 사용이 0은 아님 |
메모리 사용을 최소화하도록 설정하면 대용량 파일도 메모리를 효율적으로 사용하면서 처리가 가능 | 업로드 중 중단 될 경우 전체 업로드가 실패함 |
따라서 Stream 업로드 방식은 단일 요청의 저용량 데이터 업로드에 효과적입니다.
MultipartFile Upload는 MultipartFile을 사용하여 AWS S3에 파일을 업로드하는 방식입니다.
작동 방식은 다음과 같습니다.
특징은 다음과 같습니다.
장단점은 다음과 같습니다.
장점 | 단점 |
---|---|
파일을 청크 단위로 업로드하므로 메모리를 효율적으로 관리할 수 있음 | 디스크 또는 메모리에 임시 파일을 저장하므로, 서버 부하가 발생 |
업로드가 중간에 실패한 경우, 실패한 청크만 재업로드가 가능 | 청크 수만큼 HTTP 요청이 발생함 (15MB 파일을 3MB 기준으로 청크를 나눈다면, 5번의 요청이 발생) |
대규모 파일 업로드에서 안정적으로 동작 |
따라서 MultipartFile 방식은 대용량 데이터 업로드에 효과적입니다.
PresignedURL Upload는 AWS S3에서 제공하는 파일 업로드 방식입니다.
업로드할 파일을 작은 part로 나누어 각 부분을 개별적으로 업로드하고, 발급된 URL을 통하여 직접 S3에 업로드하기 때문에 Spring 서버를 거치지 않고 업로드가 가능합니다.
모든 part가 업로드되고 난 후, AWS에서 하나의 객체로 조립하여 저장됩니다.
작동 방식은 다음과 같습니다.
특징은 다음과 같습니다.
장단점은 다음과 같습니다.
장점 | 단점 |
---|---|
서버에서 파일 데이터를 처리하지 않으므로, 메모리 / 디스크 사용량이 없음 | PresignedURL이 유출되면 누구나 업로드 가능 |
동시 업로드가 많아도 서버 부하가 존재하지 않음 | 클라이언트가 S3와 직접 통신하므로 네트워크 환경에 따라 성능 차이가 발생 |
서버는 PresignedURL을 생성하는 로직만 구현 | 서버를 거치지 않기 때문에, 어떤 파일이 올라가는지 정확히 알 수 없음 |
항목 | Stream | MultipartFile | PreSigned URL |
---|---|---|---|
메모리 사용 | 최소로 설정 가능 | 설정한 청크 크기 만큼 사용 | 없음 |
디스크 사용 | 없음 | 있음 | 없음 |
대용량 파일 처리 | 비효율적 | 효율적 | 클라이언트의 네트워크 환경 의존 |
재시도 가능 | 불가 | 가능 | 클라이언트가 재시도 |
권장하는 파일 크기 | 5MB 이하 | 5MB 이상 ~ 5GB 이하 | 제한 없음 |
보안 | 서버에서 처리하므로 안전 | 서버에서 처리하므로 안전 | URL 유출 시 위험 |
Stream 업로드 방식의 경우 대용량 파일 처리 측면에서는 비효율적입니다. 따라서 단순 이미지 파일, 텍스트 파일과 같은 크기가 작은 파일을 업로드 시 용이합니다.
MultipartFile 업로드 방식의 경우 대용량 파일 처리 측면에서 효율적입니다. 하지만 디스크를 사용하기 때문에 서버에 부하가 발생할 수 있다는 단점이 존재합니다. 따라서 해당 문제를 해결한다면, 대용량 파일을 업로드 시 용이합니다.
PresignedURL 업로드 방식의 경우 서버 부하가 발생하지 않고, 대용량 파일 처리 측면에서 효율적이라는 장점이 존재합니다. 하지만 클라이언트의 네트워크 환경에 의존해야 하고, 비교적 많은 HTTP 요청이 발생한다는 단점이 존재합니다. 대용량 파일 업로드 시 용이합니다.
위 내용을 바탕으로, 실제 Spring 프로젝트를 구현하여 실제로 어떻게 동작하는지 테스트를 진행해보겠습니다.
테스트에서는 Stream 업로드 방식, MulitpartFile 방식을 비교하는 테스트를 하겠습니다.
PresignedURL의 경우, Client에서 직접 S3에 업로드하는 방식이기 때문에, 테스트에서 제외하였습니다.
프로젝트에 Dependency를 추가합니다.
dependencies {
...
// AWS
implementation 'io.awspring.cloud:spring-cloud-aws-starter:3.1.1'
// S3
implementation 'software.amazon.awssdk:s3:2.31.0'
}
AWS S3와 연결하기 위하여 환경변수를 설정합니다.
cloud:
aws:
credentials:
access-key: ${AWS_S3_ACCESS_KEY}
secret-key: ${AWS_S3_SECRET_KEY}
s3:
bucket: ${AWS_S3_IMAGE_BUCKET}
region:
static: ${AWS_REGION}
AWS S3에 업로드하기 위하여 Config 파일을 생성하고, 업로드에 필요한 S3Client을 Bean으로 등록합니다.
@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 S3Client s3Client() {
AwsCredentials awsCredentials = AwsBasicCredentials.create(accessKey, secretKey);
return S3Client.builder()
.region(Region.of(region))
.credentialsProvider(StaticCredentialsProvider.create(awsCredentials))
.build();
}
}
테스트는 다음과 같이 진행됩니다.
처리 속도는 각 메서드의 처리 시간을 사용했습니다.
메모리 사용량은 Grafana와 Prometheus를 사용하여 확인해보겠습니다.
테스트는 Postman을 통하여 API를 호출해보았습니다.
Stream 업로드 방식을 구현한 예제 코드는 다음과 같습니다.
public Double uploadViaStream(HttpServletRequest request, String fileName) throws IOException {
double startTime = System.currentTimeMillis();
PutObjectRequest putObjectRequest = PutObjectRequest.builder()
.bucket(bucketName)
.key(fileName)
.contentType(request.getContentType())
.contentLength(request.getContentLengthLong())
.build();
s3Client.putObject(putObjectRequest, RequestBody.fromInputStream(request.getInputStream(), request.getInputStream().available()));
double endTime = System.currentTimeMillis();
return (endTime - startTime) / 1000;
}
MultipartFile 업로드 방식을 구현한 예제 코드는 다음과 같습니다.
메모리 사용량의 극적인 효과를 반영하기 위하여, fileByte를 받아와 메모리에 업로드한다고 가정하겠습니다.
public Double uploadViaMultipart(MultipartFile file) throws IOException {
double startTime = System.currentTimeMillis();
byte[] fileBytes = file.getBytes();
PutObjectRequest putObjectRequest = PutObjectRequest.builder()
.bucket(bucketName)
.key(file.getOriginalFilename())
.contentType(file.getContentType())
.contentLength(file.getSize())
.build();
s3Client.putObject(putObjectRequest, RequestBody.fromInputStream(
new ByteArrayInputStream(fileBytes), fileBytes.length));
double endTime = System.currentTimeMillis();
return (endTime - startTime) / 1000;
}
테스트는 Postman을 통하여 API를 호출해보았습니다.
Stream 업로드 방식
MultipartFile 업로드 방식
Stream 업로드 방식
MultipartFile 업로드 방식
Stream 업로드 방식
MultipartFile 업로드 방식
단위(초) | Stream 방식 | MultipartFile 방식 |
---|---|---|
10MB | 1.376 | 1.531 |
200MB | 38.118 | 33.168 |
1.5GB | 200.981 | 189.869 |
다음 결과와 같이, 10MB 정도의 파일의 경우 속도가 비슷하지만, 200MB, 1.5GB 파일 업로드의 경우 MultipartFile 방식이 비교적 빠른 모습을 확인할 수 있습니다.
메모리 사용은 1.5GB 파일을 업로드 하였을 때 Stream 방식 과 MultipartFile 방식의 사용량을 확인했습니다.
Stream 업로드 방식
MulitpartFile 업로드 방식
다음과 같이, Stream 방식의 경우 메모리 사용량이 비교적 일정한 모습을 확인할 수 있습니다.
하지만 MultipartFile 방식의 경우, 메모리 사용량이 파일 크기만큼 올라간 모습을 확인할 수 있습니다.
MultipartFile 방식에서도 메모리 사용량을 비교적 일정하게 유지할 수 있는 방법이 있습니다.
public Double uploadViaMultipart(MultipartFile file) throws IOException {
double startTime = System.currentTimeMillis();
PutObjectRequest putObjectRequest = PutObjectRequest.builder()
.bucket(bucketName)
.key(file.getOriginalFilename())
.contentType(file.getContentType())
.contentLength(file.getSize())
.build();
s3Client.putObject(putObjectRequest, RequestBody.fromInputStream(file.getInputStream(), file.getSize()));
double endTime = System.currentTimeMillis();
return (endTime - startTime) / 1000;
}
위 코드와 같이 file을 getByte
로 받아오지 않고, file.getInputStream()
을 사용한다면, 메모리 사용량이 Stream 방식과 동일하게 일정하게 유지가 가능합니다.
하지만 메모리를 사용하는 대신, 디스크에 임시 파일을 저장하고 이후 업로드가 성공하면 해당 임시파일을 삭제하게 됩니다.
따라서 디스크를 사용한다는 측면에서는 결국 서버에 부하를 준다는 점이 존재하게 됩니다.
이번 포스팅에서는 Spring에서 S3에 파일을 업로드하는 세 가지 방법을 알아보았습니다.
Stream 방식의 경우, 저용량 파일을 업로드할 시 유리합니다. 하지만 대용량 파일을 업로드하는데에는 비효율적인 단점이 있습니다.
MultipartFile 방식의 경우, 대용량 파일을 업로드할 시 유리합니다. 하지만 서버에 부하를 준다는 단점이 있습니다.
PresignedURL 방식의 경우, 서버의 부하 없이 S3에 클라이언트가 파일을 직접 올릴 수 있다는 장점이 있습니다. 하지만 그에 따른 보안 문제와 고아 객체 관리를 해결해야 한다는 단점이 존재합니다.
각 프로젝트의 환경에 따라 3가지의 방식 중 가장 맞는 방식을 선택하는 것이 좋습니다. 만약 저용량 파일 업로드가 대부분인 프로젝트에서 MultipartFile 또는 PresignedURL을 사용할 경우 오버 엔지니어링일 수 있기 때문입니다.
따라서 현재 진행하고 있는 프로젝트에서 맞는 방식을 사용하여 오버 엔지니어링을 막고 효율적으로 S3에 파일을 업로드할 수 있도록 하는 것이 중요합니다.
https://github.com/JEONTAK/spring-docs
제 Github에 해당 구현을 위한 프로젝트를 구성해놓았으니, 참고하셔도 좋을 것 같습니다.
해당 Repository의 S3Upload 입니다.