크롤러에 잠들지 않는 심장을 달다: 24/7 무인 운영 자동화 및 성능 최적화 분투기

이동휘·2025년 6월 29일
0

매일매일 블로그

목록 보기
36/49

안녕하세요! 웹 크롤링 프로젝트를 진행하다 보면, 개발자는 종종 이런 고민에 빠지게 됩니다.

"내 크롤러는 왜 자꾸 한밤중에 죽어있을까?"
"수집한 데이터에 왜 이렇게 잘못된 정보가 많지?"
"이걸 언제까지 매일 손으로 돌려야 하지?"

단순히 데이터를 수집하는 크롤러는 웹사이트 구조 변경, IP 차단, 데이터 오염 등 작은 변화에도 쉽게 멈춰버리는 연약한 존재입니다. 개발자의 지속적인 개입 없이는 무용지물이 되기 십상이죠.

그래서 저희 프로젝트에서는 이 문제를 근본적으로 해결하기 위해, 한 단계 더 나아가 스스로를 진단하고, 문제를 해결하며, 24시간 365일 쉬지 않고 동작하는 '무인 운영 자동화 시스템'을 구축했습니다. 더 나아가, "7일 걸리던 작업을 6시간 만에 끝내는" 극적인 성능 최적화 과정과, JavaScript로 렌더링되는 까다로운 웹사이트에서 데이터를 캐내는 비법까지, 저희 크롤러에 '잠들지 않는 심장'을 달아준 그 모든 여정을 이 글에 상세히 공유하고자 합니다.


Part 1: 크롤러의 두뇌 - 통합 운영 자동화 시스템 구축하기

모든 자동화의 시작은 '스케줄링'과 '지능적인 자기 관리'입니다. 저희는 파이썬의 schedule 라이브러리를 활용해 크롤러의 모든 작업을 지휘하는 중앙 통제 시스템을 만들었습니다. 이 시스템은 단순히 정해진 시간에 작업을 실행하는 것을 넘어, 각 시스템의 상태를 종합적으로 관리하는 두뇌 역할을 합니다.

주요 자동화 스케줄:

  • 매일 02:00: 서울 전 지역의 가게 정보 업데이트 (일일 크롤링)
  • 매일 03:00: 수집된 데이터의 품질 자동 검증
  • 매일 04:00: 가게들의 휴업/폐업 상태 자동 확인
  • 매주 월요일 09:00: 주간 데이터 동향 분석 및 시각화 보고서 이메일 발송
  • 매 30분: 시스템 헬스 체크 및 상태 모니터링

이 모든 작업은 하나의 클래스(AutomatedOperations)에 의해 유기적으로 연결되어, 마치 하나의 생명체처럼 스스로를 돌보며 동작합니다.

크롤러의 면역 시스템: 자동 데이터 품질 관리

잘못된 데이터는 서비스의 신뢰도를 떨어뜨리는 가장 큰 적입니다. 우리는 데이터가 DB에 저장되는 과정에서 스스로를 정제하고 치유하는 강력한 면역 시스템을 구축했습니다.

1. ML 기반 중복 데이터 자동 탐지:

"강남 돼지상회"와 "돼지상회 강남점"은 같은 가게일까요? 사람은 쉽게 판단하지만 기계는 어렵습니다. 우리는 이 문제를 머신러닝으로 해결했습니다.

  • 텍스트 유사도 분석: 가게 이름, 주소, 설명 등의 텍스트 정보를 TF-IDF로 벡터화하고, 코사인 유사도(Cosine Similarity)를 계산하여 텍스트적 유사성을 측정합니다.
  • 위치 기반 클러스터링: DBSCAN 알고리즘을 사용해 지리적으로 가까운 거리에 있는 가게들을 하나의 군집(Cluster)으로 묶습니다.
  • 종합 판단: "텍스트 유사도가 85% 이상이고, 같은 위치 군집에 속해 있다면?" 시스템은 이들을 '중복 의심 가게'로 분류하고 개발자에게 보고하여 최종 판단을 돕습니다.

2. 데이터 유효성 검증 및 자동 수정:

수집된 데이터에 포함될 수 있는 논리적 오류를 자동으로 찾아내고 수정합니다.

  • 좌표 검증: "가게 위치가 바다 한가운데에 찍혔네?" 수집된 좌표가 한국 영역을 벗어나거나, 주소와 실제 위치가 1km 이상 차이 나면 자동으로 플래그를 지정하고, 주소를 기반으로 지오코딩을 재시도하여 좌표를 수정합니다.
  • 영업시간 논리 검증: "오픈 시간이 마감 시간보다 늦다고?" 와 같은 논리적 오류를 감지하고, "오전 10시", "밤 10:00" 등 제각각인 표기를 "10:00", "22:00"과 같은 표준 형식으로 자동 변환합니다.

