썸네일 이미지 만들기

Dltmd202·2024년 7월 16일
post-thumbnail

썸네일의 필요성

요즘 아이폰을 기준으로 사진을 찍으면 대략 2MB~8MB 사이로 굉장히 용량이 크다.

물론 덕분에 우린 더 고화질의 사진을 볼 수 있게 해주니까 분명 좋기 때문에
2MB ~ 8MB를 렌더링 시켜주는 게 그렇게 무리는 아니다.

하지만 이렇게 리스트뷰로 보이는 데서 개당 2MB~8MB 정도되는 이미지를 렌더링하면 어떨까?

메모리를 사용하는데 무리가 됨은 물론, 어차피 작아서 고화질을 체감하기도 힘들다.

이렇게 잘 보이지도 않는데 고화질을 그대로 렌더링하는 건 분명 굉장히 비효율적이다. 때문에 우리 팀에서 리스트 뷰에서는 썸네일을 받아서 렌더링하도록 했다.

서버리스를 활용한 썸네일 작업

사진을 다운샘플링하는 연산은 굉장히 많은 리소스를 사용한다.

때문에 기존의 비즈니스로직을 동작시켜야하는 웹 어플리케이션에서 이를 병행하는 건 무리가 있다고 판단했다.

그렇게 되면 이제 두 가지 선택지가 생긴다.

  • 다운 샘플링만 진행하는 서버 인스턴스를 하나 더 만든다.
  • 서버리스로 다운 샘플링한다.

이 중 우리는 서버리스를 선택했다.

  • 비용을 줄 일 수 있다.
    • 24시간 쉬지 않고 가동되는 것이 아닌 이벤트가 발생했을 때만 컨테이너 형식으로 배포가 되고, 종료가 되면 자원이 회수되기 때문에 비용을 획기적으로 줄일 수 있다.
  • 자동 스케일 아웃
    • 우리가 서버를 만들어서 다운 샘플링을 진행하고 있는데 사용자가 갑자기 급증하면, 서버가 죽어버릴지 모른다. 서버리스는 요청이 몰리면 알아서 자원을 더 할당하기 때문에 갑자기 서버가 다운될 걱정이 없다.
  • 단점으로 꼽히는 인프라회사에 대한 강한 의존
    • 현재 NCP의 크래딧을 제공받고 있는 이상, 다른 인프라 회사로 옮길 이유가 없기 때문에 이러한 단점이 없어진다.
  • 또 하나의 단점으로 꼽히는 Cold Start 문제
    • Cold Start는 간단히 설명하면 서버리스가 시작되기 위해 자원을 할당(컨테이너 구동, 런타임 초기화, 컴파일) 하는 시간 때문에 응답에 지연이 발생할 수 있는 문제가 있다.
    • Cold Start 자체는 매번 발생하는 문제가 아닐 뿐만 아니라, 썸네일 자체는 조금 지연되어서 저장되고 로직을 수행하는데 큰 문제가 없다. (썸네일이 저장되어 있지 않는 레코드는 원본 이미지 URL을 반환하는 방법으로 해결가능)

서버리스의 효용성

사진을 위한 IO 작업은 굉장히 많은 리소스를 사용한다.

사실 거기까진 우리의 예측이었고, 현실적으로 메모리를 얼마나 사용할까 자체는 잘 알지 못했다.

하지만 한 번의 실수를 통해 나온 결과로 메모리 사용량이 엄청나다는 걸 알 수 있었다.

위 사진은 서버리스 테스트 중, 실수로 로직이 재귀적으로 돌게 만들어버려서 1분정도 동안 200회 정도의 다운샘플링 연산을 시켜버린 결과인데 150GB에 달하는 메모리를 썻다는 것을 알 수 있다.

서버리스 사용하기

서버리스를 사용하기 위해서는 하나의 함수 형태를 등록해주면 된다. Node.js, Python, Java 등등 몇 가지 런타임을 제공하지만, 우리는 파이썬을 선택했다.

  • 이미지 관련 모듈이 가장 잘 되어있을 것 같아서
  • NCP Cloud Functions 자체가 Python 예제가 가장 많은 것으로 보아 제일 안정적일 것 같아서

그렇게 최종적으로 이렇게 소스코드를 등록할 수 있다.

인터넷이 연결되면 외부 모듈이 필요하면 이렇게 requirements.txt 와 main.py 를 압축해서 올려도 사용할 수 있다.

등록 코드

import os

import boto3
from PIL import Image

service_name: str = 's3'
endpoint_url: str = 'https://kr.object.ncloudstorage.com'


def downsample_image(input_path: str, output_path: str,
					downsample_size: tuple[int, int] = (500, 500)):
    if not os.path.exists(os.path.dirname(output_path)):
        os.makedirs(os.path.dirname(output_path), exist_ok=True)
    with Image.open(input_path) as img:
        downsampled_img: Image.Image = img.resize(
        downsample_size, Image.Resampling.LANCZOS)
        downsampled_img.save(output_path)


def main(args):
    bucket_name: str = args['container_name']
    thumbnail_bucket: str = f"{args['container_name']}-thumbnail"
    object_name: str = args['object_name']

    s3: boto3 = boto3.client(
        service_name,
        endpoint_url=endpoint_url,
        aws_access_key_id=args['access_key'],
        aws_secret_access_key=args['scret_key'],
    )

    object_path: str = f"./{object_name}"
    thumbnail_path: str = f"./thumbnail/{object_name}"

    s3.download_file(bucket_name, object_name, object_path)
    downsample_image(object_path, thumbnail_path)
    s3.upload_file(thumbnail_path, thumbnail_bucket, thumbnail_path)

    return args
  • requirements.txt
boto3==1.29.5
botocore==1.32.5
jmespath==1.0.1
Pillow==10.1.0
python-dateutil==2.8.2
s3transfer==0.7.0
six==1.16.0
urllib3==2.0.7

압축 후 업로드

$ zip -r ds.zip __main__.py requirements.txt

실행

  • 6MB의 사진을 serverless라는 버킷에 업로드
    • 기존 호출이 있어서 Cold Start가 안걸린 상태
    • Cold Start가 걸리면 지연시간이 꽤 걸리는 경우도 있었다.
  • serverless-thumbnail이라는 버킷에 해당 사진의 썸네일이 48KB의 사이즈로 업로드 됨

0개의 댓글