개요
"현재 자신의 상황에 맞는 식당 추천 시스템"을 개발하기 위해 지금까지 식당, 리뷰 크롤링 및 기본적인 벡터 기반 유사도 검색에 대해 알아보았습니다.
이번에는 작성한 크롤링 코드를 기반으로 ETL 데이터 파이프라인을 구축하고 데이터 수집 자동화 시스템을 AWS 상에서 구축하려고 합니다.
AWS 크롤링 환경 선택
먼저 AWS에서 크롤링을 진행할 환경을 선택해야 합니다.
AWS에서 크롤링을 진행할 때 대표적인 선택지는 두 가지 정도가 있습니다.
- AWS Lambda
- AWS Batch
먼저 AWS Lambda는 서버리스 컴퓨팅 플랫폼으로 함수 형태로 코드를 작성해두면 컴퓨팅 자원을 직접 관리할 필요없이 쉽게 코드를 실행할 수 있습니다.
AWS batch는 대규모 배치 작업을 관리하고 실행하기 위한 클라우드 기반 서비스로 작업 정의를 통해 쉽게 작업을 실행하고 내부에 Job queue도 존재하기 때문에 대규모 작업을 안정적으로 실행할 수 있습니다.
🛠️ 첫 번째 시도 - AWS Lambda와 S3 trigger를 활용한 크롤링

처음에는 AWS Lambda와 S3 trigger를 통해 크롤링을 수행하려고 시도했습니다.
Lambda에서 크롤링 후 S3에 저장하면 trigger를 통해 Lambda에서 임베딩 후 데이터베이스에 저장하는 구조입니다.
기본적인 MVP 데이터 파이프라인을 만들기 가장 쉽다고 생각했습니다.
AWS Lambda에 코드를 올려 실행하는 방식에는 두 가지가 있습니다.
.zip archive를 활용하는 방식과 docker image를 활용하는 방식입니다.
이 두 가지 방식의 가장 큰 차이점은 용량입니다.

.zip archive 방식은 레이어 및 사용자 정의 런타임을 포함한 배포 패키지 콘텐츠의 최대 크기가 250MB이고
docker image 방식은 모든 레이어를 포함한 최대 압축 해제 이미지 크기가 10GB입니다.
아래 공식 문서에서 자세한 내용을 확인할 수 있습니다.
https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-limits.html
Lambda quotas - AWS Lambda
The Lambda documentation, log messages, and console use the abbreviation MB (rather than MiB) to refer to 1,024 KB.
docs.aws.amazon.com
그리고 앞선 포스팅에서 저희는 playwright를 사용하고 있다고 했습니다.
playwright 라이브러리 뿐만 아니라 chromium 브라우저 또한 사용하고 있기 때문에 일반적인 .zip archive 방식의 최대 크기인 250MB를 훌쩍 넘어가기 때문에 .zip archive 방식은 사용할 수 없습니다.
그래서 Lambda를 사용하여 크롤링 할 때 저희의 선택지는 docker image뿐이었습니다.
하지만 docker image 방식 또한 어려움이 있었습니다.
🚧첫 번째 어려움 - 로컬 테스트의 어려움
첫 번째 겪은 어려움으로 로컬 테스트의 어려움이었습니다.
우선 AWS Lambda라는 환경 자체는 EC2와 같이 우리가 직접 인프라를 관리할 수 있는 게 아니라 AWS에서 관리를 해주기 때문에 마음대로 세세한 설정을 할 수가 없습니다.
이러한 점에서 로컬 환경과 배포 환경에서의 차이가 발생했습니다.
AWS Lambda라는 특수한 환경 속에서는 로컬에서 실행되는 모든 docker image를 실행할 수 없었습니다.
AWS Lambda에서 일반적인 docker image를 실행할 수 없고 추가적인 과정을 거쳐야 합니다.