크롤러의 반사 신경: 지능형 예외 처리 및 자동 복구

크롤러 운영의 가장 큰 스트레스는 예기치 않은 '돌발 상황'입니다. 우리는 크롤러가 스스로 위험을 감지하고 회피하는 반사 신경을 심어주었습니다.

  • 사이트 구조 변경 자동 감지: 평소보다 크롤링 실패율(Failure Rate)이 급격히 증가하면(예: 15% 이상), 이를 '웹사이트 구조 변경'으로 간주합니다. 시스템은 어떤 부분(CSS Selector)에서 문제가 발생했는지 분석하고 즉시 개발자에게 알림을 보냅니다.
  • IP 차단 자동 대응: 특정 IP에서의 접속이 계속 실패하면 'IP 차단'으로 판단합니다. 이때 시스템은 사전에 준비된 수십 개의 프록시(Proxy) IP 리스트에서 사용 가능한 다음 IP로 자동으로 교체하고, User-Agent까지 변경하여 마치 다른 사용자인 것처럼 크롤링을 재개합니다.
  • 자동 복구 메커니즘: 3회 이상 연속으로 특정 작업이 실패하면, 시스템은 스스로 '자동 복구 모드'에 진입하여 관련 서비스를 재시작하고, 10분 후 다시 작업을 시도합니다.

크롤러의 건강 관리: 가게 상태 자동 업데이트

열심히 수집한 가게가 다음 날 폐업한다면, 이 데이터는 더 이상 유용하지 않습니다. 우리는 가게의 '생사'를 주기적으로 확인하는 건강 관리 시스템을 도입했습니다.

  • 주기적 상태 확인: 매일 스케줄에 따라 등록된 가게들의 전화번호 유효성, 웹사이트 접속 가능 여부, 최신 블로그 리뷰 유무 등을 종합적으로 체크합니다.
  • 자동 상태 변경: 90일 이상 최신 리뷰 활동이 없고, 전화번호가 결번이며, 웹사이트에 '폐업' 키워드가 감지되면, 시스템은 해당 가게의 상태를 '운영중'에서 '폐업 의심' 또는 '폐업'으로 자동 업데이트하여 서비스에서 노출되지 않도록 합니다.

크롤러의 목소리: 스마트 알림 및 리포팅

아무리 시스템이 똑똑해도, 무슨 일이 벌어지는지 알 수 없다면 소용없겠죠. 우리는 크롤러가 자신의 상태를 개발자에게 꾸준히 '보고'하도록 만들었습니다.

  • 일일 요약 보고 (Slack/Discord): 매일 아침, "어제 총 1,250개 가게 정보를 업데이트했고, 5개 가게에서 품질 이슈를 발견해 3개를 자동 수정했습니다. 현재 데이터 품질 점수는 98.5점입니다." 와 같은 간결한 요약 리포트를 받아볼 수 있습니다.
  • 주간 상세 보고서 (Email): 매주 월요일, 지난 한 주간의 데이터 성장 추이, 지역별 신규 가게 분석 등이 담긴 시각화 차트(Matplotlib 활용)를 포함한 HTML 보고서를 이메일로 자동 발송합니다.
  • 실시간 에러 알림: IP 차단, 시스템 다운 등 심각한 문제가 발생하면, 즉시 Slack으로 긴급 알림을 보내 개발자가 빠르게 대응할 수 있도록 합니다.

Part 2: 7일 걸릴 크롤링, 6시간 만에 끝내기 - 성능 최적화 분투기

"서울 시내 무한리필 가게 정보 전체 수집, 예상 소요 시간: 7일"

프로젝트가 특정 단계에 도달했을 때 마주한 암담한 현실이었습니다. 서울 25개 구, 수많은 키워드를 하나의 프로세스로 순차 처리하니 속도가 너무 느렸고, 이대로는 데이터를 최신 상태로 유지하는 것이 불가능했습니다. 그래서 우리는 '압도적인 성능 최적화'를 다음 목표로 잡았습니다.

어떻게 7일짜리 작업을 단 6시간으로 단축시켰을까요? 그 비결은 세 가지 핵심 기술에 있었습니다.

1. 첫 번째 무기: 병렬 처리로 작업 시간 쪼개기 (multiprocessing)

