즐겨보던 브런치 사이트에서 RSS Feed를 제공해주지 않는다는 것을 알게된 후, Feed를 생성해주는 웹사이트를 만들어보기로 정했습니다. 그동안 많이 해본 페이지 뷰를 구성하는 작업보다는, 크롤링과 Feed를 생성하는 기능적인 면에 집중했습니다.

Feed는 어떻게 만들지?

가장 먼저 생각해봐야할 것은 Feed를 어떻게 만들어야하는지에 관해서였습니다. html이 아닌 xml로 이루어져야하고, 다양한 Feed 프로그램과 연동이 되어야했습니다. 여러 방법을 찾다가 Django에서 Feed Library를 제공해준다는 것을 알게되습니다. Django Feed의 경우, URI에 전달된 get parameter를 사용합니다. ORM을 사용해 get parameter에 해당되는 QuerySet을 불러오고, QuerySet의 Object들이 Feed의 본문을 구성하게 됩니다. Feed를 따로 DB에 저장시킬 필요가 없으며, 데이터베이스에 저장된 글을 사용해 xml 방식으로 변환시켜 Feed 페이지를 구성하게 되는 방식입니다.

가장 중요한 것은 ORM을 사용해 해당되는 Object를 불러오는 일이고, 그 다음으로는 title, description, pubdate 등과 같은 Django Feed의 기본 속성들에 적절히 크롤링한 글의 내용을 넣어주는 것입니다. ORM을 사용하는 데에는 큰 문제가 없었으나, title, description, link, pubdate 등에 해당되는 내용을 원하는 방식으로 커스터마이징 하는 과정에서 꽤 시간을 보냈습니다. Feed 자체에 대한 title과 Feed를 구성하는 글들에 대한 title 속성이 비슷한 이름의 메서드로 제공되어졌기에, Django Feed의 소스코드 흐름을 뜯어보는 것이 힘들었습니다. 공식문서나 예제코드와 같이, 설명이 함께 제공되는 코드를 주로 봐왔던 저에게 오직 코드와 디버깅만으로 소스코드의 흐름을 파악하고 커스터마이징해보았던 것은 정말 큰 경험이었습니다. Feed Library를 뜯어본 후, 생소한 모듈이나 라이브러리를 사용하는 것에 대한 진입장벽이 낮아졌다는 것이 꽤 체감되기때문입니다.

중복된 데이터 제거하기

Feed 생성과 커스터마이징 구현이 가능해졌기에, 본격적으로 프로젝트를 수행하기위해 브런치 사이트의 글들을 크롤링하는 코드를 짰습니다. 사용자가 입력한 키워드에 매칭되는 브런치 글을 크롤링하는 방식입니다. requests와 selenium을 사용해 검색된 글들을 크롤링하는 것은 어렵지 않았지만, 글을 저장하는 과정에서 many to many 관계에 있는 Keyword 모델과 Article 모델을 고려해 같은 글이 중복되어 저장되지 않도록 하는 작업이 요구되었습니다.

Keyword : 검색된 단어
Writer : 검색된 작가
Article : Keyword와 Writer에 해당되는 글

예를 들어 '파이썬'이라는 키워드로 이미 크롤링 된 '글 1'이 DB에 저장되었다고 가정합니다. 이 상태에서 '장고'라는 검색어로 크롤링을 진행하게 되었는데 이미 '파이썬'이라는 키워드로 저장 된 '글 1'이 '장고'라는 키워드에도 매칭이 되어 크롤링이 진행되었습니다. 이 때 글 1(keyword='파이썬'), 글 1(keyword='장고') 와 같이 저장되어, 동일한 record가 2개로 존재할 뿐만 아니라, 같은 글을 두 번 크롤링하는 낭비되는 시간이 발생합니다.(참고 : Feed 생성 시에는 '파이썬', '장고'와 같은 Keyword로 필터링을 하므로 생성된 Feed에는 글의 중복이 없습니다.)

따라서 '글 1'(Article)이 '파이썬', '장고'(Keyword)를 갖도록 처리하는 과정을 구현하기위해, Article의 고유한 txid(작가아이디+글 번호)를 부여하여 중복을 검사하도록 하였습니다. 그리고 '글 1(keyword='파이썬')'과 같이 이미 DB에 존재하는 Article에 대해서, 검색된 Keyword('장고')를 추가시켜주어, '글 1'(keyword=['파이썬', '장고'])와 같이 구성되도록 크롤링 처리를 해주었습니다. 복잡한 코드는 아니었지만 ForeignKey, ManyToMany와 같은 모델 설계와 그에 따른 DB 구성 방식을 직접 구현해보니, 기타 Library나 다른 사람의 코드에서 구성된 모델/DB 구조를 파악하는 것이 전보다 나아졌음을 느꼈습니다.

트러블 슈팅 : 크롤링이 너무 느리다..

