ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 온디맨드 이미지 리사이징 아키텍처로 트레바리 웹 페이지 성능 최적화 시키기
    핵인싸 개발자의 길/트레바리 활동(2019.8~2020.07) 2019. 12. 23. 01:53


    이번에 트레바리 서비스의 웹 성능 향상을 위해서, '온디맨드 이미지 리사이징'이라는 새로운 아키텍처를 구현하고 도입하게 되었다.
    그래서 이번 글을 통해 트레바리 서비스에 적용된 과정을 글로 공유해보려고 한다.

     

    트레바리 서비스의 이미지 처리에 대한 필요성

    현재 트레바리 홈페이지에서는 배너와 클럽 썸네일 등 이미지가 꽤 많이 사용되고 있다.
    그만큼 사용자가 처음 화면에 접속할 때, 이미지 수와 용량에 비례하여 다운로드 시간이 길어지고 페이지 로딩에 엄청난 영향을 미치고 있었다. 

    트레바리 홈페이지 메인 화면
    트레바리 멤버십 신청 페이지

    또한 기존에는 웹 화면과 모바일 화면 모두 동일한 고화질의 원본 이미지로 사용되었으며, 웹 화면 뿐 아니라 모바일 환경에서도 필요 이상의 고화질을 이용하므로써 불필요한 성능을 낭비하고 있었다.

    이러한 과도한 용량 사용과 로딩 시간을 조금이라도 줄이기 위해서는 새로운 이미지 처리 방식에 대한 최적화가 절실했고, 이미지 처리를 통해 어떻게 페이지 성능을 상승시킬 수 있을지 고민하게 되었다.

    기존의 이미지 요청 방식

    기존의 트레바리 서비스의 이미지 아키텍처는 위 그림과 같다.
    클라이언트에서 <img>태그 또는 background-image 속성을 통해 S3 버킷의 엔드포인트로 바로 접근하여 해당 이미지를 요청하는 것이다.
    그러면 S3 버킷에 저장되어 있는 원본 이미지를 클라이언트에게 전송하게 된다.

    기능적으로는 문제가 없지만, 저장되어 있는 고화질의 원본 이미지를 그대로 가져와서 사용하게되어 이미지를 다운로드하는 속도에 많은 영향을 미치게 된다.

     

    새로운 이미지 처리 방식 도입 과정

    이미지 로딩을 최적화 시키기위해서는 고화질의 이미지를 항상 그대로 사용하는 것이 아니라, 페이지 크기에 적당한 이미지 화질을 사용하기 위한 방법을 찾아야했다. 

    이미지 화질을 웹에 맞추면 모바일 화면에는 여전히 불필요한 고화질을 사용하게 되고, 모바일 화면에 이미지 화질을 맞추게 되면 웹 화면에서 유저에게 보이는 화질이 엄청 깨져서 보여진다.

    그래서 각 화면에 맞는 이미지를 처리하는 방식을 찾아보다가, 이미지 처리 전용의 Sharp라이브러리가 있는 것을 알게 되었다.
    이 이미지 라이브러리를 이용하여 동적으로 이미지 크기와 화질을 변경하여 사용할 수 있을 것이라 생각했다.

     

    첫 번째 시도 - 이미지 업로드 시, 서버에서 리사이징 처리 후 업로드

    처음에는 모바일 화면과 웹 화면에서 사용될 이미지 파일을 따로 저장하여 사용하는 방식을 생각했다.

    이미지를 업로드 시, 서버에서 원본 이미지를 여러 사이즈의 이미지 파일로 리사이징하여 각 파일을 생성 후 S3 버킷에 모두 저장한다.
    이후 유저가 이용 중인 페이지의 크기에 맞게 필요로 한 사이즈만을 S3 버킷에 요청하여 사용하게 된다.

    이렇게 하면 페이지 접근 시 해당 화면에 최적화된 이미지를 가져옴으로써 페이지의 성능을 향상시킬 수 있다. 

    그러나 서버에서 해당 이미지 리사이징 처리 후 리사이징된 이미지를 각각 업로드 처리를 할 시, 엄청나게 많은 성능과 시간이 소요하게 되는 문제가 있었다. 페이지 로딩 속도를 높이려고 업로드 성능을 포기하는 셈이었다.

    서버에서 해당 이미지 처리가 끝날 때까지 기다릴 필요 없이 Promise로 비동기 처리를 통해 해당 작업을 별도로 돌리면 되지 않을까 생각을 했으나, 비동기로 병렬 처리가 진행되는 것 또한 메모리 성능을 그만큼 잡아먹게 된다.
    (예를 들어 10명의 유저가 동시에 업로드 시, 이미지 크기 종류가 3개이면 총 30개의 프로미스가 동작하게 되는 것이다...)

    그러다가 AWS Lambda에서 Trigger 기능이 있는 것을 알게 되었다.

     

    두 번째 시도 - 이미지 업로드 후, Lambda Trigger를 이용하여 리사이징 처리

     

    Lambda Trigger를 이용하면 S3에서 객체 생성 및 삭제 시 Lambda 함수를 호출하여 이미지 리사이징 처리를 할 수 있다.
    이 방식이면, 기존의 서버에 영향을 주지 않고 람다 함수를 별도로 동작시켜서 리사이징 처리를 할 수 있게 된다.

    그러나 막상 이 방식을 도입하려고 하니, 이미 기존에 S3 버킷에서 사용 중인 수많은 이미지들도 모두 리사이징 및 저장 작업을 해주어야하는 큰 부가적인 작업이 있었다.

    또한 결정적으로는, 같은 이미지의 여러 파일을 생성하고 저장함으로써 이미지 수가 증가할수록 S3의 저장공간이 급격히 높아지는 치명적인 단점을 가지게 된다.
    해당 방식을 통하여 얻을 성능에 비해 추가 지불할 S3 요금이 현재로서는 많지는 않았지만, 페이지 성능을 위해 저장공간을 극도로 희생해버리는 만큼 좋은 방안이 아니라는 생각이 들었다.

     

    세 번째 시도 - 온디맨드 이미지 리사이징 아키텍처

    이미지를 리사이징하여 업로드를 할 수 있다면, 반대로 다운로드할 때 역시 리사이징 처리를 할 수 있지 않을까 라는 생각이 들었다.

    그리고 찾아본 결과 내 예상은 적중했으며, 이미지 요청 시 원하는 방식으로 리사이징 된 이미지를 받을 수 있는 '온디맨드 이미지 리사이징' 아키텍처를 알 수 있었다.

     

    온디맨드 이미지 리사이징은 앞에서 설명한 아키텍처와 반대로, 이미지를 다운로드할 때 리사이징 처리를 하게된다.

    유저가 이미지를 요청할 때, S3 버킷 내 고화질의 이미지를 Lambda를 이용하여 리사이징 후 유저에게 전송한다.
    그러면 S3 버킷에 화질 별로 이미지를 따로 중복 저장을 하지 않아도 되며, 업로드 시 서버의 성능 저하를 막을 수 있다.

    그러나 여기서 이런 의문이 들 수 있다.

    '이미지를 요청할 때마다 리사이징 처리 후 응답을 받게되니, 이 방법도 속도가 느리지 않는가?'

    위의 아키텍처 구성에 보면, 중간에 CloudFront라는 녀석이 있다. 이 녀석을 통해서 해당 리사이징 처리를 빈번히 수행하지 않고 이미지를 사용할 수 있다.

    💡CloudFront는 무엇인가요?
    CloudFront는 콘텐츠 데이터(이미지, 동영상 등)를 효율적으로 전달하기 위해 사용할 수 있는 CDN 서버이다. 전 세계에서 분산되어있는 네트워크 서버로, 특정 서비스의 리소스를 요청 시, 본연의 서버까지 접근하지 않고 제일 가까운 CDN 서버에서 캐싱된 리소스를 가져오게 되므로 응답 속도가 빠르다.

    💡전 세계로 분산시켜 운영하는 네트워크이면, 그만큼 불필요한 네트워크 이용이나 요금이 들어가지 않나요?
    CDN 서버는 실제로 서비스를 운영하는 서버와 별도로 운영되며 CDN 서버의 엔드포인트가 별도로 가지고 있다.
    요금 또한 운영시간이나, 네트워크 사용 대역폭 등이 아닌, 요청 횟수로 정산이 되기 때문에 전 세계로 분산시키는 네트워크라고 해서 문제되는 점은 없다.

     

    CloudFront를 이용하면, 리사이징 처리를 한번만 수행 후 캐싱처리를 하여 다음 요청시에는 따로 리사이징 처리를 하지 않고 미리 캐싱해놓은 이미지를 반환하면 된다.

    이렇게 서버 성능 저하, S3 저장용량 문제, 리사이징 처리 속도에 대한 문제를 모두 해결하면서 페이지 성능을 향상시킬 수 있게 되었다.

    아래에서 해당 방식에 대한 자세한 동작원리를 다시한번 설명하려고 한다.

     

    온디맨드 이미지 리사이징 동작 과정 설명

     

    클라이언트가 이미지를 요청할 때, 이미지의 포맷, 사이즈, 퀄리티 등을 쿼리 스트링으로 같이 전달하여 CloudFront로 이미지 요청을 한다.

    <img src="https://cdn.url.co.kr/myImage.png?q=90&w=150&h=150" />
    

     

    요청 URL 형태에 대한 설명 

    • https://cdn.url.co.kr - CloudFront 엔드포인트 URL.
    • myImage.png - S3에 저장된 이미지 엔드포인트. CloudFront를 통해서 S3 버킷에 접근하려면, CloudFront에 접근할 S3 버킷을 설정해야한다. 
    • q=90&w=150&h=150 - 해당 이미지 요청 시, 리사이징 처리할 옵션을 쿼리 스트링으로 전달.
      (q:quelity, w:width, h:height)
    여기서 주의해야 될 점은, 쿼리 스트링 순서가 변경되면 이미지가 캐싱 되어있어도 CloudFront가 다른 이미지 요청으로 인식한다.
    그래서 캐싱 적중을 하지 않고 리사이징 작업을 하게 되므로, 쿼리 스트링 순서는 통일시켜 사용하는 것이 효율적이다.

     

     

    해당 이미지가 CloudFront에 이미 캐싱이 되어 있다면, S3에 접근하지 않고 바로 요청한 이미지를 전달받을 수 있다. 캐싱이 되어 있지 않다면, 원본 데이터가 저장되어 있는 S3에 요청을 한다. 그러면 S3가 원본 데이터를 CloudFront에 이미지를 전달해주게 된다.

    CloudFront에는 Lambda@Edge의 'Origin Response(응답 받을 시 호출)' 트리거가 걸려 있다.
    해당 트리거는 CloudFront가 어떠한 요청에 대한 응답을 받으려고 할 때, 이 Lambda@Edge의 함수가 실행이 되는 방식이다.

    💡 Lambda와 Lambda@Edge의 차이는 무엇인가요?
    Lambda는 특정 리전 하나에만 존재하는 함수이다. 이 함수를 호출하려면, 그 리전의 Lambda에 직접 접근해서 호출을 해야 한다.

    Lambda@Edge는 CloudFront 안에 저장되어 있는 Lambda 함수이다. 버지니아 북부 리전에 존재하는 Lambda 함수가, 각 리전에 퍼져있는 CloudFront의 Edge Location(캐싱 서버)에 복제되어 사용되게 된다. 이는 제일 가까운 위치에서 사용되기 위해서이다.
    (무조건 버지니아 북부 리전에만 Lambda@Edge로 배포할 함수를 생성할 수 있다.)

     

    요청을 받은 S3는 CloudFront 안에 있는 Lambda@Edge의 함수를 실행키고, Lambda@Edge는 S3의 이미지 객체를 가져와서 리사이징 후 반환한다.
    그러면 이미지 객체는 CloudFront를 통해 유저에게 전송된다.

     

    # CloudFront에 캐싱이 되어 있지 않을 때의 과정

     


    # CloudFront에 캐싱이 되어 있을 때의 동작 과정

     

     적용 결과

    아래는 Lambda@Edge를 통한 이미지 리사이징을 도입하기 전과 후의 트레바리 메인 페이지의 로딩 시간을 비교한 내용이다.
    해당 비교 테스트는 실제 서비스 배포 전인 로컬 테스트 환경에서 둘 다 캐싱처리가 된 상태 측정 결과이다.

    기존의 이미지 요청 방식도, 브라우저 자체의 캐시 기능이 있기 때문에, CloudFront 도입에 대한 캐시 측정은 해당 테스트로 비교하기가 무의미하다. 하지만 CloudFront를 이용함으로써, 모든 유저와 캐시 공유가 되기 때문에 첫 유저만 캐싱 처리를 하면 그 이후의 유저부터는 바로 캐싱된 이미지를 가져올 수 있다.

     

    # 리사이징 도입 전 로딩 시간 측정

     

    리사이징 기능 도입하기 전에는 S3에 저장되어 있는 고화질의 용량을 그대로 이용하였다. 위 사진에 보이다시피 100KB를 훨씬 넘는 용량의 이미지가 몇 개 존재하는 것을 볼 수 있다.
    전체적으로 로딩이 끝나는 시간은 총 5.03초가 걸렸다.

     

    #리사이징 도입 후 로딩시간 측정

    리사이징 된 이미지의 용량이 대부분 100KB를 넘지 않게 되었으며, 그 결과, 전체적으로 로딩이 끝나는 시간이 2.77초까지 단축된 것을 확인할 수 있다.

    # 2019.12.25일 기준, 프로덕션 성능 테스트 추가 업로드 (개발자 도구-Network Tab이용)

    [기존 성능]

    • 평균 Resources : 12.82MB
    • 평균 Finish Loading Time : 7.648s

    [리사이징 적용 후 성능 측정]

    • 평균 Resources : 7.88MB
    • 평균 Finish Loading Time : 4.692s

    이미지 리사이징 적용 후, 웹 서비스에서 사용되는 리소스(apply 페이지 첫 화면 기준)가 약 4.7MB 감소, 로딩시간은 1.7초 감소되었다.

     

    향후 개선을 위한 방안

    # 이미지 포맷 형식을 webP로 변경

    요즘 차세대 이미지 포맷 형식인 webP에 대해 알아보니, jpg 포맷 형식보다 더 압축률이 뛰어나고 용량도 약 20% 절감할 수 있다고 한다. 그러나 이상하게 아직까지도 지원이 되는 브라우저가 많지 않아 언뜻 사용하기가 힘들어 보였다.

    출처: https://caniuse.com/

    그러나 다행히 현재 클라이언트를 동작시키고 있는 브라우저가 webP 포맷이 지원이 되는지 확인하는 방법이 여러 가지 나오는 것 같다.
    이를 이용하여 현재 브라우저가 Webp포맷 지원이 된다면 쿼리 스트링으로 webP를, 아니면 jpg를 보내도록 해서, webP지원이 되는 브라우저에는 조금 더 가벼운 이미지로 사용할 수 있도록 개선을 해볼 수 있을 것 같다.

    또한 현재 S3 버킷에 저장되어 있는 이미지도 전부 jpg 또는 png 포맷이므로, S3에 저장될 이미지도 webP포맷이라면 저장공간과 이미지 요청에 대한 속도도 더욱 개선될 것이라고 예상된다. (가능할지는 모르겠지만...)

     

    마치며

    이번 기능을 구현해보면서 AWS 기능들과 많이 친해질 수 있었던 것 같다. 특히나 서비스의 문제점을 직접 찾고 어떻게 문제를 해결할지 고민했던 과정과 해결하기 어려울 것만 같았던 기술적 문제들을 특정 기능들을 활용하여 직접 해결하면서, 스스로 문제를 해결하는 개발 역량을 크게 쌓을 수 있었던 기회였던 것 같다.

    앞으로도 이와 같은 멋진 개발 시스템을 경험해볼 수 있는 기회를 계속해서 찾아보고 시도해보도록 노력해야겠다.


    아래부터는 아키텍처 구현 중 어려웠던 부분, 구글링과 시간을 엄청나게 쏟았던 내용들을 정리하였다.

     

    구현 중 어려웠던 내용 정리

    #  apex 프레임워크 사용에 대한 어려움.

    Lambda 함수 기능을 생성하면, 웹 콘솔에서 직접 Lambda 함수를 정의할 수 있지만, 정책이나 라이브러리 배포 등의 불편함이 있어 대체적으로 프레임워크를 사용하여 더 쉽게 Lambda 기능을 구현하고 배포할 수 있다.

    이전에는  Serverless Framework만 이용해 봤었는데, Serverless Framework의 가장 큰 장점은 배포하기 전에 로컬에서 함수를 실행시켜볼 수가 있다는 것이었다. 로컬에서 실행을 시키면 가상의 엔드포인트가 생성되고, 해당 엔드포인트로 요청을 하면 로컬에서 Lambda함수가 실행이 된다.

    그러나 기존의 트레바리에서 사용되는 프레임워크는 Apex이기에, 처음 접해보는 만큼 Apex에 대해 알아봐야 하는 시간이 들었고, 또한 Apex는 테스트를 해보려면 'apex deploy FunctionName'으로 배포한 후, 'apex invoke FunctionName'으로 실행을 시켜볼 수 있었지만, 해당 실행 명령어로는 받아오는 event 파라미터 인자 값이 존재하지 않으므로 정확하게 테스트를 해볼 수 없다는 단점이 있었다.
    (향후에 Serverless Framework로 변경하는 것을 제안해보면 어떨지 생각 중이다...ㅎㅎ)

    또한 apex 명령어를 사용하기 위한 권한 설정에 어려움을 겪었다...

    stack overflaw 중

    apex 명령어를 사용하려고 하면 'export profile'을 하라는 에러 메시지가 나타났는데, 구글링을 해보니, apex 명령어를 사용하려면, ~/.aws/configure, ~/.aws/credentials의 프로필 환경변수를 맞춰야 된다고 대부분 나와있다.

    배포 명령어
    $ apex deploy functionName
    실행 명령어
    $ apex invoke functionName

    ~/.aws/configure 중
    ~/.zshrc 중

    그러나 나는 이러한 설정을 해도 apex의 명령어를 실행이 되지 않았고 끊임없이 구글링을 해야만 하는 삽질을 하게 되었다... 그 결과 아래와 같은 방법으로 해결이 가능했다.

    export AWS_ACCESS_KEY_ID= 'ACCESS_KEY_ID'
    export AWS_SECRET_ACCESS_KEY= 'SECRET_ACCESS_KEY'
    export AWS_REGION= 'REGION'
    

    해당 터미널에서 위 코드를 이용해 직접 AWS의 키와 리전을 설정해 주었고, 이 방식으로 apex의 권한이 설정되고 Lambda를 제어할 수 있었다. 

    어떻게 보면 정말 단순한 설정이지만, 아직 AWS 설정이 익숙지 않은 주니어 개발자인 나로선 많은 어려움을 겪어야 했던 부분이었던 것 같다.

     

    #  이미지 리사이징을 위한 라이브러리인 sharp 패키지가 설치되지 않음

    이것은 아직도 원인을 알 수가 없는 상태이다.
    npm을 이용한 sharp 패키지를 설치하려고 하는데 계속 아래와 같은 에러 메시지가 나타났다.

    xcode-select: error: tool 'xcodebuild' requires Xcode, but active developer directory '/Library/Developer/CommandLineTools' is a command line tools instance

    구글링을 하면, xcode를 다시 설치 하라던가, 업데이트를 하라던가, xcode 설정을 리셋하라는 내용이 대부분이다. 정말 구글링으로 나오는 내용을 하나같이 다 해봤는데도 계속 같은 메시지가 나타났으나, 이것저것 하다 보니 어쩌다 설치가 되었다..;;

    혹시 몰라 설치 시도 후 실패한 최근 상황부터, 설치가 된 상황 사이의 입력한 명령어들을 기록해봤다.

    $ sudo xcode-select —reset
    $ sudo xcodebuild -license accept
    $ sudo xcode-select —install (당연히 already install로 나옴)
    $ sudo xcode-select -s /Applications/Xcode.app/Contents/Develope

     

    #  'Error: function FunctionName: build hook: Hash: ~~~' 에러 메시지 출력

    apex를 이용한 Lambda 배포를 하려고 하니, 해당 메시지가 계속 출력이 되었다.

    $ apex deploy getResizingImage
    Error: function getResizingImabe: build hook: Hash: 457827ffa3284789

     

    함수 폴더 안에 function.json을 생성하지 않아서 일어난 에러였다. 이 부분은 Lambda 함수 실행 메모리와 runTime시간을 설정해 주는 부분이다.
    일반 Lambda 함수에는 해당 설정을 안해도 배포가 되어서 상관 없을 줄 알았는데, 정확하지는 않지만 해당 함수가 sharp라는 라이브러리를 사용하다 보니 따로 해당 설정을 요구하게 되는 것 같다.

     

    # Lambda was unable to delete lambda function

    이건 테스트로 돌리던 람다 함수를 삭제하려는데 나타난 에러이다.
    콘솔에서 함수를 삭제하려 하는데, 해당 메시지와 같이 삭제가 되지 않는 이유는 이미 CloudFront에 배포되어 대기 상태에 있기 때문이다.
    먼저 CloudFront에서 해당 Lambda@Edge가 걸려있는 behavior를 삭제한 후 1,2시간 뒤 시도를 하면 정상적으로 함수를 삭제할 수 있다. (behavior을 삭제하는 과정도 배포되는 과정이기에 시간이 꽤 걸린다.)

     

    참고한 자료

    # 당근마켓

     

    AWS Lambda@Edge에서 실시간 이미지 리사이즈 & WebP 형식으로 변환

    안녕하세요, 당근마켓에서 백엔드 서버 개발 인턴으로 근무하고 있는 Marco입니다. 저는 이번에 당근마켓 서비스의 썸네일 생성 방식을 On-The-Fly 이미지 리사이징으로 새롭게 구현하였습니다. 이번 글을 통해 그 과정을 공유하려고 합니다.

    medium.com

     

    # AWS Image Resizing Example

     

    Resizing Images with Amazon CloudFront & Lambda@Edge | AWS CDN Blog | Amazon Web Services

    Do you have lots of images that need to be modified before delivery? No problem with Amazon CloudFront and Lambda@Edge. Read more on how you can use our services to modify image dimensions, apply watermarks, or optimize formats based on browser support all

    aws.amazon.com

     

    반응형

    댓글

Designed by Tistory.