docker image를 AWS Lambda 환경에 맞추려면 Runtime Inteface Client가 필요합니다.
이를 사용하기 위해 awslambdaric를 설치하고 Dockerfile을 다음과 같이 작성해야 합니다.
# Define custom function directory
ARG FUNCTION_DIR="/function"
FROM python:3.12 AS build-image
# Include global arg in this stage of the build
ARG FUNCTION_DIR
# Copy function code
RUN mkdir -p ${FUNCTION_DIR}
COPY . ${FUNCTION_DIR}
# Install the function's dependencies
RUN pip install \
--target ${FUNCTION_DIR} \
awslambdaric
# Use a slim version of the base Python image to reduce the final image size
FROM python:3.12-slim
# Include global arg in this stage of the build
ARG FUNCTION_DIR
# Set working directory to function root directory
WORKDIR ${FUNCTION_DIR}
# Copy in the built dependencies
COPY --from=build-image ${FUNCTION_DIR} ${FUNCTION_DIR}
# Set runtime interface client as default command for the container runtime
ENTRYPOINT [ "/usr/local/bin/python", "-m", "awslambdaric" ]
# Pass the name of the function handler as an argument to the runtime
CMD [ "lambda_function.handler" ]
물론 이렇게 복잡하게 작성하기 싫다면 Runtime Interface Client가 포함된 python 이미지를 사용하는 방법도 있습니다.
그리고 docker image를 빌드할 때도 다음과 같은 추가적인 옵션을 추가해야 합니다.
docker buildx build --platform linux/amd64 --provenance=false -t docker-image:test .
또한 이렇게 AWS Lambda 환경에 맞추어 작성된 docker image 파일은 로컬에서 바로 실행할 수 없습니다.
로컬에서 이 image를 실행하려면 먼저 Runtime Interface Emulator를 설치한 뒤
mkdir -p ~/.aws-lambda-rie && \
curl -Lo ~/.aws-lambda-rie/aws-lambda-rie https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/latest/download/aws-lambda-rie && \
chmod +x ~/.aws-lambda-rie/aws-lambda-rie
아래와 같은 명령어를 입력해야 합니다.
docker run --platform linux/amd64 -d -v ~/.aws-lambda-rie:/aws-lambda -p 9000:8080 \
--entrypoint /aws-lambda/aws-lambda-rie \
docker-image:test \
/usr/local/bin/python -m awslambdaric lambda_function.handler
docker image를 AWS Lambda에 올리기 위해 굉장히 번거로운 작업들이 많은 것을 확인할 수 있습니다.
물론 로컬 테스트가 조금 번거롭긴 하지만 못 할 정도는 아닙니다.
🚧 두 번째 어려움 - Lambda 최대 실행 시간 15분
결정적으로 AWS Lambda를 포기하게 된 이유는 AWS Lambda의 경우 함수의 최대 실행 시간이 15분이라는 점이었습니다.
제가 크롤링 해야하는 정보는 식당과 리뷰 정보인데 우선 식당의 경우 한 번에 크롤링 하는 횟수가 많지 않아 15분 제한 시간에 걸리는 경우는 많지 않았습니다.
하지만 가장 큰 문제는 리뷰 크롤링이었습니다.
식당마다 인기 식당의 경우 수천 개의 리뷰를 가지고 있었고 이전 포스팅에서 언급했듯이 약 1,250개 리뷰를 크롤링 했을 때 속도가 급격하게 떨어지는 현상이 있었습니다.
https://growth-coder.tistory.com/357
식당 추천 시스템 개발기 #2 - 네이버 식당 리뷰 정보 크롤링
개요이번에 새로운 프로젝트를 시작하게 되었습니다. 프로젝트 주제는 "현재 자신의 상황에 맞는 식당 추천 시스템"입니다. 이 시스템을 만들기 위해 해당 식당에 대한 분위기, 특징과 같은 정
growth-coder.tistory.com
1,250개 리뷰가 넘어가게 될 경우 크롤링 속도가 급격하게 떨어지게 되고 Lambda 제한 시간인 15분을 넘겨버리게 됩니다.
그렇다면 리뷰를 한 번에 크롤링 하지 않고 페이지네이션을 통해 여러 개의 Lambda를 실행하는 방법도 생각해 볼 수 있습니다.
하지만 리뷰 크롤링 포스팅에서도 언급했던 것처럼 네이버 리뷰는 페이지네이션을 사용하기가 어려웠습니다.
기본적으로 페이지가 존재하지 않고 계속 스크롤을 내려 더보기 버튼을 클릭하는 방식입니다.
물론 더보기 버튼을 눌러 API를 호출할 때 페이지네이션이 존재하지만 이 API를 직접적으로 쓰는 것은 제한되어 있어서 여러 개의 Lambda로 나누어 리뷰 크롤링하는 것은 굉장히 어려웠습니다.
그래서 저는 안정적인 크롤링을 위해 AWS Lambda를 활용하는 것을 포기하고 AWS Batch를 활용하기로 결정했습니다.
AWS Batch에 대해
먼저 AWS Batch에 대해 알아봅시다. 위에서 AWS Batch는 대규모 배치 작업을 관리하고 실행하기 위한 클라우드 기반 서비스라고 설명했습니다.
AWS Batch는 4가지 구성 요소를 가지고 있습니다.
1. 컴퓨팅 환경 (Compute Environment)
작업을 실행하는 컴퓨팅 환경에는 두 가지 종류가 있습니다.
- 관리형: AWS가 자동으로 인스턴스를 확장하고 관리하며, EC2 인스턴스 타입이나 Fargate 선택 가능
- 비관리형: 사용자가 직접 ECS 클러스터의 인스턴스를 설정하고 관리
2. 작업 (Job )
AWS Batch에 제출하는 작업 단위로 쉘 스크립트 혹은 Docker 이미지가 될 수 있습니다.
AWS Fargate 혹은 EC2 리소스에서 컨테이너 애플리케이션으로 실행됩니다.
3. 작업 정의 (Job Definition)
작업을 실행하기 위한 방법은 정의한 템플릿입니다.
docker image, vCPU, memory, 환경 변수와 같은 요소들을 미리 정의할 수 있고 작업 정의를 기반으로 작업을 실행할 때 이러한 요소들을 재정의 할 수도 있습니다.
4. 작업 대기열 (Job Queue)
AWS Batch에 작업을 제출하면 먼저 작업 대기열로 이동한 뒤 컴퓨팅 환경이 준비될 때까지 대기합니다.
대기열 간 우선순위를 설정할 수 있습니다.
queue가 존재하기 때문에 대규모 작업을 안정적으로 실행할 수 있습니다.