가장 먼저 해결해야 할 문제는 '느린 속도'였습니다. 하나의 일꾼(프로세스)이 서울 전역을 돌아다니는 대신, 여러 명의 일꾼이 각자 구역을 맡아 동시에 일하게 만들면 어떨까요? 바로 병렬 처리(Parallel Processing)의 시작이었습니다.

  • 구현 방식: 파이썬의 내장 라이브러리인 multiprocessingProcessPoolExecutor를 활용했습니다.
    1. 작업 분할: 서울 25개 구를 각각의 독립된 '크롤링 작업(Task)' 단위로 나눕니다.
    2. 프로세스 풀(Pool) 생성: 시스템의 CPU 코어 수에 맞춰 최적의 프로세스(일꾼) 수를 결정합니다. (예: 8코어 CPU → 7개 프로세스)
    3. 작업 할당 및 실행: 생성된 프로세스 풀에 25개의 '구 크롤링' 작업을 제출하면, 각 프로세스가 알아서 작업을 가져가 동시에 수행합니다.
    4. 결과 취합: 모든 프로세스의 작업이 끝나면, 메인 프로세스에서 그 결과들을 안전하게 취합합니다.

이 방식 하나만으로도 이론적으로 CPU 코어 수만큼의 성능 향상을 기대할 수 있었습니다.

2. 두 번째 무기: Redis 캐싱으로 불필요한 요청 원천 차단

크롤링을 하다 보면 어제 검색했던 키워드를 오늘도 검색하고, 이미 방문했던 가게 상세 페이지를 또 방문하는 일이 비일비재합니다. 이는 엄청난 네트워크 자원 낭비이자, IP 차단 위험을 높이는 주범입니다.

  • 해결책: 인메모리(In-memory) 데이터 저장소인 Redis캐싱(Caching) 시스템으로 도입했습니다.
  • 다층 캐시 전략:
    • Level 1 (가게 목록 캐시): '강남역 무한리필'과 같은 검색 결과는 6시간 동안 캐시에 저장합니다. 6시간 내에 같은 요청이 오면 실제 크롤링 없이 Redis에서 즉시 결과를 반환합니다.
    • Level 2 (가게 상세 정보 캐시): 한 번 방문한 가게의 상세 정보는 7일간 캐시에 저장합니다.
    • Level 3 (실패 URL 캐시): 접속에 실패했거나 정보가 없는 URL은 24시간 동안 '실패'로 캐싱하여, 불필요한 재시도를 막습니다.
  • 효과: 이 캐싱 시스템 덕분에 전체 네트워크 요청의 50~60%를 줄일 수 있었고, 이는 곧 크롤링 속도 향상과 안정성 증대로 이어졌습니다.

3. 세 번째 무기: DB 삽입, 20배 빠르게 (COPY 명령어)

수만 개의 데이터를 데이터베이스에 저장할 때, INSERT 구문을 하나씩 반복 실행하는 것은 대표적인 성능 병목 구간입니다.

  • 해결책: PostgreSQL이 제공하는 COPY 명령어를 활용하여 데이터를 대량으로, 그리고 매우 빠르게 적재하는 방식을 채택했습니다.
    1. 데이터를 파일처럼 변환: 파이썬의 io.StringIO를 사용해 수집된 데이터 목록을 메모리 상에서 마치 하나의 거대한 CSV 파일처럼 만듭니다.
    2. 한 번에 COPY: cursor.copy_from() 메서드를 호출하여 메모리에 있던 모든 데이터를 단 한 번의 명령으로 DB 테이블에 쏟아붓습니다.
    3. (필요시) 중복 처리: ON CONFLICT 구문을 사용하여 중복 데이터는 업데이트하거나 무시하도록 처리합니다.

이 방법을 통해 개별 INSERT 대비 20배 이상의 놀라운 데이터베이스 쓰기 성능을 확보할 수 있었습니다.

결과: 30배 빨라진 크롤러의 탄생!

병렬 처리로 시간을 나누고, 캐싱으로 불필요한 과정을 생략하며, 고성능 DB 처리로 병목을 제거한 결과, 7일 걸리던 작업은 단 6~12시간 만에 끝낼 수 있게 되었습니다.


Part 3: JavaScript 렌더링의 숲에서 데이터 캐내기 (with Selenium)

"분명히 눈에는 보이는데, 왜 크롤러는 데이터를 못 가져올까요?"

최신 웹사이트들은 React, Vue.js 같은 JavaScript 프레임워크를 사용해 동적으로 데이터를 화면에 그려줍니다. 이 때문에 초기 HTML 소스는 텅 비어있고, 우리가 원하는 정보는 브라우저가 JavaScript를 모두 실행한 뒤에야 나타나죠.

저희 프로젝트 역시 특정 동적 사이트에서 GPS 좌표와 같은 핵심 정보를 수집해야 하는 큰 숙제를 안고 있었습니다. HTML 어디에서도 찾을 수 없는 이 데이터를 어떻게 추출했을까요?