Asyncio를 활용한 비동기 http
크롤링 자체를 구현하는 것은 어렵지 않았지만 검색어를 입력한 후 약 7-8초를 로딩한 후 Feed URL을 반환하므로 속도 개선이 절실했습니다. 가장 시간을 많이 소요하는 작업은 브런치 사이트에서 검색어를 search한 후 글 목록이 로드되는 작업이었습니다. 당연히 Brunch가 제공해주는 API가 없으니, Javascript로 글 목록이 로드되는 시간은 어쩔 수 없었고, 차선책으로 택한 것이 asyncio입니다. 로드된 글 목록에서 글 본문 페이지로 보내는 http request를 비동기 방식으로 구현 가능한 aiohttp 라이브러리를 사용했습니다. 로드된 글 갯수가 5개라면, Django의 동기적 처리방식은 request를 보낸 후 response를 올 때까지 기다리는 방식을 5번 반복하게됩니다. http 비동기 처리 라이브러리인 aiohttp를 사용할 경우, request를 보낸 후, response를 기다리지 않고 다음 URL에 대한 request를 실행하게 됩니다. 5개의 request를 보낼 동안, 네트워크 IO를 통해 response를 얻은 후, request 작업이 끝나면 응답 순서대로 나머지 작업을 처리하게 됩니다. 보내야할 request가 많을수록 낭비되는 시간이 줄어들게 되었고, 10개의 URL을 기준으로 기존의 2-3초 가량 걸리던 시간을 약 1-1.5초 가량 단축할 수 있었습니다.

Celery를 활용한 백그라운드 작업
하지만 http 비동기 처리를 통해 시간을 단축했음에도, 여전히 5-6초나 걸리는 크롤링 작업의 해결책이 될 수 없었습니다. 근본적인 원인은 Javascript로 로드되는 시간과 글 정렬을 최신순으로 click()하는 selenium 작업입니다. 그래서 사용한 것이 Celery입니다. 크롤링 작업의 순서를 먼저 살펴보겠습니다.

Celery 적용 전 : 사용자 검색어 입력 --> 크롤링 수행(긴 수행시간 발생) --> Feed URL 출력

사용자는 입력한 검색어에 대한 Feed URL이 필요할 뿐입니다. 그렇다면 크롤링 작업을 제거하고 Feed URL을 바로 출력시켜줄까? 라는 생각이 들었고, 크롤링 작업을 Celery를 통해 백그라운드로 수행하게 했습니다.

Celery 적용 후 : 사용자 검색어 입력 -- (백그라운드에서 크롤링 수행) --> Feed URL 출력

바뀐 크롤링 로직입니다. 사용자가 검색어 입력 후, 곧바로 Feed URL을 얻게됩니다. 그리고 백그라운드 작업으로 크롤링을 수행합니다. 크롤링이 수행되는 동안 출력된 Feed URL 접속 시, Feed 생성중이라는 title을 가진 Feed를 리턴하도록 하였습니다. 크롤링이 완료되면 정상적으로 Article object들에 대한 Feed를 리턴하게 됩니다. 이로인해 사용자가 Feed URL을 얻는 5-6초에 걸리는 시간을 2초 안쪽으로 단축시킬 수 있었습니다.(검색어에 대한 글이 존재하는지 검사하는 작업으로 2초 가량 소요됩니다.)

두 방식의 차이
이렇게 두가지 방법을 사용해 크롤링 시간을 단축시켜보았습니다. 단, 두 방식에는 다음과 같은 결정적인 차이가 있습니다.
Asyncio 비동기 http 처리의 경우, 크롤링 작업 시간 자체를 단축 시킨 방식입니다.
Celery 백그라운드 처리의 경우, 크롤링 수행 방식 변경함으로써 결과물을 리턴하는 시간을 단축 시킨 방식입니다.

한 마디로, '문제를 해결하기 위해 다양한 방식을 고안해볼 수 있겠구나!' 라는 생각을 갖게해준 트러블 슈팅이었습니다.

이것도 해볼까?

그 후 Crawling 기능을 EC2의 Django 서버에서, Lambda로 동작하도록 분리해보았습니다. 크롤링만을 위한 기능에 Django를 통째로 올려놓기가 부담스러웠기에 Lambda에서 구현된 크롤링은 Django ORM이 아닌 SQL로 DB처리를 구현했습니다. 하지만 EC2에 비해 http request와 selenium이 동작하는 부분에서 속도 저하 현상이 크게 나타났기에, 아쉽게도 Lambda를 활용한 크롤링 서버 구현은 작동하도록 만든 수준에 만족하고 접게됬습니다. 그럼에도 불구하고 Serverless Architecture에 대한 경험을 가졌다는 점, 특정 기능만을 위한 서버를 분리했다는 점은 새로운 시도였기에 꽤나 재미있는 경험이었습니다.

마치며

그 외에 Sentry, Celery의 일부 기능을 활용했고, 특히 크롤링에 사용한 파이썬 비동기 처리 모듈인 asyncio는 활용가능성이 무궁무진해보이고 공부하기에도 재미있는 모듈이라는 생각이 듭니다. 처음으로 개인 프로젝트를 진행해보니, 처음 세운 계획보다 훨씬 더 고려해야할 것이 많아 당황스러웠습니다. 하지만 그 과정에서 전에 사용하지 않았던 것들을 사용해보고 얻게되는 지식들이 참으로 많았습니다. 이래서 사람들이 프로젝트~ 프로젝트~ 하는구나, 느꼈습니다.

+ Recent posts