들어가며
필자는 토이 프로젝트를 진행하는 것을 굉장히 좋아한다. 대부분의 토이 프로젝트에서 자주 구현하는 것 중 하나가 크롤러이다. 정보가 넘쳐흐르는 웹 세상에서 원하는 정보를 끌어와 가공하여 다양한 서비스를 제공할 수 있다는 점이 굉장히 흥미로웠기 때문에 많은 토이 프로젝트에서 크롤러를 구현하곤 했다. 크롤러를 만드는 데 node를 쓰기도 하고, python을 쓰기도 하고, Spring과 Kotlin 환경에서 스크래퍼를 구현해보기도 하는 등 거의 모든 프로젝트에 크롤러가 들어가는 것 같다.
이렇게 토이 프로젝트로 크롤러를 이것저것 만들다, 때는 작년 여름 시대생 앱 프로덕트에 쓰일 스크래퍼를 만들게 되었다. 단순히 스크래퍼 하나만을 만드는 것이 아니라 사용자들에게 다양한 서비스를 제공하는 애플리케이션의 유틸리티 중 하나로 학교 공지사항을 스크래핑해 오는 아키텍처를 처음부터 끝까지 만드는 것인데, 어떻게 구현하면 좋을지 굉장히 고민을 많이 했다.
정말 간단하게 스크래퍼를 구현해야 한다면 프레임워크 하나에 크롤링 라이브러리를 써서 웹 서버를 만드는 것으로 끝낼 수 있겠지만, K8s 환경에서 돌아가도록 구축해야 하며, 프로덕트 용이기 때문에 최대한 컴퓨팅 리소스를 덜 잡아먹도록 구현해야 했다. 이 외에도 스크래퍼는 스크래퍼의 역할만 하도록 구현해야 하는 등 다양한 조건을 만족시켜야 했는데, 굉장히 조사를 많이 했다.
스크래퍼를 어떻게 구현할 지에 대해서 조사하던 도중 AWS에서 포스팅한 재미있는 게시물을 보게 되었다. Serverless를 통해서 스크래퍼를 구현한 것이었는데, 그걸 참고하면서 굉장히 많은 도움이 되었다. 이번 포스팅에서는 스크래퍼를 어떻게 구현하는 지에 초점을 두지 않고 스크래퍼의 인프라를 어떻게 구축할 것인 지에 초점을 맞춰 전개될 것이다.
본론
먼저, AWS의 포스팅을 참고해 구현한 스크래퍼를 구현한 깃허브 레포지토리 링크를 첨부한다. node의 스크립트로 작성했으며 node의 패키지 매니저로는 yarn을 쓴다.
- node-crawler : https://github.com/marsboy02/node-scraper
스크래핑 기본
웹 사이트에서 다양한 정보를 스크래핑하기 전에 가장 먼저 눈여겨봐야할 것은 어떠한 데이터를 가져올 것인 지에 대해서이다. 누구나 당연하게 이렇게 하겠지만, 생각보다 실제로 뜯어보면 HTML의 요소들이 이상하게 배치되어 있는 경우도 있기 때문에 불친절하게 있을 것도 고려해둬야 한다. 이런 경우에 제대로 스크래핑할 수 있는지 확인해야 한다.
다음은 사이트가 정적인지 동적인지 확인하는 것인데, 일반적으로는 크롤링 라이브러리의 원리가 특정 웹사이트에 요청을 보내고 받은 HTML을 뜯어서 데이터를 가져오는 것이다. 요청을 보내는 웹사이트가 정적이라면 원하는 데이터가 HTML에 다 들어있겠지만, 동적이라면 간혹 원하는 데이터는 자바스크립트가 다 렌더링된 후에야 얻을 수 있는 경우가 있다.
이 경우에는 seleninum과 같이 직접 크롬을 띄워서 데이터를 가져오는 구조를 고려해야한다. headless chrome이라는 키워드를 찾아보면 도움이 될 수 있다. 이 외에 일반적으로 쓰는 라이브러리는 Spring의 Jsoup, node의 axios & puppeteer 등이 있다. 다음의 사이트를 참고하면 많이 쓰이는 스크래퍼 라이브러리를 참고할 수 있었다.
또 추가적으로 고려해야할만한 것은 웹사이트의 root 도메인에서 /robots.txt로 요청을 보내면 텍스트 파일을 받을 수 있는데, 이는 검색로봇에게 사이트 및 웹페이지를 수집할 수 있도록 허용하거나 제한하는 국제 권고안이다. 따라서 크롤링할 때 robots.txt에 적힌 규칙을 준수해야 한다. robots.txt 파일이 없다면 모든 콘텐츠를 수집할 수 있도록 간주하여 크롤링할 수 있다.
스크래퍼 아키텍처
간단하게 크롤링하는 방법은 위에서 설명한 대로 진행하면 될 것 같다. 이번에는 복잡한 구조의 인프라 아키텍처를 설계하는 방법에 대해서 알아보자. 어떤 서비스를 구현하고자 하는 지에 따라서 적절한 아키텍처를 설계하는 것이 큰 도움이 될 것이다. 간단한 스크래핑 서비스라면 굳이 복잡한 아키텍처를 택할 이유가 없을 것이다.
아래의 아키텍처들은 다양한 리서치를 통해서 스크래퍼를 어떤 식으로 발전시켜 나갈 수 있는 지에 대해서 필기해 둔 것으로 적절한 구조를 선택하는 것이 중요할 것이다.
스크래퍼 서버
가장 간단하게 스크래핑 서버를 만드는 경우를 생각해보자. 특정 URI로 요청이 들어오면, origin에서 HTML 데이터를 가져와서 응답으로 보내주는 웹 서버를 생각할 수가 있다.
실제로 위와 같은 구조로 유희왕 카드에 대한 정보를 스크래핑해서 사용자에게 데이터를 전송하는 서버를 위와 같이 간단하게 구축한 적이 있다.
해당 토이 프로젝트는 오로지 데이터의 크롤링만을 목적으로 진행되었기 때문에, 프레임워크 위에 크롤링 라이브러리를 올려 웹 서버를 실행시켜 두었다. AWS의 EC2를 사용하도록 구현해 두었는데, 가장 간단한 구조로 스크래핑만을 위한 서버를 만든다면 위와 같이 구현할 수 있을 것이다.
하지만 특정 서비스에 스크래퍼를 탑재하는 경우를 생각해보자. 여기서부터가 본론인데, 스크래퍼는 스크래퍼 자체만의 목적을 가지고 구현해야할 경우이다.
서드파티 스크래퍼
위와 같은 스크래퍼를 목적으로 만드는 서버가 아닌, 프로덕트의 서드파티로 스크래퍼를 만드는 경우 다양한 인프라 아키텍처에 대해서 고민을 했다. 온프레미즈나 컨테이너 환경 혹은 크론 작업을 하는 것인데, 순서대로 AWS의 EC2, ECS, Lambda에 대응하는 것들이다. 관련해서 참조할만한 포스팅의 링크를 달아 둔다.
- 참조 : https://aws.amazon.com/ko/blogs/architecture/serverless-architecture-for-a-web-scraping-solution/
위 포스팅을 요약하면 스크래퍼의 인프라를 어떻게 구축하냐는 것인데 크게 세 가지 옵션이 있다고 한다. 첫 번째는 온프레미즈로 AWS의 EC2가 해당하고, 두 번째는 docker 이미지 기반으로 AWS의 ECS에 해당하고, 마지막 옵션으로는 서버리스인 AWS의 Lambda에 해당하는 것으로 구현하는 것이다.
위 게시글의 주요한 요지는 스크래퍼를 구현하는 경우에는 Lambda를 쓰라는 것인데, 위 이미지를 보면 알겠지만, 람다는 15분 이내의 작업을 실행시킬 수 있으며 컴퓨팅 리소스를 굉장히 적게 사용한다는 것이었다. 즉, 간단한 스크립트를 돌리는 정도는 람다로 처리하는 것이 좋고, 스크래핑의 역할을 서버에 위임하거나, 스크래핑을 위한 서버를 만드는 것은 비효율적이기 때문에 이를 참고해서 아키텍처를 구현하기 시작했다.
시대생 애플리케이션의 방향성을 결정할 때, 공지사항 스크래퍼는 다른 서비스들과의 의존성이 없었으면 좋겠다는 기술적인 이야기가 나왔다. 따라서 스크래퍼는 온전히 스크래핑의 역할만을 수행하며 DB에 데이터를 쌓는 부분만 하는 것이다. 이 부분에 대해서는 크게 어려운 일이 아니지만 추가적인 요청 사항으로 K8s의 cronjob 환경에서 돌아가게끔 구현해 달라는 요청 사항이 있었다. ( 우리는 EKS를 사용하기 있기 때문이었다 )
K8s cronjob은 쿠버네티스에서 제공하는 스크립트의 종류로 정해진 시각에 도커라이징된 이미지를 띄우는 역할을 한다. 따라서 가장 큰 걸림돌은 stateless 하다는 것이었다. 풀어서 설명하면, 매분마다 스크래퍼가 실행되어, 공지사항에 대한 스크래핑을 수행하는데 문제는 이 매분마다 뜨는 스크래퍼 애플리케이션이 어디까지 스크래핑을 했는지 모른다는 것이다. 따라서 이러한 stateless 한 상황(매분마다 뜨는 pod들이 어디까지 했는지 모르는 상황)을 해결하기 위해서 큐나 redis를 써야 하지 않을까? 고민을 하고 있었는데, 마침 AWS에서 Serverless로 스크래퍼를 구현하는 포스팅을 많이 참고했다.
- 참고 : https://aws.amazon.com/ko/blogs/architecture/scaling-up-a-serverless-web-crawler-and-search-engine/
위 포스팅에서 제시한 Serverless 스크래퍼의 아키텍처는 다음과 같다.
위에서는 람다를 사용하는데, 람다가 뜰 때마다 앞서 뜬 람다가 어디까지 스크래핑했는지에 대한 맥락이 전혀 없기 때문에 이를 위해서 DynamoDB를 사용하는 구조였다. 해당 아키텍처를 참고하여 redis와 cronjob을 통해서 구현할 수 있지 않을까?라는 생각을 하게 되었고 포스팅을 많이 참고했다.
K8s를 사용하는 서버리스 스크래퍼 아키텍처
node 스크립트를 K8s의 cronjob으로 띄우는 구조를 포함한 구조는 위와 같다. FCM을 통해서 클라이언트에 공지사항에 대한 알림을 보내는 구조를 갖추기 위해서 위와 같은 아키텍처를 구현하게 되었다. 또한 앞서 스크래퍼가 DB에 공지사항을 쌓는다는 표현을 썼는데, 바로 RDS에 쌓는 것이 아니라, Utility server를 통해서 row를 쌓을 수 있게 구현이 되어있다.
이렇게 유틸리티 서버를 거쳐가면서 utility server가 이를 감지하여 웹훅으로 FCM(Firebase Cloud Message) SDK를 이용하게 된다. 이러한 과정을 통해서 매분마다 학교 공지사항을 크롤링하여 새로운 공지사항이 올라올 경우 사용자에게 푸시 알림을 보내주는 구조이다.
K8s를 사용하지 않는 경우
위에서는 K8s를 통해서 매분마다 크롤링을 수행하는 pod을 띄어두지만, 만약 K8s를 사용하지 않는 경우에는 batch 작업이나 crontab 명령어를 통해 대체할 수 있을 것이다.
redis - node 간의 통신 알고리즘
위 아키텍처에서 K8s간에 redis와 node간 통신하는 구조를 볼 수 있다. 그 구조를 좀 더 자세히 살펴보자
위에서 참조 링크를 올려둔 AWS에서의 Serverless web scraper를 구현하는 사이트에서 예시를 든, redis-node의 기본적인 state machine이다. 람다와 마찬가지인 크론잡은 컨테이너가 실행되면 어디까지 크롤링했냐에 대한 데이터가 없는 stateless(무상태)이기 때문에 queue를 써서 상태 관리를 한다. 그 과정을 간단하게 위와 같이 나타낸다.
이는 좀 더 복잡하고 견고하게 구성한 상태 머신인데, redis와 node를 통해서 위와 같은 상태 머신을 구현했다. redis는 자료를 참고하면 큐로도 사용할 수 있었다. 따라서 레디스를 큐로 사용하도록 설정하여 위와 같은 State Machine을 그대로 구현해 보았다.
마치며
몰랐던 점은 위와 같은 Serverless Web Scraper 아키텍처를 검색 엔진(Search Engine)에도 쓴다는 것이었다. 자세히 조사해 봤는데, 웹 페이지의 HTML을 AWS의 S3에 담아두었다가, AWS Kendra를 통해서 웹 HTML을 서빙함으로써 검색 엔진을 구축하는 것이었다.
AWS Kendra와 비슷한 역할을 하는 것은 Elastic Search였는데, 자주 들어봤지만 어디서 쓰는지 자세히 몰랐던 엘라스틱서치의 활용 방법을 알아볼 수 있었던 기회가 되었다. 웹 사이트를 크롤링하는 것은 생각보다 많은 중요한 역할을 하는 것이었다.
참고
- [AWS] Serverless Architecture for a Web Scraping Solution : https://aws.amazon.com/ko/blogs/architecture/serverless-architecture-for-a-web-scraping-solution/
- [AWS] Scalin up a Serverless Web Crawler and Search Engine : https://aws.amazon.com/ko/blogs/architecture/scaling-up-a-serverless-web-crawler-and-search-engine/
감사합니다.
'project' 카테고리의 다른 글
HPC Cluster에서 Grafana dashboard 만들기 (2) | 2024.09.24 |
---|---|
채팅 서버 만들기 feat. AWS, Serverless (2) | 2024.09.20 |
github.io를 이용한 포트폴리오 사이트 만들기 (2) | 2024.03.06 |