본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 05. 26. 10:27

반복적인 스크래퍼가 변경되지 않은 데이터를 다시 다운로드하고 있다면: 15줄로 해결하는 방법 (conditional GET)

요약

반복적인 웹 스크래핑 시 서버 부하를 줄이고 차단을 방지하기 위한 '조건부 GET(conditional GET)' 활용법을 소개합니다. HTTP 표준 헤더를 사용하여 데이터 변경 여부를 확인함으로써 불필요한 데이터 다운로드를 방지하는 실용적인 방법을 다룹니다.

핵심 포인트

  • 윤리적 스크래핑의 핵심은 robots.txt 준수를 넘어 서버 부하를 최소화하는 것
  • HTTP conditional GET을 통해 변경되지 않은 데이터의 재다운로드 방지 가능
  • ETag 및 Last-Modified 헤더를 활용한 효율적인 요청 메커니즘
  • 304 Not Modified 응답을 통한 네트워크 대역폭 및 파싱 비용 절감

참고: 이 글은 교차 게시물입니다. 원문(전체 긴 글)은 제 블로그에 있습니다: https://blog.spinov.online/blog/ethical-scraping-is-a-rate-limit-question/

요약 (TL;DR)

"윤리적 스크래핑 (ethical scraping)" 논쟁은 계속해서 robots.txt와 이용 약관 (ToS)에 대해 논쟁합니다. 그것들은 실재하는 것이지만, 첫 번째 요청을 보내기 전 단 한 번 내리는 결정입니다. 그것들은 200번째, 600번째, 혹은 900번째 실행에 대해서는 아무것도 알려주지 않습니다. 그리고 바로 그 지점에서 실제로 타인의 서버에 부하를 주게 되며, 실제로 차단(ban)을 당하게 됩니다. (이 포스트를 위한 좋은 프롬프트: The Web Scraping Club의 Federico Trotta의 "How to Scrape Open-Source Datasets Ethically", 2026년 5월 24일 — "Amazon 서버에서는 노이즈로 간신히 인식될 수준의 스크래퍼가 공공 데이터 포털의 성능을 진정으로 저하시킬 수 있다"는 그의 문장은 robots.txt 논쟁이 계속해서 간과하고 있는 부분입니다.)

32개의 스크래퍼를 통해 **2,190회의 프로덕션 스크래핑 (production scrapes)**을 수행한 결과(가장 바쁜 Trustpilot 리뷰 스크래퍼 하나만 해도 자체적으로 962회 실행됨), 저는 한 가지를 확신하게 되었습니다. 실제 스케줄에 따라 작동할 때, "출처에 대해 예의를 갖추는 것"과 "차단당하지 않는 것"은 두 개의 별개 질문이 아니라 하나의 질문이 됩니다. 그리고 그 해답은 robots.txt 체크박스가 아니라, 대부분 **조건부 GET (conditional GET)**과 합리적인 속도 제한 (rate limit)입니다.

해당 수치의 출처: 2026년 5월 기준, 저의 Apify 대시보드 (apify.com/knotless_cadence)입니다. 2,190회는 제가 게시한 32개 액터 (actors)의 총 실행 횟수를 합산한 것이며, 962회는 Trustpilot 스크래퍼 자체의 생애 카운터입니다. 샘플링되거나 추정된 것이 아닌 플랫폼의 가공되지 않은 수치입니다.

이 글은 실용적이고 코드 중심적인 버전입니다. 긴 형태의 논리적 근거(그리고 한 사이트를 대상으로 962회 실행하며 실제로 배운 것)는 위의 원문 포스트에 있습니다.

대부분의 스크래퍼가 건너뛰는 메커니즘: conditional GET

이것은 편법이 아닙니다. HTTP 표준(RFC 9110 §13, 그리고 이전의 관련 표준인 RFC 7232: Conditional Requests)에 명시되어 있는 내용입니다. 올바르게 요청하기만 한다면, 대부분의 서버는 본문(body)을 보내기 _전_에 페이지가 변경되었는지 여부를 — 비용 없이 — 알려줄 것입니다.

  • 서버가 응답(response) 시 ETag 및/또는 Last-Modified를 보냅니다.
  • 당신은 다음 요청 시 이를 If-None-Match / If-Modified-Since 헤더로 다시 보냅니다.
  • 변경 사항이 없다면 → 서버는 **빈 본문(empty body)**과 함께 **304 Not Modified**로 응답합니다. 당신은 파싱(parsing) 과정을 건너뛸 수 있습니다. 소스 서버는 거의 아무런 작업도 수행하지 않습니다.

304는 당신이 받을 수 있는 가장 배려 깊은 응답입니다. 이미 가지고 있는 페이지를 서버가 다시 렌더링(render)하고 전송하게 만들지 않으면서도, 새로운 데이터가 없음을 확인했기 때문입니다. 또한 당신의 파이프라인(pipeline)에 중복된 행(row)이 들어가는 것도 방지할 수 있습니다.

페처 (실행 가능, 약 15줄의 로직)

순수하게 httpx를 사용합니다. 실행 간에도 유지될 수 있도록 캐시(cache)를 디스크에 저장합니다. 하나의 호스트를 과도하게 공격하지 않도록 스스로 속도 제한(throttle)을 겁니다. requests 라이브러리도 동일하게 작동합니다 — 헤더 이름도 같고, 304 응답 방식도 같습니다.

import time
import json
import os
...

5분 안에 검증하기

httpbingo.org에는 ETag를 반환하고 If-None-Match를 준수하는 /etag/{tag} 엔드포인트가 있습니다:

f = PoliteFetcher(min_interval=0.5)
url = "https://httpbingo.org/etag/demo123"

...

제가 실행했을 때의 출력 결과입니다:

run 1: {'status': 200, 'changed': True,  'body_hash': '<your-hash>'}
run 2: {'status': 304, 'changed': False, 'body_hash': '<your-hash>'}
run 3: {'status': 304, 'changed': False, 'body_hash': '<your-hash>'}

당신의 body_hash는 다를 것입니다. httpbingo는 요청 헤더(User-Agent, 타임스탬프 등)를 본문에 그대로 에코(echo)하므로, 16진수 값은 제 것이 아니라 당신의 것입니다. 재현 가능한 것은 해시값이 아니라 200 → 304 → 304로 이어지는 상태(status) 시퀀스입니다.

나머지 절반: 설정이 아닌 예의로서의 속도 제한 (rate limit)

위의 _throttle() 함수는 의도적으로 단순하게 설계되었습니다. 호스트당 하나의 고정된 지연 시간(delay)을 갖습니다. 보통은 똑똑한 로직이 필요하지 않습니다. 접속 로그를 읽는 사람이 보고도 눈 하나 깜짝하지 않을 정도의 지연 시간이 필요할 뿐입니다. 제가 실제로 준수하는 세 가지 규칙은 다음과 같습니다:

  • 한 번에 하나의 호스트, 혹은 그에 가깝게. 서로 다른 도메인 간의 동시성 (Concurrency)은 괜찮습니다. 하지만 하나의 도메인에 20개의 워커 (worker)를 투입하는 것은 당신에 대한 차단 규칙이 만들어지게 만드는 예외적인 상황입니다. 제가 가장 오래 살아남았던 실행 방식은 호스트당 낮은 동시성을 유지하는 것이었습니다. 지루한 방식이 승리합니다.
  • 429 / Retry-After를 준수할 것. 해당 헤더는 서버가 당신에게 정중한 재시도 간격을 문자 그대로 알려주는 근거입니다. 이를 무시하는 것은 소프트한 속도 제한 (soft throttle)을 하드한 차단 (hard ban)으로 격상시키는 행위입니다.
  • 예약된 실행 시간을 분산할 것. 이것은 크론 잡 (cron job)입니다. 예산을 한 시간 동안 분산하는 것은 비용이 들지 않으며, 상대방 측의 부하 급증 (load spike)을 완화해 줍니다.

이 중 그 어떤 것도 robots.txt에 명시되어 있지 않습니다. 윤리적인 속도 제한 (rate limit)은 당신의 코드 안에 존재합니다.

"어떤 소스가 유지되는가"에 대한 솔직한 이야기

특정 사이트들의 가동 시간 (uptime) 순위를 매긴 표를 드릴 수는 없습니다. 수치를 지어내지 않고는 발표할 수 있을 만큼 소스별로 깨끗한 데이터를 가지고 있지 않으며, 수치를 지어내는 것은 스크래핑 관련 포스트를 가치 없게 만드는 가장 빠른 방법이기 때문입니다. 2,190번의 실행을 통해 제가 말할 수 있는 것은 다음과 같습니다. 계속 작동했던 소스들은 제 스크래퍼가 사려 깊은 손님처럼 행동했던(conditional GET, 지연 시간, 정직한 User-Agent 사용) 곳들이었습니다. 제가 접근을 놓친 곳들은 대개 동시성에 욕심을 부렸거나, "겨우 몇 천 페이지인데 뭐"라며 conditional GET을 건너뛰었던 곳들이었습니다.

마지막 사례는 제가 직접 실수하며 배운 것입니다. 제가 정기적으로 돌리는 스크래퍼 중 하나인 첫 번째 버전에는 conditional GET 레이어가 없었습니다. 저는 "겨우 몇 천 페이지니까, 캐싱은 나중에 추가하자"라고 생각하며 이를 건너뛰었습니다. 약 200회 실행 즈음(정확한 로그 수치는 아니며 대략적인 기억입니다)부터 이전에는 없던 속도 제한 (throttling)이 걸리기 시작했습니다. 저는 일주일 동안 사이트 탓을 했습니다. 그러다 ETag / If-None-Match 레이어를 추가하자, 실행당 요청 횟수가 줄어들었고 속도 제한도 멈췄습니다. 버그는 바로 저였습니다.

그것은 상관관계일 뿐, 통제된 실험은 아닙니다. 액세스 차단 사고 중 일부는 아마도 사이트 측에서 자체 방어 체계를 변경한 것이었을 것이며, 저와는 아무런 관련이 없을 수도 있습니다. 저는 이를 깔끔하게 분리해낼 수 없으므로, 예의 바른 행동이 가동 시간(uptime)을 '유발'했다고 가장하지는 않겠습니다. 또한 이를 특정 퍼센티지를 사용하여 업계 트렌드로 부풀리지도 않을 것입니다. 하지만 그 방향성은 미묘하지 않습니다: 예의(politeness)와 지속성(persistence)은 함께 움직입니다. 소스에 친절한 스크래퍼가 다음 분기에도 여전히 실행되고 있는 스크래퍼입니다.

전체 긴 글 (추론 과정, 962회 실행 이야기, 월요일 체크리스트): https://blog.spinov.online/blog/ethical-scraping-is-a-rate-limit-question/

저는 32개의 스크래퍼를 통해 2,190회의 프로덕션 스크래핑을 수행했습니다 (프로필: https://apify.com/knotless_cadence). 만약 200번째 실행에서 속도 제한(throttling)에 걸리는 대신 계속 작동하는 반복형 스크래퍼가 필요하다면, 제가 그것을 만듭니다 — 소스와 일정을 알려주세요: spinov001@gmail.com.

AI의 도움을 받아 초안을 작성하였으며, 제가 편집 및 사실 확인을 완료했습니다.

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0