이 프로젝트는 조사 주제마다 “가이드”(검색할 회사와 키워드를 정해둔 YAML
설정 파일)를 하나씩 두고, 그 키워드로 네이버 뉴스를 검색해 기사를 모으는
파이프라인이다. 9편까지
physical_ai_corp 가이드에서 네이버가 내던 403(접근 거부)은, 검색 요청을 작은
배치로 쪼개고 배치 사이에 진짜로 쉬는 시간을 주는 걸로 해결됐다(30분에서
시작해서 1분까지 줄여도 클린했다). 그런데 국방 관련 조사를 위한 defense_aerospace_2024 가이드는 며칠째 계속
403을 내고 있었다. 이번에도 같은 방법이 통할 거라고 생각했다.
첫 번째 시도 — 38시간 쉬었는데도 안 됐다 (네이버 크롤링 403 대기 시간)
이전 시도 기록을 뒤져보니 defense_aerospace_2024는 세 번(1분 휴식, 15분 휴식 두 번)이나 시도했는데 전부 실패해 있었다. 마지막 시도로부터 38시간 20분이 지난 상태였다. “이 정도면 충분히 쉬었겠지” 싶어서 9편에서 검증한 구조(배치를 두 그룹으로 나눠 동시에 돌리고, 각 그룹 안에서는 배치 2개를 차례로 돌리며 사이에 15분씩 쉬는 구조) 그대로 다시 돌렸다.
결과는 똑같았다. 배치의 첫 쿼리부터 곧바로 403이 났다. 시작 9분 만에 두 배치에서 116건, 114건씩 “둘 다 실패”(요청을 보낼 수 있는 두 경로가 모두 실패했다는 로그)가 쌓였고, 멈출 때까지 146건, 140건이 됐다.
두 번째 시도 — 배치 크기를 줄여봤지만 역시 틀렸다
이상한 점이 하나 있었다. 같은 VPS IP로 쿼리 두 개(physical_ai_corp 것 하나, defense_aerospace_2024 것 하나)를 배치 구조 없이 따로 날려 보니 둘 다 바로 성공했다. IP가 막힌 것도, 쿼리 내용이 차단 대상인 것도 아니었다. 그러면 남는 건 배치 구조였다.
다음 가설은 그래서 배치 크기였다. physical_ai_corp의 성공 사례를 다시 보니 배치당 타깃 4개 이하였는데, defense_aerospace_2024는 4개짜리 원본 배치 두 개가 5개씩이었다. 검증된 상한을 넘긴 게 원인일 수 있겠다 싶어서, 13개 타깃을 전부 4개 이하로 재분할하고 다시 돌렸다.
또 실패했다. 이번에도 첫 쿼리부터 바로 막혔다. 배치 크기도 원인이 아니었다.
최소 단위까지 줄여보기로 했다
배치 구조 없이 따로 날린 쿼리는 성공하는데, 같은 쿼리도 파이프라인을 거치면 배치 크기와 무관하게 즉시 막혔다. 다음 단계로 키워드가 4개뿐인 가장 작은 타깃 하나만 파이프라인으로 돌려 보기로 했고, 로그에는 403뿐 아니라 “Connection refused” 에러가 섞여 있었다. 파이프라인을 거치면 실패하고, 파이프라인을 건너뛰면 성공하는 상황이었다.
코드를 열어보니 버그였다 — Python urllib 예외 처리와 폴백
naver_search.py의 요청 로직을 다시 읽었다. 검색 요청을 청크 단위로 나눠,
절반은 VPS의 IP로, 나머지 절반은 집 PC를 거치는 프록시(가정용 IP)로 먼저
보내는 2레인 병렬 구조였는데, 예외 처리에 구멍이 있었다. 403 에러
(HTTPError)가 나면 재시도 후 반대쪽 경로로 다시 보내는 폴백을 시도하게
돼 있었는데, Connection refused 같은 다른 종류의 예외는 이 폴백
로직을 아예 안 타고 그냥 실패 처리로 끝나 있었다. 확인해보니 집
프록시 터널(SSH 포트포워딩)이 그 사이 죽어 있었다.
2레인 구조상 청크의 절반은 프록시가 주 경로로 배정되는데, 그 청크들은
터널이 죽어있으니 시작하자마자 Connection refused가 났고, 코드는 그 즉시
포기해버렸다 — 멀쩡한 VPS 직접 IP로 한 번도 안 타보고. “둘 다 실패”로
찍히던 로그의 상당수가 사실 한쪽(VPS 직접)은 시도조차 안 된 거였다.
우회해서 검증했다
집 PC를 지금 당장 켤 수는 없어서(터널 복구는 나중으로 미뤘다), VPS 직접 경로만 쓰도록 프록시 폴백을 우회하는 진단 스크립트를 만들어 다시 돌렸다.
- 최소 단위(타깃 1개, 키워드 4개): 403 0건, 24초 완주
- 실제 배치 크기(타깃 4개, 키워드 42개): 1단계(검색) 403 0건, 클린
- 전체 가이드(타깃 13개, 키워드 144개, 2024년 이후 기간): 403 0건, URL 24,372개 발견
배치 크기도, 쉬는 시간도 문제가 아니었다는 게 확실해졌다.
코드를 고치고, 크롤링 단계도 보강했다 — Jina AI Reader
HTTPError 처리와 동일하게, 다른 종류의 예외에서도 폴백을 시도하도록
고쳤다. 겸사겸사 기사 본문 크롤링 단계에도 마지막 폴백을 하나 더
추가했다 — Jina AI Reader(API 키 없이 무료로 쓸 수 있는 리더 서비스)를
붙였다. 이전에 실패했던 URL 107개로 실측해보니 69개(64%)가 이걸로
구제됐다.
그리고 진짜 대조군을 만들었다
여기까지도 완전히 확신하긴 일렀다 — “38시간 쉬어서 됐다”와 “버그를 고쳐서 됐다”를 가르는 진짜 실험은 아직 안 했으니까. 마침 방금 전체 가이드를 한 번 돌려서 VPS IP를 최대한 쓴 직후였다. 그 상태 그대로, 0분 휴식으로 검색 단계만 다시 돌렸다. 심지어 크롤링 단계가 같은 IP로 아직 돌고 있는 와중이었다.
결과: 타깃 13개, 오류 0건, 3분 27초 만에 URL 24,368개. 403이나 실패 로그가 단 한 줄도 안 찍혔다.
남는 생각
“쉬는 시간이 부족해서”라는 가설은 이전에 다른 가이드(physical_ai_corp)에서 실제로 맞았던 설명이었다. 그 성공 경험을 그대로 옮겨왔는데, 이번엔 완전히 다른 원인(코드 버그 + 인프라 장애가 겹친 것)이었다. 이미 검증된 해법이라고 해서 다른 상황에도 그대로 통하는 건 아니었다 — 배치를 재설계하고, 최소 단위까지 줄여보고, 결국 코드를 직접 읽고 나서야 진짜 원인을 찾았다. 그리고 그 원인을 고친 다음에도, “정말 쉬는 시간이 필요 없다”는 걸 증명하려면 결국 쉬지 않고 바로 돌려보는 대조군 실험이 필요했다. 원인을 찾았다고 믿는 것과, 그 믿음을 실제로 반증 가능한 형태로 검증하는 것은 다른 단계였다.