AWS 인프라 위에서 채팅 이미지 업로드부터 조회까지

안녕하세요! 맘편한세상에서 맘시터 서비스를 개발하고 있는 손영철입니다 :)

맘시터에서는 2022년 6월 부터 부모-시터 회원 간 채팅방에서 이미지를 업로드할 수 있는 기능이 새로 추가되었습니다. 🥳

image 채팅방에 새로 적용된 이미지 업로드 기능

이번 글에서는 AWS 인프라 위에서 채팅 이미지 업로드부터 조회까지의 기능을 개발하면서 했던 고민과 실제로 운영하면서 겪은 시행착오를 통해 익힌 노하우를 가이드 형식으로 담아보려고 합니다 :)

그럼 시작합니다!


아키텍처

서비스에 맞게 아키텍처를 고려하자

아키텍처를 고려할 때 중요한 사항은 서비스의 특성입니다.

맘시터 서비스에는 이미지 업로드 기능을 채팅 기능에 추가하였는데요.
양방향 통신을 위해 웹소켓을 사용하는 채팅 서버의 경우 Stateless한 API 서버와는 달리 Stateful 하다는 특성이 있기 때문에 안정성이 중요합니다.
그렇기에 채팅 서버가 클라이언트로부터 이미지를 직접 전달받아 스토리지에 업로드 할지 따로 이미지 업로드를 관리하는 서버를 둘지는 서비스 운영에 있어 중요한 기술적 결정사항이었습니다.

트래픽이 몰리는 상황을 가정하였을 때 채팅 서버가 클라이언트로부터 직접 이미지를 받게된다면
네트워크 병목 및 이미지 데이터로 인한 서버 메모리 사용량 증가로 인해 채팅 기능 장애의 원인이 될 수 있다고 판단하여 채팅 서버와 이미지 업로드를 관리하는 서버가 분리된 형태의 아키텍처를 구성하였습니다.

image

간략하게 도식화된 아키텍처

위 아키텍처와 같이 Stateful 한 채팅 서버와 Stateless 한 이미지 서버를 분리하여 이미지 업로드 기능을 많이 사용하더라도 스케일 아웃이 쉽고 트래픽에 유연하게 대응할 수 있도록 구성하였습니다.
(추후엔 S3 presigned URL 기능을 사용해서 이미지 업로드에 대해서도 서버 부하를 최소한으로 줄이는 방식도 고려하고 있습니다)

이렇듯 아키텍처는 서비스의 특성에 따라 달라질 수 있기 때문에, 서비스의 특성을 잘 파악하고 아키텍처를 설계하시길 바랍니다.

S3

Object Key 패턴을 고려하자

코드를 작성할 때 모듈과 패키지를 잘 나누는 것이 중요하듯, S3에 업로드되는 Object에 대한 Key(경로) 패턴을 잘 정의하는 것도 중요합니다.

서비스에 맞게 새로운 종류의 리소스가 추가되거나 기능이 추가되었을 때도 유연하게 대응할 수 있는 Object Key 패턴을 정의하면 리소스 관리에 있어 부채가 줄어들게 됩니다.

예를 들면 아래와 같이 특정 채팅방과 리소스의 종류로 Object Key 패턴을 정의하면
특정 채팅방의 리소스를 리소스 종류 별로 관리할 수 있습니다.

  • /chat/room/{room id}/{resource type}/{file name}
    • /chat/room/9111/image/my_image.jpg
    • /chat/room/1109/video/my_video.avi

Public Access를 최소화하자

S3 사용 시 보안을 위해 외부에서 S3 Bucket으로의 직접적인 접근은 막아두는게 좋습니다.
가능한 public access를 최소한으로 줄이고 AWS CloudFront 등으로 제한적으로 접근하도록 설정하는 것을 고려하시길 바랍니다.

image

Object metadata 사용 시 영어 외 문자 사용을 유의하자

S3 Bucket에 업로드되는 Object에는 x-amz-meta-를 prefix로 갖는 사용자 정의 metadata를 지정할 수 있는데요.
이 사용자 정의 metadata를 통해 업로드한 리소스와 관련된 추가적인 컨텍스트를 Object와 함께 저장할 수 있습니다.

다만 이 Object metadata는 US-ASCII 만 지원하기 때문에,
AWS SDK 등을 사용하여 Object metadata를 한글과 같은 non-ASCII 문자로 지정하고 Object를 업로드하는 경우에 오류가 발생할 수 있습니다.
(관련하여 자세한 내용은 AWS 공식 문서를 확인하시기 바랍니다)

