
크롤러를 운영하다 보면 “에러 로그가 없으니 정상 작동했다”고 믿는 경우가 많습니다. 하지만 크롤러가 조용히 0건을 내놓는 상황은 에러 한 줄 남기지 않고도 자주 일어납니다. 0이라는 숫자 자체만 보고 성공과 실패를 가르면 안 되고, 상태 코드·응답 크기·과거 수집량 같은 주변 신호를 같이 봐야 실패인지 정상인지 구분할 수 있습니다.
0건이 나오는 세 가지 서로 다른 상황
같은 “0건”이라도 원인은 전혀 다릅니다. 첫째는 해당 시점에 실제로 수집 대상이 없었던 경우입니다. 공고 게시판이 주말에는 글이 안 올라오는 식이면 0건이 맞는 결과입니다.
둘째는 페이지 구조가 바뀌어서 셀렉터가 더 이상 요소를 찾지 못하는 경우입니다. 이때는 HTTP 요청은 200으로 성공하지만 파싱 로직만 빈손으로 돌아옵니다.
셋째는 차단이나 레이트리밋으로 서버가 실제 콘텐츠 대신 안내 페이지나 캡차 화면을 돌려주는 경우입니다. 이 세 경우를 구분하지 않고 “0건=실패” 또는 “0건=괜찮음”으로 단순화하면 둘 중 하나는 반드시 틀리게 됩니다.
상태 코드는 200인데 본문이 비어 있는 이유가 뭘까요?
가장 많이 놓치는 지점이 바로 여기입니다. 요청이 과도하면 서버가 429 상태 코드로 응답을 돌려주기도 하지만, 일부 서버는 접속을 막으면서도 200을 그대로 내려줍니다. 이 경우 본문은 “접근이 제한되었습니다” 같은 안내 문구나 빈 템플릿뿐이라서, 상태 코드만 확인하는 모니터링은 아무 이상도 감지하지 못합니다.
자바스크립트 렌더링이 필요한 페이지를 정적 요청으로 긁을 때도 비슷한 증상이 나타납니다. HTML은 받아오지만 실제 목록은 클라이언트 사이드에서 채워지기 때문에, 파서가 보는 문서에는 빈 뼈대만 남습니다. 응답 바이트 수나 특정 DOM 요소 존재 여부를 같이 확인해야 이런 케이스를 걸러낼 수 있습니다.

실패 판정 기준을 코드로 만들어 보기
말로만 기준을 세우면 운영 중에 흐지부지됩니다. 상태 코드, 응답 크기, 최근 수집량 대비 편차를 같이 평가하는 함수를 하나 만들어 두면 알람 여부를 바로 판단할 수 있습니다.
import statistics
def is_crawl_failure(item_count, status_code, html_bytes, recent_counts):
# recent_counts: 최근 7일간 수집 건수 리스트
baseline = statistics.median(recent_counts) if recent_counts else None
if status_code != 200:
return True, f"상태 코드 비정상: {status_code}"
if html_bytes < 2000:
return True, f"응답 본문이 비정상적으로 작음: {html_bytes}바이트"
if baseline and baseline >= 5 and item_count == 0:
return True, f"베이스라인({baseline}건) 대비 0건, 급감으로 판단"
if baseline and item_count < baseline * 0.3:
return True, f"베이스라인({baseline}건) 대비 70% 이상 감소"
return False, "정상 범위"
recent = [12, 15, 9, 14, 11, 13, 10]
result = is_crawl_failure(item_count=0, status_code=200, html_bytes=48213, recent_counts=recent)
print(result)
위 코드를 실행하면 아래와 같이 출력됩니다.
(True, '베이스라인(12건) 대비 0건, 급감으로 판단')
상태 코드와 응답 크기는 정상 범위였지만, 최근 7일 중앙값(12건) 대비 0건이라는 편차 하나만으로 실패로 분류됩니다. 상태 코드 체크만 했다면 이 알람은 끝까지 울리지 않았을 상황입니다.
임계값과 베이스라인은 어떻게 잡아야 할까요?
판정 방식을 세 단계로 나눠서 비교하면 선택 기준이 분명해집니다.
| 판정 방식 | 기준 | 장점 | 한계 |
|---|---|---|---|
| 단순 0건 체크 | 수집 건수가 0이면 실패 처리 | 구현이 매우 단순함 | 실제로 0건이 맞는 날에도 오탐 발생 |
| 베이스라인 대비 편차 | 최근 n일 중앙값 대비 급감 여부 | 요일·주기성을 어느 정도 반영 | 운영 초기 데이터가 적으면 기준이 불안정함 |
| 구조적 신호 결합 | 상태 코드, 응답 크기, 매칭 요소 수를 함께 평가 | 원인을 구분해 알람에 담을 수 있음 | 신호마다 임계값을 따로 설계해야 함 |
중앙값을 쓰는 이유는 하루 이상치가 평균을 끌어올리는 것을 막기 위해서입니다. 다만 최소 샘플 수(예: 7일 이상) 없이 임계값을 적용하면 운영 초반에는 알람이 아예 울리지 않거나 반대로 계속 울리는 상태가 됩니다.

이 방식이 안 통하는 경우
베이스라인 비교가 항상 들어맞지는 않습니다. 원래 수집 주기가 불규칙한 대상, 예를 들어 주 1회만 공고가 올라오는 게시판에서는 중앙값 자체가 0에 가깝게 잡히기 때문에 편차 판정이 거의 무력해집니다.
또한 신호를 여러 개 결합할수록 각 신호의 임계값을 바꿀 때마다 과거 데이터를 다시 돌려 검증해야 하는 운영 부담이 늘어납니다. 대상이 적고 변동이 큰 크롤러라면 구조적 신호 결합보다 단순 0건 체크에 수동 확인 절차를 더하는 쪽이 오히려 효율적일 수 있습니다.
재시도를 넣었는데도 왜 똑같이 0건이 나올까요?
차단이나 레이트리밋으로 인한 0건은 재시도만으로 해결되지 않는 경우가 흔합니다. IP 기반 차단이 걸려 있으면 같은 요청을 몇 번 반복해도 서버는 같은 응답을 돌려주기 때문입니다.
이때는 재시도 간격을 늘리는 것보다 응답 본문에 캡차나 차단 안내 문구가 포함되어 있는지, 서버가 robots.txt 규칙을 어겼다고 판단해 접근을 막는지부터 확인하는 편이 원인 파악에 더 빠릅니다. 재시도 횟수를 늘리는 대신 신호 자체를 바꿔서 봐야 하는 전형적인 사례입니다.
모니터링을 운영 흐름에 넣는 순서
0건을 실패로 판정하는 로직을 만들었다면, 다음 단계는 이 판정 결과를 알람 채널에 바로 연결하는 것입니다. 상태 코드와 응답 크기, 베이스라인 편차를 각각 로그에 남겨두면 나중에 오탐이 생겼을 때 어느 조건이 걸렸는지 바로 추적할 수 있습니다.
처음에는 단순 0건 체크로 시작해서 오탐·미탐 사례를 2~3주 쌓아 보고, 그 데이터를 기준으로 베이스라인 편차와 구조적 신호를 하나씩 추가하는 순서가 안전합니다. 모든 신호를 처음부터 한꺼번에 넣으면 어떤 조건 때문에 알람이 울렸는지 구분하기 어려워집니다.