AWS에서는 AWS Batch를 활용하여 확장 가능한 웹 크롤링 시스템 아키텍처를 다음과 같이 제안하고 있습니다.

🛠️ 두 번째 시도 - AWS Batch를 활용한 크롤링
위 아키텍처를 참고해서 아키텍처를 다시 생각해보았습니다.

먼저 식당 크롤링과 리뷰 크롤링을 분리했습니다.
식당 데이터의 경우 변화가 많지 않으나 리뷰 데이터의 경우 하루에도 많은 리뷰가 새로 올라오기 때문에 데이터 수집 단계에서 매번 식당을 크롤링 할 필요는 없다고 생각했습니다.
그래서 두 과정을 분리하여 식당 크롤링 단계와 리뷰 크롤링 단계로 나누었습니다
물론 이 구조에서도 Lambda와 S3 trigger를 사용했습니다.
이전 단계에서는 Lambda가 직접 크롤링을 하는 역할을 수행했다면 이번에는 AWS Batch를 실행하는 중간 단계 용도로 사용했습니다.
확실히 AWS Lambda라는 제한된 환경에서 크롤링하는 것보다 EC2와 ECS로 실행하는 것이 안정적이었습니다.
하지만 이 구조 역시 문제가 있었습니다.
가장 큰 문제는 전체 워크플로우를 한눈에 파악하기 어렵다는 점이었습니다.
Lambda 함수들이 S3 이벤트로 인해 연쇄적으로 트리거되다 보니 각 함수는 독립적으로 실행되고 CloudWatch에도 개별적으로 로그가 쌓였습니다.
예를 들어 A → B → C → D 순서로 실행되는 ETL 파이프라인에서 C 단계가 실패했을 때 다음과 같은 어려움이 있었습니다.
- 어느 단계에서 실패했는지 추적하기 어려움
- 각 단계는 로그가 독립적으로 쌓임
- workflow 진행 상태를 한 눈에 보기 어려움
또한 에러 처리도 직접 구현해야 했습니다.
무엇보다 "지금 파이프라인이 어디까지 진행됐는지" 실시간으로 확인할 방법이 없다는 게 가장 불편했습니다.
그래서 저는 AWS Step function을 사용하기로 결정했습니다.
🛠️ 세 번째 시도 - Step Function 활용
우선 AWS Batch의 작업 정의에 다음 2가지 작업을 생성했습니다.
- 식당 크롤링
- 리뷰 크롤링
그리고 다음 두 가지 Lambda 함수를 생성했습니다.
- 리뷰 임베딩
- 임베딩 데이터 DB 저장 API 호출