Java 애플리케이션에서 aws-sdk-java를 사용하여 Object의 metadata를 지정하고자 한다면, java.net.URLEncoder, java.net.URLDecoder 를 사용하는 것을 추천합니다.

(관련해서 aws-sdk-java에 이런 이슈가 올라온 적이 있는데 참고로 살펴보는 것도 좋을 것 같습니다 🙂)

CloudFront

CDN 사용을 고려하자

정적 리소스는 특성 상 수정보다는 조회가 많은데요.
그렇기 때문에 정적 리소스는 빠른 응답을 위해서 주로 CDN을 캐시 레이어로써 사용합니다.
빠른 응답 및 외부에서 S3 bucket으로의 직접적인 접근을 막고, 원본 리소스에 대한 부하를 낮추는 용도로도 CDN 사용을 고려하시길 바랍니다.

특히 AWS를 사용한다면 AWS에서 제공하는 CloudFront를 CDN으로 사용하는게 일반적이며 맘시터 서비스 역시 AWS CloudFront를 사용합니다.

Behavior 설정을 고려하자

BehaviorCloudFront가 어떻게 동작하는지에 대한 설정이며, 쉽게 말해 CDN 캐시에 대한 정책이라고 볼 수 있습니다.
이러한 Behavior 설정에 대해 충분히 파악한다면 CloudFront 사용에 있어서 다양한 기술적인 선택지를 선택할 수 있습니다.

대표적인 Behavior 설정으로 두가지만 언급하겠습니다.

Path pattern

CloudFront는 경로(path)에 따라 캐시 정책을 다르게 가져갈 수 있습니다.
리소스 마다 다른 캐시 정책이 필요한 경우 다른 Path pattern을 갖는 Behavior를 여러개 사용하는 방식을 고려할 수 있습니다.

image

Response headers policy

웹에서 외부 리소스를 요청하기 위해서는 CORS 설정이 필요합니다.
웹 서비스거나 앱 내에서 웹 뷰를 사용한다면 Response headers policy 설정을 잊지 마시길 바랍니다.

image

CloudFront에 도메인 네임 지정 시 N.Virginia 리전에 인증서를 등록하자

CloudFront는 특정 리전에 종속적인 서비스가 아니기 때문에 Route53에 등록된 domain으로 CloudFront의 Alternate domain name(CNAME)을 설정하려면 N.Virginia(us-east-1) 리전에 인증서를 등록해야합니다.

만약 Seoul(ap-northeast-2) 리전으로 AWS Certicate Manager에 등록된 인증서가 있다면 N.Virgina 리전으로 스위칭하여 기존 인증서와 동일한 domain name으로 인증서를 등록해주면 됩니다.

image

이미지 리사이징

이미지 리사이징을 고려하자

이미지 리사이징은 이미지를 보여줘야하지만 항상 원본 이미지를 보여줄 필요가 없는 경우 네트워크 사용량을 줄이기 위해 원본 보다 작은 크기로 이미지를 변환하는 기법입니다.

채팅방의 이미지와 같이 단순히 보여지는 다수의 이미지를 조회해야하는 경우 이미지 리사이징을 적용한다면 줄어든 데이터의 크기만큼 이미지 로딩이 더 빨리지고, 사용자의 네트워크 사용량을 줄여줄 수 있어 유저 경험을 위해서 고려할 수 있는 방법입니다.

image

이미지 리사이징 방식에 대해 이해하자

이미지 리사이징 방식은 여러가지가 있지만 잘 알려진 방식으로는 크게 2가지가 있습니다.

이미지 업로드 시 리사이징된 이미지 파일을 생성하는 방식

이미지를 업로드 할 때 원본 이미지와 정해진 크기로 리사이징된 이미지를 추가로 저장하는 방식으로
AWS를 사용하는 아키텍처라면 주로 S3 Object 생성 이벤트를 trigger로 하여 Lambda 함수에서 리사이징된 이미지를 생성 후 S3에 업로드하는 방식을 사용합니다.

image

AWS S3와 Lambda를 사용한 이미지 리사이징 아키텍처

원본 이미지 업로드와 동시에 리사이징된 이미지가 생성되기 때문에 리사이징된 이미지 조회 시 추가적인 작업 없이 조회할 수 있다는 장점이 있지만,
거의 조회되지 않는 이미지에 대해 리사이징을 적용한 경우에는 불필요하게 저장 공간을 낭비할 수 있다는 단점이 있습니다.
또한 리사이징 해야할 이미지의 크기가 변경되야하는 경우 기존에 리사이징된 이미지에는 변경을 적용하기가 번거롭다는 것이 이 방식의 단점이라고 볼 수 있습니다.