1. 수사 착수: 데이터는 어디에 숨어있나?

개발자 도구(F12)는 우리의 가장 친한 친구입니다. Network 탭, HTML 구조, JavaScript 소스 코드를 샅샅이 뒤진 결과, window 객체의 특정 전역 변수(예: window.PLACE_INFO, window.poi) 안에 우리가 찾던 위도, 경도 값이 고스란히 담겨 있는 결정적 단서를 발견했습니다!

2. 핵심 전략: Selenium으로 JavaScript 변수 직접 추출하기

BeautifulSoup은 렌더링된 HTML을 파싱할 뿐, JavaScript를 실행할 수는 없습니다. 바로 이 지점에서 Selenium이 진가를 발휘합니다.

Selenium의 driver.execute_script() 메서드는 브라우저의 콘솔에서 JavaScript 명령어를 직접 실행하고 그 결과를 파이썬으로 반환받을 수 있게 해줍니다. 우리는 이 기능을 활용해 HTML을 파싱하는 대신, JavaScript 변수 값을 직접 빼내는 전략을 선택했습니다.

# src/core/crawler.py 의 핵심 로직 예시
def _extract_coordinate_info(self) -> Dict:
    try:
        # JavaScript 실행으로 좌표 정보가 담긴 전역 변수 값을 직접 가져옴
        lat_script = "return window.latitude || (window.PLACE_INFO && window.PLACE_INFO.lat);"
        lng_script = "return window.longitude || (window.PLACE_INFO && window.PLACE_INFO.lng);"

        lat = self.driver.execute_script(lat_script)
        lng = self.driver.execute_script(lng_script)

        if lat and lng:
            return {'lat': float(lat), 'lng': float(lng)}
            
    except Exception as e:
        logger.error(f"JavaScript 좌표 추출 오류: {e}")
    
    return {}

이 방법은 웹사이트의 디자인(CSS 클래스명 등)이 바뀌어도 JavaScript 변수명은 상대적으로 잘 바뀌지 않기 때문에, HTML 파싱보다 훨씬 안정적이고 정확합니다.

3. 심화 학습: 동적 상호작용 후 데이터 추출하기

'영업시간' 정보처럼, '더보기' 버튼을 클릭해야만 나타나는 데이터도 있었습니다. 이 문제는 다음과 같이 사용자 행동을 시뮬레이션하여 해결했습니다.

  1. 사용자 행동 시뮬레이션: Selenium으로 '영업시간 더보기' 토글 버튼을 찾아 클릭(button.click())합니다.
  2. 콘텐츠 로딩 대기: time.sleep()이나 WebDriverWait를 사용하여 새로운 정보가 렌더링될 시간을 확보합니다.
  3. 업데이트된 소스 파싱: 다시 페이지 소스를 가져와 BeautifulSoup으로 파싱하고, 정규식 등을 활용하여 원하는 정보를 추출합니다.

4. 다층 방어 전략: 실패를 대비하는 견고한 크롤러

하나의 방법만 고집하는 것은 위험합니다. 우리는 어떤 상황에서도 데이터를 놓치지 않기 위해 다음과 같은 다층적 추출 전략을 설계했습니다.

  1. 1차 방어선: JavaScript 변수 직접 추출 (가장 빠르고 정확)
  2. 2차 방어선: <script> 태그 내에 숨겨진 JSON 데이터 파싱 (차선책)
  3. 3차 방어선: 최종 렌더링된 HTML 요소 파싱 (전통적이지만 가장 불안정)
  4. 최후의 보루: 그래도 좌표를 못 찾았다면? 추출한 주소 정보를 지오코딩 API에 넘겨 좌표를 얻어냅니다.

결론: 단순한 크롤러를 넘어, 살아있는 데이터 파이프라인으로

이 프로젝트는 웹에서 데이터를 긁어오는 단순한 작업을 넘어, 데이터의 품질과 최신성을 스스로 유지하고, 자신의 문제를 해결하며, 운영 비용을 최소화하는 자동화된 데이터 파이프라인을 구축하는 여정이었습니다.

이러한 자동화 시스템 덕분에 개발자는 더 이상 매일 크롤러 로그를 들여다보거나, 오류 때문에 새벽에 깨는 일 없이, 서비스의 핵심 가치를 개발하는 데 더 집중할 수 있게 되었습니다.

여러분도 혹시 밤마다 크롤러의 안녕을 걱정하고 계신가요? 그렇다면, 오늘 소개해 드린 자동화 시스템 구축과 성능 최적화, 그리고 지능형 데이터 추출 경험이 여러분의 크롤러에게도 '잠들지 않는 심장'을 달아주는 데 작은 영감이 되기를 바랍니다.

0개의 댓글