Skip to content
게으른 엔지니어의 기술 블로그
Go back

네이버 뉴스 검색 403, 3개월치를 순차로 돌렸는데 한 번도 안 막혔다

막힌 이유를, 몇 번이고 다시 찾았다 · 1/9편

고객사 뉴스 리포트를 만드는 파이프라인 하나가 있다. 회사명과 키워드를 조합해 네이버에서 뉴스를 검색하고 웹 크롤링하고, 중복을 제거하고, LLM으로 요약해서 리포트를 낸다. 이 검색 단계는 두 가지 방식으로 돌릴 수 있다 — 네이버 공식 검색 API, 또는 날짜 범위를 직접 지정할 수 있는 대신 공식 계약이 없는 search.naver.com 스크래핑 방식. 날짜 범위를 지정할 수 있다는 장점 때문에 스크래핑 방식(이하 web 백엔드)을 실전에 붙여봤다.

그런데 21개 타깃, 1년치 기간으로 실전 규모를 돌린 첫 실행에서 결과가 무너졌다. 공식 API로 돌렸을 때는 55,366개였던 발견 URL 수가, web 백엔드로는 100개로 줄어 있었다. 커밋 메시지에 “rate-limit/backoff 없음이 원인으로 추정”이라고 남기고, 일단 기본값을 다시 공식 API로 되돌려뒀다. 그 뒤로 이 web 백엔드는 방치돼 있었다.

차단 원인 후보: 요청량 · 속도 · 동시성

이 붕괴의 원인 후보는 세 가지였다.

문제는 이 세 가지가 실전 사고 당시 전부 동시에 섞여 있었다는 것이다. 21개 타깃이 ThreadPoolExecutor(max_workers=10)로 동시에 돌았고, 요청 사이 딜레이도 없었고, 총 요청 수도 당연히 많았다. 뭐가 진짜 방아쇠였는지 분리할 수가 없었다.

그래서 변수를 하나씩 통제한 실험을 순서대로 해보기로 했다. 파이프라인 본체는 건드리지 않고, 완전히 분리된 1회성 진단 스크립트로.

1차 — 순차 + 딜레이 1초

가장 보수적인 조건부터 시작했다. 키워드 하나(“삼성전자” “로봇” — 실전 사고 당시 삼성그룹 리포트와 같은 키워드), 완전 순차 실행(동시성 0), 페이지 요청 사이 1초 고정 딜레이. 1년치 기간을 30일 단위로 쪼개서 훑고, 에러가 뜨면 그 즉시 멈추도록 했다. 하드캡은 400건.

400건 전부 HTTP 200. 에러 0건. 하드캡에 걸려서 스스로 멈췄을 뿐, 막혀서 멈춘 게 아니었다. 3개월 분량(약 1,890페이지에 해당하는 순차 접근)을 21분 동안 딜레이만 주고 돌렸는데 단 한 번도 안 걸렸다.

2차 — 동시성 5 + 딜레이 1초

같은 키워드의 1년치를 5개 구간으로 나눠, 각 구간을 워커 하나가 맡게 하고 5개 워커를 동시에 돌렸다. 워커 안에서는 1차와 똑같이 순차 + 1초 딜레이. 동시성이라는 변수 하나만 더한 셈이다.

이번에도 400건 전부 200, 에러 0건. 다만 소요 시간은 1차의 21분에서 4분 39초로 줄었다 — 워커 5개로 나눈 만큼 정확히 비례해서 빨라졌다(약 4.55배). 딜레이가 워커마다 독립적으로 걸리니 병렬화 효과가 그대로 나타난 것이다. 그리고 이 조건에서도 막힘은 한 번도 없었다.

이 시점에서 살짝 무게가 실리는 쪽은 “동시성 자체보다는 딜레이 없음 (요청 속도)이 진짜 원인 아닐까”였다. 다만 실전 사고는 동시성 10이었고 이번 실험은 5였다는 차이가 남아 있어서, 아직 단정할 단계는 아니었다.

3차 — 순차 + 딜레이 없음

