서론
해당 포스팅은 P-프로젝트를 진행했을때 프로필, 게시글 이미지 업로드를 위하여 S3를 사용했을때, Presigned URL 방식을 적용하여 업로드 속도 및 보안 측면에서의 이점을 가져왔었던 내용으로 작성해보려고 한다.
S3 이미지 업로드
먼저 Presigned URL 방식에 대해 얘기해보기전에, S3 업로드 방식에 대한 내용을 다루어보겠다.
AWS S3는 AWS Simple Storage Service의 줄임말로, 말 그대로 아마존에서 제공해주는 파일 서버의 역할을 하는 서비스라고 생각해주면 될거같다. 별도로 파일을 저장해주기에 개발자가 따로 용량을 추가하거나 성능을 높이는 작업을 최소화 할 수 있도록 도와준다
아래는 내가 노션에 문서화했던 S3 초기 설정 내용이다
Presigned URL 이란 ?
모든 객체는 기본적으로 비공개이며, 객체 소유자만 객체에 액세스 할 수 있다. 객체 소유자는 필요할 경우 자신의 보안 자격 증명을 사용하여 일정기간 동안 객체 다운로드를 허가하는 미리 서명된 URL 을 만들어 다른 사용자와 객체를 공유할 수 있다.
본론
이제 기존방식과 비교해서 Presigned URL을 적용하여 어떻게 개선했는지에 대해 구체적인 내용을 코드와 함께 공유해보려고 한다
기존 방식
기존의 방식은 사용자가 업로드한 MultipartFile을 서버가 직접 받고, S3Client를 이용해 AWS S3에 전송해주는 방식을 사용했었다
@Service
@RequiredArgsConstructor
public class ImageService {
private final S3ImageService s3ImageService;
private final ImageRepository imageRepository;
@Transactional
public Image save(MultipartFile image, User user) throws IOException {
ImageDto profileImageDto = s3ImageService.uploadImage(image);
return imageRepository.save(Image.from(profileImageDto, user));
}
public ImageDto getImage(MultipartFile image) throws IOException {
return s3ImageService.uploadImage(image);
}
}
S3ImageService에서 파일을 직접 업로드하게 되면서 파일 I/O, 예외 처리, 응답 처리 등등 서버단에서 많은 책임을 가지게 되었다
PutObjectRequest request = PutObjectRequest.builder()
.bucket(bucketName)
.key(fileName)
.contentType(file.getContentType())
.build();
s3Client.putObject(request, RequestBody.fromInputStream(file.getInputStream(), file.getSize()));
따라서 파일 전송이 서버를 거쳐 이루어지므로, 서버단에서 파일을 메모리에 로드하고 처리했어야했기 때문에 서버 네트워크 트래픽이 증가했고 대용량 이미지 혹은 다중 업로드 시 성능 저하 문제가 발생했었다
개선된 방식
Presigned URL 방식을 사용해서 서버가 이미지 파일을 직접 받기 보다는, 클라이언트에게 S3에 업로드할 수 있는 임시 URL만 서버단에서 생성해서 반환해주는 방식으로 개선하였다
public String createPresignedUploadUrl(String fileName) {
String uniqueFileKey = generateUniqueKey(fileName);
PutObjectRequest uploadRequest = PutObjectRequest.builder()
.bucket(bucket)
.key(uniqueFileKey)
.contentType("image/jpeg")
.build();
PutObjectPresignRequest presignRequest = PutObjectPresignRequest.builder()
.putObjectRequest(uploadRequest)
.signatureDuration(Duration.ofSeconds(250))
.build();
return s3Presigner.presignPutObject(presignRequest).url().toString();
}
위 코드는 개선한 코드의 일부로 크게 아래의 로직이라고 생각해주면 될거같다
1. 서버는 fileName만 입력받음
2. 파일 확장자를 기반으로 S3 key를 생성
3. Presigned URL을 생성하여 클라이언트에 전달
이제 Presigned 방식을 도입하면서 가져왔었던 두가지 이점을 설명해보려고 한다
보안상의 이점
먼저 보안상의 이점이다 S3 버킷의 객체는 모두 링크를 통해 접근할 수 있다. (조회, 다운로드, 업로드 등)
그러나 S3 객체는 기본적으로 비공개이고, 공개한다 해도 객체와 직접적으로 연결된 S3 링크가 외부에 유출되는 것은 보안상 치명적일 수 있다. 따라서 이때 Pre-Signed URL을 사용하면 해결해줄 수 있다.
객체 소유자는 필요에 따라 자신의 보안 자격 증명을 사용하여 일정기간 동안 (나는 유효기간을 250초로 설정했었다) 객체 접근을 허가하는 Pre-Signed URL을 만들어서 다른 사용자와 객체를 공유할 수 있기에, Pre-Signed URL은 설정한 기간 이후에 사라지기 때문에 외부 유출에도 큰 위험이 없다는 보안상의 이점이 있다
성능상의 이점
두번째로는 성능상의 이점이다. 앞서 얘기했듯이 초기에 사용한 방식처럼 서버가 직접 S3에 이미지를 업로드하는 방식을 사용한다면, 구현하기에는 간단할 수 있지만 사용자 수가 증가하거나 대용량 이미지가 늘어날수록 서버에 과부하가 생길 수 있다는 문제가 있다. Presigned URL을 사용하면 사용자가 직접 S3에 이미지를 업로드할 수 있게 되어, 서버에서는 결과적으로 이미지 파일을 전달받지도 않고, 저장하지도 않기때문에 트래픽과 서버 리소스를 대폭 줄일 수 있고 이는 실제 사용자 경험의 향상으로도 이어질 수 있었다
느낀점
실제 Presigned URL 방식을 도입해보니 예상 이상으로 체감 성능이 확 올라갔고, 프로필 이미지를 업로드할때 눈에 띄게((3초에서 0.1초 정도로👀) 빨라진 모습을 보고 뿌듯함도 느낄 수 있었다. 거기다가 일정 시간만 유효한 URL을 사용해주어서 보안까지 챙기고, 사용자 경험까지 여러가지 측면에서 만족스러운 결과였다 👍🏻
참고한 자료
https://docs.aws.amazon.com/ko_kr/AmazonS3/latest/userguide/using-presigned-url.html