리사이징된 이미지 요청 시 리사이징된 이미지를 생성하는 방식

온 디맨드(on-demand), 온 더 플라이(on-the-fly) 방식이라고도 불리는 방식으로 리사이징된 이미지 조회 요청이 오면 그때서야 리사이징을 하는 방식입니다.

앞서 살펴본 리사이징된 이미지를 바로 생성하는 방식에 비해 거의 조회되지 않는 이미지의 리사이징으로 인한 인프라 리소스 낭비를 줄일 수 있다는 장점이 있지만,
첫 리사이징된 이미지 요청에 대해서는 리사이징 프로세스가 진행되어야하기 때문에 응답이 늦는 것은 이 방식의 단점입니다.

AWS의 CloudFront + Lambda@Edge 스택을 사용하면 S3에 리사이징된 이미지를 직접 저장하지 않고도 리사이징된 이미지를 CloudFront에 캐싱할 수 있으며
만약 캐싱된 이미지의 사이즈를 변경해야하는 케이스가 생긴다면 기존에 CloudFront에 캐싱된 리사이징 이미지 캐시만 삭제하면 되기 때문에 변경에도 유연하게 대응할 수 있습니다.

image

AWS CloudFront와 Lambda@Edge를 사용한 이미지 리사이징 아키텍처

채팅 서비스의 특성 상 오래된 메세지는 잘 확인하지 않기 때문에, 맘시터 채팅 서비스에서는 이 방식을 채택하여 사용하고 있습니다.

Lambda@Edge

제한사항을 숙지하자

Lambda@Edge는 일반적인 AWS Lambda 와는 다르게 지원하는 언어와 버전, 환경 변수 사용 제약 등의 제한사항이 있습니다.
그렇기 때문에 Lambda@Edge를 사용한 아키텍처를 꾸리기 전에 기술적인 제한사항에 대해 충분히 숙지하시기 바랍니다.
(특히 이미지 리사이징의 경우 Origin 응답 변경 시 응답의 크기가 1MB가 넘는지 확인하는 것도 중요합니다)

참고 링크

sharp

sharp는 Node.js 런타임의 Lambda 함수에서 이미지 리사이징 시 자주 사용되는 라이브러리입니다.
맘시터 서비스에서도 Lambda@Edge 함수에서 이미지 리사이징을 할 때 sharp를 사용하는데요.
관련하여 짧게 몇 가지 유의 사항을 공유하겠습니다.

install 시 platform 설정을 고려하자

sharp는 install 시 추가적으로 platform 설정을 할 수 있습니다.

image

https://sharp.pixelplumbing.com/install#cross-platform

sharp를 사용하는 런타임 환경에 따라 platform 설정을 확인하시길 바랍니다.

rotate 설정을 고려하자

sharp에서 제공하는 rotate 설정은 특정 각도로 이미지를 회전시키는 설정이지만
각도를 입력하지 않고 사용하면 이미지의 EXIF 데이터의 Orientation tag를 확인하여 회전된 이미지와 동일하게 회전시킵니다.

sharp(originalImage)
    .rotate() // <==
    .resize({ width })
    ...

원본을 회전시킨 이미지를 리사이징 할 때 그 결과물도 동일하게 회전되어있길 원한다면 rotate 설정을 확인하시길 바랍니다.

(참고: https://sharp.pixelplumbing.com/api-operation#rotate)


마무리

정리하고보니 전달하고 싶은 내용이 많아서 글이 길어졌네요.

이 글을 쓸 수 있었던 것은 단지 저 혼자 모든 것을 리서치하고 개발했기 때문이 아니라 주변 동료들의 조언과 아이디어 덕분입니다.
저 혼자서는 이렇게 인프라를 성공적으로 구성하고 운영할 수 없었을 거에요.
이 자리를 빌어 모든 팀원들에게 감사의 인사를 전달하고 싶습니다 :)

그리고......
빠르게 성장 중인 맘시터 서비스를 함께 만들어갈 동료를 찾고있습니다!!
공고는 여기에서 확인하실 수 있습니다! (많은 관심 부탁드릴게요!!)

마지막으로 긴 글 읽어주셔서 감사합니다 :)

이미지 기능 개발
AWS