자동화한 부분
프로젝트를 하면서 공연 정보를 다음과 같이 보여주어야 했습니다. 그런데 공연 정보들을 모아주는 기능이 없다 보니, 팀원들이 인터넷에서 공연 정보를 찾고 등록하는 일을 모두 수동으로 진행해야 했습니다. 때문에 공연 정보를 업데이트하기 위해선 다음과 같은 절차를 따라야 했습니다.
- 자신이 맡은 공연 카테고리(연극, 전시 등)와 공연이 열리는 지역을 알고 있어야 한다.
- 공연을 예매할 수 있는 yes24, 인터파크 등의 사이트에 접속해 공연을 찾는다.
- 자신이 찾은 공연이 기존에 찾은 공연과 다른지 확인한다.
- 찾은 공연이 업로드하기에 적합하다면 어드민 페이지를 통해 업로드한다.
위 과정 중 1 ~ 3번 부분이 가장 번거로웠습니다. 자신이 맡은 공연 카테고리와 지역을 기억하고 있어야 하고, 중복 검사를 사람이 하기 때문에 공연 정보가 늘어나면 실수할 가능성이 높아지기 때문입니다. 그래서 1~3번 과정을 자동화하기로 결정했습니다. 그 결과 위의 절차를 다음과 같이 단순화했습니다.
- 어드민 페이지에서 수집된 공연 정보를 조회한다.
- 공연 정보는 하루에 한 번 배치 프로세스가 크롤러를 호출해 정보를 가져오고, 알맞게 파싱 한다.
- 원하는 공연 정보를 선택해 업로드한다.
사용한 기술 스택과 아키텍처
자동화 구현을 위해 다음과 같은 기술 스택을 사용했습니다.
- 크롤러
- selenium web driver
- 스케줄링: 젠킨스
- 배치: spring batch
크롤러 만들기
공연 정보를 자동으로 수집하는 크롤러를 만들기 위해 공연 사이트(yes24, interpark 등)들을 조사해 보니, 템플릿 엔진과 같은 기술을 사용해 사용해 Server-Side Rendering을 해주는 사이트가 많지 않다는 것을 알게 되었습니다. 결국 단순히 HTML 파일만 요청해서는 원하는 데이터를 모두 수집할 수 없었습니다. 결국 동적으로 생성되는 데이터까지 크롤링 하기 위해(실제 사용자가 브라우저에서 웹 페이지를 열고 JS가 동적으 데이터를 생성하게 하는 것을 코드로 구현하기 위해) selenium을 사용했습니다. Selenium은 웹 애플리케이션 테스트를 자동화해주는 툴입니다. 이를 위해서 Selenium은 크롬, 파이어폭스 등 여러 브라우저에 해당하는 드라이버를 제공합니다. 드라이버는 실제 브라우저와 통신합니다. 결국, Selenium web driver를 사용하면 데이터를 동적으로 생성해 주는 페이지를 크롤링 할 수 있습니다. 여기서 selenium web driver를 이용해 실제 브라우저와 커뮤니케이션하는 작업은 무겁기 때문에 web driver를 띄우는 작업은 별도의 서버를 만들어 진행했습니다. 공연 정보를 자동 수집하기 위해 크롤러는 별도의 서버를 띄우는 대신 스프링 배치를 돌리는 서버에 구현하는 것을 선택했습니다. 이 크롤러는 검색엔진을 위한 크롤러처럼 수십억 건의 데이터를 저장할 필요도 없고, 불특정 다수의 페이지를 크롤링 할 필요도 없습니다. 오직 주어진 링크 (공연 카테고리 하나에 대한 공연들이 모여있는 페이지 링크) 들에 존재하는 링크(공연 하나에 대한 세부 정보를 보여주는 링크)를 수집하고, 수집한 링크들을 방문해 내부에 있는 정보들을 수집하면 됩니다. 때문에 한번 동작 시 동작 기간은 수 분이며 하루에 한 번 동작하는 것으로 충분합니다. 결국 크롤러를 위한 별도의 서버를 띄우는 것은 자원 낭비(하루에 수 분만 동작하기 때문)이며 크롤러가 순환에 빠지는 등의 문제 역시 고려하지 않아도 된다고 판단했습니다. selenium chrom driver와 구현한 크롤러만을 생각했을 때 최종 구조는 다음과 같습니다.
스프링 배치 사용
크롤러를 구현했지만 공연 정보를 원하는 형식으로 파싱해 보여주기까지 다음과 같은 절차를 외부 간섭 없이 순차적으로 주기적으로 수행되어야 합니다.
- 크롤러를 동작시킨다.
- 서로 다른 웹 사이트에서 크롤링 한 데이터들을 합친다.
- 기존에 수집한 데이터들과 중복된 데이터들이 존재하는지 판단한다
- 존재한다면 해당 데이터는 버린다
- 필터링 과정을 마친 데이터들을 저장한다
- 공연 정보 수집 과정 완료를 알 수 있는 메시지를 전송한다.
또 한, 위 과정은 selenium web driver, 외부 페이지 구성 요소, DB, 완료 여부 알람을 위한 slack web hook과 같이 외부 의존성을 많이 필요로 하는 작업이기에 작업 도중 문제가 발생한다면 어느 부분에서 문제가 발생했는지 빠르게 파악할 수 있어야 합니다. 외부 간섭 없이 순차적으로 실행한다는 것은 결국 배치 작업이 필요하다는 의미입니다. 스프링 배치는 각 job과 job 내부의 step의 상태를 JobRepository에 저장하고 관리하기 때문에 위 절차를 여러 step으로 나눈다면 어느 부분에서 문제가 발생했는지 빠르게 파악할 수 있습니다. 따라서 스프링 배치를 사용하되 다음과 같이 위 과정을 3개의 step으로 나누어 구현했습니다.
- step1: 크롤링을 진행하고 데이터를 합치는 과정
- 크롤러를 동작시킨다.
- 서로 다른 웹 사이트에서 크롤링한 데이터들을 합친다.
- step2: 데이터를 저장하는 과정
- 기존에 수집한 데이터들과 중복된 데이터들이 존재하는지 판단한다
- 존재한다면 해당 데이터는 버린다
- 필터링 과정을 마친 데이터들을 저장한다
- 기존에 수집한 데이터들과 중복된 데이터들이 존재하는지 판단한다
- step3: 완료 알람을 보내는 과정
- 공연 정보 수집 과정 완료를 알 수 있는 메시지를 전송한다.
1 2 3 4 5 6 7 8
@Bean public Job crawlPerformances() { return jobBuilderFactory.get("generatePerformance") .start(crawPerformancePages()) // 크롤링을 진행하고 데이터를 합치는 과정 .next(outputPerformances()) // 데이터를 저장하는 과정 .next(sendCompleteMessage()) // 완료 알람을 보내는 과정 .build(); }
이렇면 다음과 같이 각 step execution에 대한 결과가 BATCH_STEP_EXECUTION 테이블에 저장돼서 장애 발생 시 어느 부분에서 발생한 건지 파악할 수 있습니다.
- 공연 정보 수집 과정 완료를 알 수 있는 메시지를 전송한다.
스프링 배치를 스케줄링하기 위해선 기존에 CI/CD를 위해 사용 중이던 젠킨스를 사용했습니다. 배치를 주기적으로 실행하기 위해 별도의 아이템을 만들어 매일 새벽 2시마다 배치를 구동시키게 했습니다.
배치 결과는 다음과 같이 어드민 페이지에서 확인할 수 있습니다.
최종 아키텍처는 다음과 같습니다.