또한 각 단계 별로 결과물을 S3에 저장하였습니다.
Step Function을 사용한 이유는 전체 파이프라인 진행 과정을 실시간으로 모니터링 할 수 있을 뿐더러 장애 재시도 로직 또한 제공했기 때문입니다.
그리고 결정적으로 OpenAI Batch API 때문에 Step Function을 사용하게 되었습니다.
OpenAI Batch API는 API를 한 번 호출해서 여러 개의 API 요청을 묶어 한 번에 처리할 수 있는 API입니다.
저희는 하나의 식당에 대해 수백 개 리뷰를 수집하고 각 리뷰마다 벡터를 구한 뒤 평균을 내는 시스템입니다.
즉, 수백 개 OpenAI API 요청을 하나로 묶어 수행하는 OpenAI Batch를 사용하기 적합했습니다.
50%나 저렴하다는 장점이 있지만 비동기로 실행되기 때문에 주기적으로 HTTP 요청을 날려 진행 상황을 확인해야 하고 최대 24시간까지 걸릴 수 있다는 단점이 있습니다.
그리고 이 주기적으로 진행 상황을 확인해야 하기 때문에 만약 요청이 완료되지 않았을 때 재시도하는 로직이 필요합니다.
이러한 로직을 Step Function이 도와주기 때문에 Step Function을 도입하게 되었습니다.
하지만 이러한 구조는 아래와 같은 단점이 있었습니다.
- 수백 개 리뷰 임베딩 API의 높은 비용
- 적은 데이터를 보내도 완료까지 수 시간이 걸리는 OpenAI Batch API 문제
특히 OpenAI Batch API가 너무 오랜 시간이 걸려 하나의 workflow가 완료될 때까지 시간이 너무 오래 걸렸습니다.
그 와중에 식당에 대한 벡터 생성 방법 또한 변경이 되었습니다.
기존에는 모든 리뷰의 벡터를 구한 뒤 평균을 내서 식당에 대한 벡터를 생성하는 방식이었는데 이러한 방식은 벡터 고유한 특성을 잃어버릴 수 있다는 단점이 있어 모든 리뷰를 요약한 뒤 요약 리뷰의 벡터를 생성하는 방식으로 변경 되었습니다.
즉, Batch API를 사용할 필요가 없게 되었습니다.
🛠️ 네 번째 시도 - 각 단계마다 MQ를 두어 느슨한 결합 구현
이제 바뀐 리뷰 벡터 생성 방식을 반영하고 OpenAI Batch API에서 일반 API로 변경하여 아키텍처를 다시 설계했습니다.
우선 Step Function을 사용하지 않기로 결정했습니다.
step function을 사용한 결정적인 이유는 비동기로 수행되는 Batch API의 진행 상황을 확인하기 위해 복잡한 재시도 로직이 필요했지만 Batch API를 사용하지 않게 되면서 step functioin을 사용해야 하는 결정적인 이유가 없어졌습니다.
그리고 메시지 큐를 통해 느슨한 결합을 구현했습니다.
이전 아키텍처는 직접적으로 다음 단계를 호출하는 구조라 강력한 결합 관계가 형성되어 있었습니다.
이러한 구조는 사실 step function을 사용하면 각 단계마다 재시도 로직이 존재하기 때문에 크게 문제가 되지 않습니다.
하지만 바뀐 구조에서는 step function이 오히려 오버 엔지니어링이라고 판단했고 재시도 로직 및 장애 복구를 위해 메시지 큐를 도입하기로 결정했습니다.
특히 AWS에서 제공해주는 메시지 큐인 SQS는 장애 복구를 위한 visibility timeout 기능을 제공합니다.
그래서 최종 아키텍처는 아래와 같은 구조가 되었습니다.

또한 데이터를 저장할 때, EC2 API를 대량으로 호출하여 EC2 서버가 다운되던 현상도 메시지 큐를 통해 해결할 수 있었습니다.
이 과정은 다음 포스팅에서 다뤄보겠습니다.
마무리
이렇게 저희 프로젝트에서 ETL 데이터 파이프라인 아키텍처의 변화 과정에 대해 알아보았습니다.
AWS에서 데이터 파이프라인 구축하는 방법에는 여러가지 방법이 있으니 본인의 상황에 맞는 적절한 아키텍처를 선택하는 것이 중요한 것 같습니다.