이번엔 반대쪽을 없앴다. 1차와 똑같이 완전 순차인데, time.sleep() 호출 자체를 지워서 딜레이를 뺐다. 실전 사고와 가장 가까운 조건이라 하드캡은 좀 더 보수적으로 100건으로 낮췄다.

100건 전부 200, 에러 0건. 딜레이를 완전히 뺐는데도 안 막혔다 — 예상과 다른 결과였다.

여기서 눈여겨볼 부수 발견이 하나 있었다. time.sleep()을 뺐다고 해서 요청이 진짜로 촘촘하게 나간 건 아니었다는 것. 요청을 보내는 함수가 동기 호출이라, 응답이 돌아올 때까지(평균 2.5초 정도) 다음 요청을 못 보낸다. 즉 이 순차 구조에서는 “딜레이 없음”이 사실상 “요청당 자연스러운 간격 2.5초”와 같은 뜻이었다. 진짜로 촘촘하게 두드리려면 애초에 동시성이 있어야 한다 — 여러 연결을 동시에 열어야 실제 요청 간격이 좁아진다.

세 번 재현해도 403은 뜨지 않았다

세 번의 통제 실험 전부 무차단이었다. 표로 정리하면 이렇다.

실험동시성딜레이결과
1차0(순차)1초400건 무차단
2차51초(워커별)400건 무차단
3차0(순차)없음100건 무차단
실전 사고10없음대량 차단, 55,366→100건 붕괴

3차의 관찰(순차 구조에서는 딜레이 유무와 무관하게 요청 간격이 실제 응답 시간에 묶인다)까지 고려하면, 진짜 촘촘한 요청은 동시성이 있어야만 나온다는 뜻이 된다. 그렇다면 지금까지 확인된 조합 중 유일하게 안 해본 건 “동시성 10” — 실전 사고와 같은 조건이다.

다만 그건 지금까지 실험 중 가장 위험한 조건이기도 하다. 실제로 막힐 가능성이 제일 높고, 만약 막히면 그 자체로 유의미한 결과지만, 이 IP에 대한 네이버 쪽 “낯익음”이 더 나빠질 수도 있다. 다른 가능성도 아직 남아 있었다 — 실전 사고는 21개 타깃이 각자 다른 키워드를 썼다는 점 (이번 실험들은 전부 같은 키워드), 실전 사고가 이번 실험들보다 훨씬 많은 총 요청을 냈다는 점, 그날 이 IP가 이미 어느 정도 “낯이 익어서” 임계값이 낮아져 있었을 가능성.

남는 생각

세 번의 실험을 조심스럽게 쌓아가는 동안, 진짜 원인이 무엇인지는 전혀 좁혀지지 않았다. 오히려 “딜레이가 없어도 안 막힌다”는, 처음 세웠던 가설(딜레이 없음이 원인일 것)과 어긋나는 결과만 하나 더 쌓였다. 통제 실험이 늘 원인을 좁혀주는 건 아니다 — 때로는 후보를 하나씩 지워가며 마지막 남은 조합(“동시성 10”)으로 몰아가는 역할을 할 뿐이고, 그 마지막 조합은 하필 가장 재현하기 조심스러운 조건이었다.


Share this post:

막힌 이유를, 몇 번이고 다시 찾았다

  1. 1. 네이버 뉴스 검색 403, 3개월치를 순차로 돌렸는데 한 번도 안 막혔다
  2. 2. 차단됐다는 증거부터 없었다
  3. 3. 식은 IP도 순식간에 막혔다, 고쳤더니 이번엔 너무 느렸다
  4. 4. 프록시는 한 번도 쓸 일이 없었다
  5. 5. 두 배 빨라졌다, 그리고 다시 막혔다
  6. 6. 고쳤는데도 안 됐다, 진짜 이유는 따로 있었다
  7. 7. 순서를 뒤집자, 패턴도 정확히 뒤집혔다
  8. 8. 가장 많이 죽던 자리를 그대로 두고 다시 돌렸는데, 이번엔 다 살았다
  9. 9. 30분에서 1분까지, 얼마나 줄일 수 있는지 끝까지 밀어봤다