대규모 웹 크롤러 시스템 설계
분류: Layer 9 - 아키텍처 & 설계 패턴
대규모 웹 크롤러 시스템 설계
섹션 제목: “대규모 웹 크롤러 시스템 설계”1. 한 줄 정의
섹션 제목: “1. 한 줄 정의”대규모 웹 크롤러는 수십억 개의 URL을 URL Frontier(우선순위·정중함을 관리하는 분산 큐), Fetcher(HTTP 다운로더), Parser(링크 추출), Storage(콘텐츠/메타데이터 저장), Dedup(중복 URL/콘텐츠 제거)로 분리해 수평 확장하면서, robots.txt와 도메인별 rate limit을 지키도록 설계된 분산 시스템이다.
2. 왜 중요한가
섹션 제목: “2. 왜 중요한가”프론트엔드 브릿지: SPA 라우터에서 동적 쿼리 파라미터로 무한히 새로운 URL이 만들어지는 상황을 떠올려 보자. ?sort=price&size=M&color=red 같은 조합이 384가지로 폭증하는 현상은 검색 엔진 입장에서 crawler trap과 동일한 문제다. 프론트엔드에서 라우팅 키를 정규화하지 않으면 상태 관리가 망가지듯, 크롤러도 URL을 정규화하지 않으면 같은 페이지를 수백 번 다운로드한다.
BackOps 엔지니어로 성장하면서 이 지식이 필요한 이유:
- 시스템 설계 인터뷰 단골 주제: “Design a web crawler”는 Google·Meta·Amazon 인터뷰에서 가장 자주 나오는 주제 중 하나다. URL Frontier, Bloom filter, politeness 정책을 설명할 수 있어야 한다.
- 분산 큐 설계 감각: 크롤러는 SQS·Kafka·Redis 기반 분산 큐 설계의 가장 잘 알려진 사례다. 이걸 이해하면 EDA(Event Driven Architecture)·작업 큐·배치 파이프라인 설계가 모두 같은 패턴임을 알게 된다.
- 회사 내부 데이터 수집 파이프라인: 외부 API 데이터, 경쟁사 가격 모니터링, 컴플라이언스 로그 수집 같은 사내 BackOps 업무도 결국 “정중하게, 중복 없이, 장애에 강하게” 가져오는 문제로 귀결된다.
- 법적·윤리적 경계 인지: robots.txt 무시, 과도한 요청, 인증 우회는 LinkedIn vs hiQ Labs 같은 실제 소송 사례로 이어진다. 시스템 설계 단계에서 경계를 그어야 한다.
2.1 선행 한계와 등장 배경
섹션 제목: “2.1 선행 한계와 등장 배경”초기 웹 크롤러는 BFS에 가까운 단일 FIFO 큐로도 설명할 수 있었다. 그러나 웹 페이지의 상대 링크는 같은 호스트 URL을 연속으로 만들기 쉽고, 여러 worker가 단일 FIFO에서 동시에 dequeue하면 같은 서버에 병렬 요청이 몰린다. Mercator 논문은 이 현상이 서버를 과부하시키거나 일부 서버를 중단시킬 수 있다고 보고했고, “호스트당 동시 요청 1개” 수준의 약한 정중함만 적용한 수천만 문서 크롤에서도 운영자 불만이 발생했다고 설명한다. 즉 문제는 단순히 URL이 많다는 점이 아니라 URL 폭발, 같은 호스트로의 요청 집중, 중복 URL 판정 비용이 동시에 커지는 데 있다.
이 토픽의 핵심 메커니즘은 그 한계를 셋으로 분해해 푸는 것이다. URL Frontier는 front queue에서 중요도 순서를 보존하고 back queue에서 호스트별 요청 간격을 강제한다. Mercator의 프로덕션 설정은 crawling thread 수의 약 3배 back queue를 두고, 한 호스트의 직전 다운로드 시간보다 10배 긴 시간을 기다린 뒤 다시 접속했다. 중복 URL 제거도 메모리 HashSet만으로는 커진다. Mercator는 10억 URL checksum 저장에 5GB 이상이 필요하고, 디스크 기반 hash table은 평균 8ms seek 때문에 초당 750회 중복 검사, 페이지당 평균 10개 링크 기준 초당 75 다운로드 수준으로 병목이 된다고 분석했다. 그래서 현대 설계는 URL 정규화, 메모리 1차 필터, 영구 저장소 확인을 계층화한다. 출처: Mercator 논문 https://www.cs.cornell.edu/courses/cs685/2002fa/mercator.pdf.
3. 핵심 개념
섹션 제목: “3. 핵심 개념”3.1 URL Frontier (Front/Back Queue 이중 구조)
섹션 제목: “3.1 URL Frontier (Front/Back Queue 이중 구조)”URL Frontier는 단순한 FIFO가 아니다. 우선순위(중요한 페이지를 먼저) 와 정중함(같은 호스트에 동시 접속 금지) 이라는 두 제약을 동시에 만족해야 한다. Mercator 논문(1999, Compaq SRC)은 front-end FIFO queue가 priority를, back-end FIFO queue와 heap이 강한 politeness를 담당하는 구조를 제안했다. 논문 기준 프로덕션 크롤은 back queue를 worker thread의 약 3배로 두고, 같은 호스트 재접속을 직전 다운로드 시간의 10배 뒤로 미뤘다.
[Priority 분류기] │ ▼┌──────────────────────────────┐│ Front Queues (우선순위 F개) │ ← URL 들어옴│ F1 (high) F2 (mid) F3 (low) │└──────────────────────────────┘ │ (Biased queue selector — 높은 우선순위를 더 자주 뽑음) ▼┌──────────────────────────────┐│ Back Queues (호스트당 1개, B개)│ ← 호스트별로 분리│ B1: example.com ││ B2: news.com ││ B3: blog.io │└──────────────────────────────┘ │ (Heap: 다음 fetch 가능 시각으로 정렬) ▼[Fetcher Worker Pool]왜 두 단계인가:
- Front Queue 단독으로는 정중함 위반: 우선순위만으로 뽑으면 같은 인기 사이트(예: news.com)가 연속으로 선택되어 서버를 폭격한다.
- Back Queue 단독으로는 우선순위 무시: 호스트별로만 나누면 중요한 페이지와 무관한 페이지를 똑같이 처리한다.
- 이중 구조의 효과: Front에서 우선순위 기반 추출 → Back에서 호스트별 직렬화. 두 제약이 충돌하지 않는다.
- K8s work queue와 같은 원리: controller-runtime의 rate-limiting queue도 key 단위 dedup과 재시도 backoff를 섞는다. URL Frontier의 host key를
namespace/name리소스 key로 바꾸면, 특정 리소스 장애가 전체 reconcile 처리량을 잡아먹지 않게 하는 같은 패턴이다.
불변식 (Mercator 원본):
- Back queue는 크롤이 진행 중인 동안 비어 있지 않다.
- Back queue는 단일 호스트의 URL만 담는다.
- Heap은 (다음 fetch 가능 시각, back queue index)를 키로 정렬한다.
새 URL 도착: hash(host) → 매핑된 back queue로 직행 매핑 없으면 빈 back queue를 할당 + heap 갱신
Fetcher가 URL 요청: heap.pop() → (시각, queue_idx) if 시각 > now: sleep(시각 - now) url = back_queue[queue_idx].pop() if back_queue 비었음: front queue에서 새 host 채움 (호스트 ↔ queue 매핑 갱신) fetch(url) heap.push((now + delay(host), queue_idx))3.2 Politeness Policy (robots.txt + Crawl-Delay + Adaptive Backoff)
섹션 제목: “3.2 Politeness Policy (robots.txt + Crawl-Delay + Adaptive Backoff)”정중함은 단순히 “느리게 요청”이 아니라 명시적 규칙 + 측정 기반 적응의 조합이다.
| 계층 | 메커니즘 | 출처 |
|---|---|---|
| 명시적 | robots.txt의 User-agent, Disallow, Allow | RFC 9309 (2022) |
| 명시적 | Crawl-delay | 비표준 확장 |
| 명시적 | <meta name="robots" content="noindex,nofollow"> | HTML 표준 |
| 명시적 | X-Robots-Tag HTTP 헤더 | 비표준 (Google) |
| 측정 기반 | 응답 시간 증가 시 backoff | Mercator heuristic |
| 응답 기반 | HTTP 429 (Too Many Requests)의 Retry-After가 있으면 우선함 | RFC 6585 |
| 응답 기반 | HTTP 503 시 더 긴 cooldown | RFC 7231 |
Mercator 휴리스틱 (현장 표준):
같은 호스트에 대한 다음 요청 간격은 직전 응답 시간의 10배 이상.
- 직전 fetch가 200ms 걸렸으면 → 다음 요청은 최소 2초 뒤.
- 직전 fetch가 5초 걸렸으면 → 50초 뒤. 서버가 느리면 자동으로 더 정중해진다.
- 이 휴리스틱은 도메인별 부하를 동적으로 추적하므로, 정적
crawl-delay: 10보다 우월하다.
HTTP 상태 코드 기반 동작:
200 OK → 현재 rate 유지301/302 Redirect → 새 URL을 frontier에 추가, 원본은 처리 완료 표시404 Not Found → 영구 실패로 기록, 재시도 안 함429 Too Many → Retry-After 헤더가 있으면 그 시간까지 대기, 없으면 exponential backoff500 Internal → 짧은 backoff 후 1~2회 재시도503 Service Unavail → 긴 backoff (분 단위), 재시도 횟수 제한RFC 6585는 429가 rate limiting을 의미하며 Retry-After를 포함할 수 있다고 정의하지만, 반드시 온다고 보장하지 않는다. 따라서 429를 받았는데 Retry-After가 없으면 delay(host)=min(delay*2, 1h)처럼 호스트별 지수 backoff를 적용하고, 있으면 서버가 제시한 시간을 우선한다. 이 규칙은 외부 API polling에도 그대로 전이된다. API가 X-RateLimit-Remaining을 주더라도 실제 차단은 IP·계정·resource 단위로 섞일 수 있으므로, 성공 응답 헤더만 믿고 글로벌 rate를 올리는 것은 silent failure의 시작이다.
robots.txt 캐싱:
- 호스트당 한 번만 받고 24시간을 넘기지 않는 것을 기본으로 캐싱한다. RFC 9309는 표준 HTTP cache-control을 허용하되, robots.txt가 unreachable인 경우를 제외하면 24시간 초과 캐시 사용을 권장하지 않는다.
- 파서는 최소 500KiB까지 처리할 수 있어야 한다. 이보다 큰 파일은 crawler 보호를 위해 상한을 둘 수 있지만, 500KiB 미만에서 자르면 표준 호환성이 깨진다.
- robots.txt 요청이 5xx나 네트워크 오류로 unreachable이면 “허용”이 아니라 “전체 disallow”로 취급한다. 반대로 404처럼 unavailable이면 접근 가능하다고 볼 수 있다.
- 캐싱 안 하면 모든 fetch 전에 robots.txt를 다시 받는 N+1 anti-pattern이 발생한다.
- 캐싱 자료구조: 호스트 → (parsed rules, fetched_at) 매핑. Redis로 분산 공유.
- 출처: RFC 9309
https://datatracker.ietf.org/doc/rfc9309/, RFC 6585https://www.rfc-editor.org/rfc/rfc6585.
3.3 중복 URL 검출 (URL Seen Test)
섹션 제목: “3.3 중복 URL 검출 (URL Seen Test)”수십억 URL을 모두 메모리에 두면 비용이 폭발한다. 단계별 자료구조를 쌓는다.
1단계: URL 정규화 (Canonicalization)
같은 페이지가 여러 URL로 표현되는 문제를 먼저 제거한다.
원본: HTTP://Example.com:80/Path/?utm_source=x&id=42#section정규화: http://example.com/path/?id=42
규칙: - 스킴/호스트 소문자 - 기본 포트(80, 443) 제거 - fragment(#) 제거 - 추적 파라미터(utm_*, fbclid, gclid) 제거 - 쿼리 파라미터 알파벳 정렬 - 후행 슬래시 정책 통일2단계: Bloom Filter (메모리 1차 필터)
- 공간 효율: URL 10억 개를 ~2GB로 표현 (false positive 1% 기준).
- 동작: hash(url)을 k개 비트 위치에 set. 모든 비트가 1이면 “이미 봤을 가능성”.
- 한계: false positive 존재 (실제로는 안 봤는데 봤다고 판정 — 일부 새 URL을 놓침).
- 용도: 명백히 새 URL을 빠르게 frontier에 넣고, 의심되는 URL만 다음 단계로.
3단계: 영구 저장소 확인 (정확한 중복 제거)
- Bloom filter가 “봤다”고 판정한 URL만 영구 저장소(Cassandra, ScyllaDB, RocksDB)에 hit/miss 조회.
- 영구 저장소 키: 정규화된 URL의 64비트 hash (저장 공간 절약).
- 분산 환경에서는 URL hash로 샤딩하여 한 URL은 항상 같은 노드가 소유.
4단계: 콘텐츠 중복 (Near-Duplicate Detection)
URL이 다른데 콘텐츠가 거의 같은 경우 (mirror site, syndication 기사). 연구에 따르면 웹 페이지의 약 30%가 다른 70%의 (near-)duplicate다.
- SimHash (Charikar, 2002): 문서 → 64비트 fingerprint. Hamming distance ≤ 3이면 duplicate.
- MinHash + LSH: shingles → MinHash signature → LSH로 유사 문서 묶기.
- 산업 표준은 SimHash. Google이 2007년 논문에서 자사 deduplication을 SimHash 기반으로 공개했다 (Manku et al., 2007, “Detecting near-duplicates for web crawling”).
SimHash가 깨지는 조건과 대안 선택 기준:
| 조건 | SimHash 동작 | 권장 대안 |
|---|---|---|
| 짧은 문서 (300자 미만) | 해시 비트 충돌 증가 → false positive 폭증 | MinHash + k-shingle (k=3~5) |
| 다국어 (한중일) | 형태소 분리 없이 문자 n-gram만 쓰면 유사도 과소평가 | 언어별 tokenizer 적용 후 shingle |
| 코드/로그 문서 | 변수명·숫자 변화가 커 Hamming distance 크게 나옴 | 정규화(숫자 → NUM, 식별자 → ID) 후 SimHash |
| 양이 많고 Jaccard 유사도 기준이 명확한 경우 | SimHash는 Jaccard와 직접 대응 안 됨 | MinHash + LSH (Jaccard 0.8+ 기준 명시 가능) |
실용 선택 규칙 (Google 사례 기반):
- 웹 문서 전체 dedup → SimHash (Google Search, 2007). Google의 WWW 2007 논문은 80억 페이지 저장소에서 64비트 SimHash와 Hamming distance
k=3이 합리적이고, 이 설정의 precision/recall이 둘 다 약 0.75 근처라고 보고했다. - 뉴스 개인화·short-text 클러스터링 → MinHash + LSH (Google News, 2007)
- 두 방식 혼용: URL dedup + URL 정규화는 SimHash, short snippet dedup은 MinHash
- 실패 시나리오: 광고·timestamp만 다른 긴 웹문서는 SimHash가 잘 맞지만, 200자짜리 상품명이나 로그 한 줄은 몇 단어 차이로 64비트 fingerprint가 크게 흔들릴 수 있다. 이때
k를 3에서 8로 올리면 recall은 늘지만 false positive도 함께 늘어 “새 상품 상세 페이지를 중복으로 버리는” 조용한 누락이 생긴다. 짧은 텍스트에서는 URL canonical key와 shingle 기반 MinHash를 같이 보고, dedup 판정은 삭제가 아니라 crawl priority 감점으로 먼저 적용한다. - 출처: Google Research, “Detecting Near-Duplicates for Web Crawling”
https://research.google.com/pubs/archive/33026.pdf.
3.4 Fetcher / Parser / Storage 분리
섹션 제목: “3.4 Fetcher / Parser / Storage 분리”각 컴포넌트의 자원 특성이 다르므로 독립 스케일링이 핵심이다.
| 컴포넌트 | 자원 특성 | 스케일 차원 |
|---|---|---|
| Fetcher | 네트워크 I/O 바운드 | IP 풀, 동시 연결 수 |
| Parser | CPU 바운드 (HTML/JS 파싱) | 코어 수 |
| Storage | 디스크 I/O + 네트워크 | 샤드 수, 복제 |
| Frontier | 메모리 + 영구 저장소 | 파티션 수 |
[Seed URLs] │ ▼┌──────────────┐ fetch req ┌──────────────┐│ URL Frontier │ ──────────────► │ Fetcher ││ (Redis/Kafka) │ │ (HTTP) │└──────────────┘ ◄────────────── └──────────────┘ ▲ new URLs │ │ ▼ raw HTML │ ┌──────────────┐ │ extracted links │ Parser │ └─────────────────────────│ (HTML/JS) │ └──────────────┘ │ ▼ structured doc ┌──────────────┐ │ Storage │ │ (S3 + Index) │ └──────────────┘컴포넌트 간 통신은 큐로:
- Fetcher → Parser: Kafka topic
raw_pages(S3 pointer + metadata). - Parser → Frontier: Kafka topic
discovered_urls. - Parser → Storage: 직접 또는 Kafka
parsed_docs. - 큐 기반이라 한 컴포넌트가 느려져도 다른 쪽이 멈추지 않는다 (backpressure는 큐 길이로 측정).
Frontier 백프레셔 측정 기준 (Kafka 기반):
| 지표 | 경보 임계치 | 조치 |
|---|---|---|
raw_pages consumer lag | 10,000 메시지 초과 | Parser 인스턴스 스케일 아웃 |
| Lag 증가 추이 | 5분 내 10% 이상 지속 증가 | Fetcher rate 즉시 감속 |
discovered_urls lag | 50,000 초과 | URL dedup 노드 점검 |
| DLQ 메시지 급증 | 1분 내 100건 초과 | 특정 도메인 fetch 오류 조사 |
- Kafka 단순 lag 임계치(
lag > N)는 트래픽 스파이크 시 false positive가 많다. LinkedIn의 Burrow 같은 추이 기반(trend-based) 평가가 프로덕션 표준 (출처: LinkedIn Engineering Blog). - 모니터링: Prometheus
kafka_consumer_group_lag+ Grafana alert. Lag이 선형 증가 시 파티션 수 증설 또는 소비자 추가가 근본 해결. - SQS 기반이면
ApproximateNumberOfMessagesNotVisible(처리 중) +ApproximateNumberOfMessages(대기 중) 합계로 동일한 역할. AWS 콘솔 또는 CloudWatch Metric alarm으로 설정한다.
JavaScript 렌더링 분기:
- 일반 HTML: 단순 fetch + parse (가벼움).
- SPA / 동적 페이지: Headless Chromium (Puppeteer, Playwright) — CPU/메모리 무겁다.
- 비용 차이가 10~100배라 페이지 유형을 분류해 필요한 경우만 렌더링한다.
3.5 분산 환경에서의 장애 복구 & 체크포인팅
섹션 제목: “3.5 분산 환경에서의 장애 복구 & 체크포인팅”분산 크롤러는 한 노드가 죽어도 작업이 사라지지 않아야 하고, 죽은 노드의 작업이 다른 노드에서 재개되어야 한다.
핵심 메커니즘:
| 메커니즘 | 역할 | 구현 |
|---|---|---|
| At-least-once delivery | URL이 최소 한 번은 처리됨 | SQS visibility timeout, Kafka offset commit |
| 멱등성 | 같은 URL을 두 번 처리해도 결과 동일 | URL hash 기반 dedup 영구 저장소 |
| Lease / Visibility timeout | 워커가 URL을 가져가면 N분간 다른 워커가 못 봄 | SQS 30s~12h, Redis SETNX with TTL |
| Heartbeat | 워커 살아있음 신호 | 분산 lock, 5~10초 주기 |
| Checkpointing | Frontier 상태를 주기적으로 영구화 | Kafka offset, RocksDB snapshot |
Lease 만료 → URL 재할당 단계별 시나리오 (SQS 기준):
1. Worker A가 SQS에서 URL 메시지를 수신 → 메시지가 visibility timeout(기본 30초) 동안 다른 워커에게 불가시
2. Worker A가 처리 중 heartbeat를 보내며 ChangeMessageVisibility API로 timeout을 연장 (max 12시간, 10~30초 주기)
3-a. [정상] Worker A가 처리 완료 → DeleteMessage 호출 → 메시지 큐에서 제거 → 다른 워커는 이 URL을 볼 수 없음 (중복 처리 방지)
3-b. [장애] Worker A가 죽음 → heartbeat 중단 → timeout 만료 → 메시지가 다시 가시(visible) 상태로 전환 → Worker B가 동일 URL 메시지를 수신 → 재처리
4. [반복 장애] 같은 메시지가 maxReceiveCount(예: 5회) 초과 → Dead Letter Queue(DLQ)로 이동 → 별도 alert + 수동 조사- 멱등성 보장 방법: Worker B가 URL을 처리할 때 영구 저장소에서 “이미 처리됨” 여부를 먼저 확인 후 skip. 이 체크가 없으면 같은 URL이 S3에 두 번 쓰여도 마지막 버전이 남는 구조여야 함 (last-write-wins).
- visibility timeout 설정 기준: 평균 fetch 처리 시간의 2
3배. 평균 fetch가 5초라면 visibility timeout은 1015초. 너무 짧으면 처리 중에 재분배, 너무 길면 장애 감지가 느리다.
At-least-once의 함의:
- 중복 처리는 반드시 발생한다 (네트워크 타임아웃 후 워커가 ack 못 했는데 실제로는 성공한 경우).
- → 모든 처리는 멱등이어야 한다. URL → 결과 매핑이 같은 URL 두 번 처리해도 동일해야 한다.
- → 이게 분산 시스템 기초(L9 distributed-systems-basics)의 “Eventual Consistency + Idempotency” 원리가 크롤러에서 구체화된 모습이다.
결정 기준과 실패 시나리오:
SQS를 쓰는 경우 visibility timeout은 “평균”이 아니라 p95 처리 시간과 heartbeat 실패 시간을 기준으로 잡는다. AWS 문서 기준 기본값은 30초, ChangeMessageVisibility로 연장 가능하지만 최초 수신 시점부터 최대 12시간을 넘길 수 없다. 처리 p95가 45초이고 heartbeat 주기가 15초라면 초기 timeout을 2분으로 두고, ApproximateNumberOfMessagesNotVisible이 계속 증가하는지 확인한다. 표준 큐의 in-flight 메시지는 약 120,000개 한도가 있어, 이 값을 넘기면 short polling에서는 OverLimit이 나고 long polling에서는 새 메시지가 오지 않는 것처럼 보일 수 있다.
aws sqs get-queue-attributes \ --queue-url "$QUEUE_URL" \ --attribute-names ApproximateNumberOfMessages ApproximateNumberOfMessagesNotVisible ApproximateAgeOfOldestMessage예상 출력은 대기 메시지, 처리 중 메시지, 가장 오래된 메시지 나이가 함께 증가하지 않는 상태다. ApproximateNumberOfMessagesNotVisible과 ApproximateAgeOfOldestMessage가 같이 증가하면 worker가 죽은 것이 아니라 heartbeat만 살아 있는 zombie 처리나 downstream storage hang을 의심한다. 이때는 timeout을 늘리기보다 해당 domain key를 일시 정지하고, worker 로그에서 URL hash 기준 idempotency check가 성공했는지 먼저 확인한다.
Kafka 기반이면 단순 lag > 10000 임계치만으로는 스파이크와 장애를 구분하기 어렵다. LinkedIn Burrow는 lag 단일 숫자 대신 offset commit 여부, commit 증가 여부, lag의 지속 증가 여부를 sliding window로 본다. 따라서 crawler에서도 raw_pages lag가 5분 이상 단조 증가하고 fetch 성공률은 정상이라면 Fetcher를 늘리는 것이 아니라 Parser CPU 또는 storage write latency를 먼저 본다. 출처: AWS SQS visibility timeout 문서 https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-visibility-timeout.html, LinkedIn Engineering Burrow 글 https://engineering.linkedin.com/apache-kafka/burrow-kafka-consumer-monitoring-reinvented.
Crawler Trap 방어:
- URL 깊이 제한 (host당 max depth = 5~10).
- URL 길이 제한 (200자 초과 의심).
- 파라미터 개수 제한 (10개 초과 의심).
- 도메인당 fetch 횟수 상한 (1M URL 초과 시 일시 정지).
- 캘린더/페이지네이션 무한 루프: 발견된 URL의 패턴 분석 (
/calendar/2099/12처럼 미래 날짜).
4. 실무에서 어디에 쓰이나
섹션 제목: “4. 실무에서 어디에 쓰이나”- 검색 엔진: Google Crawler (Googlebot), Bing, Yandex. 수천억 URL 규모.
- SEO 도구: Ahrefs, Semrush, Moz. 백링크 그래프 구축.
- 가격 모니터링: 이커머스 경쟁사 가격 추적 (Bright Data, Oxylabs).
- 컴플라이언스 / 데이터셋 수집: Common Crawl (월 단위 공개 덤프), AWS의 ESG 데이터 크롤링 가이드.
- AI 학습 데이터: GPT, Claude, Gemini 사전학습용 웹 코퍼스 (CCNet, RedPajama, FineWeb).
- 사내 통합 검색: 사내 위키, Confluence, 코드 저장소를 크롤해 단일 인덱스로 묶기.
5. 현재 내 업무와 연결점
섹션 제목: “5. 현재 내 업무와 연결점”플랫폼 엔지니어 / BackOps 관점에서 직접 마주칠 가능성이 높은 시나리오:
- 외부 API 데이터 수집 파이프라인: 정확히 크롤러는 아니지만, “정중하게(rate limit 준수), 중복 없이(이전 응답 dedup), 장애에 강하게(재시도 + 멱등성)” 가져오는 패턴은 동일.
- 사내 문서 통합 검색: Confluence, Notion, GitHub, Jira에서 변경된 문서를 주기적으로 가져와 OpenSearch에 인덱싱. incremental crawl 패턴이 그대로 적용된다.
- 로그 수집 / 컴플라이언스 스캐너: 여러 SaaS의 audit log API를 polling. 429 backoff와 incremental cursor 관리가 핵심.
- K8s Operator 작성 시 work queue 패턴: 사실 K8s controller-runtime의 work queue는 URL Frontier의 단순 버전이다. rate limit, retry backoff, dedup이 모두 들어 있다.
- AWS 비용 관점:
- Fetcher 노드는 outbound bandwidth와 NAT Gateway 비용이 가장 큼 (
$0.045/GB). - 큰 규모는 NAT Gateway 대신 NLB + 자체 NAT 또는 IPv6 직접 통신 고려.
- Headless 브라우저 렌더링은 ECS Fargate Spot으로 비용 80% 절감 가능.
- Fetcher 노드는 outbound bandwidth와 NAT Gateway 비용이 가장 큼 (
6. 자주 헷갈리는 개념 비교
섹션 제목: “6. 자주 헷갈리는 개념 비교”| 개념 A | 개념 B | 차이점 |
|---|---|---|
| Crawler | Scraper | Crawler는 링크를 따라 페이지를 수집(URL 발견 자체가 목적). Scraper는 정해진 페이지에서 데이터 추출. |
| BFS Crawl | Priority Crawl | BFS는 가까운 페이지부터 균등하게. Priority는 PageRank·freshness·도메인 중요도로 가중. |
| Bloom Filter | HashSet | Bloom은 false positive 있고 메모리 효율 ~10배. HashSet은 정확하지만 비싸다. |
| robots.txt Disallow | <meta noindex> | Disallow는 fetch 자체를 막음. noindex는 fetch는 하되 색인에서 제외 (이미 fetch했어야 함). |
| URL Seen Test | Content Seen Test | URL Seen은 같은 URL 중복 방지. Content Seen은 다른 URL인데 같은 콘텐츠 (SimHash). |
| Crawl-delay | Mercator 휴리스틱 | Crawl-delay는 정적 (robots.txt에 명시). Mercator는 동적 (직전 응답시간 기반). |
| At-least-once | Exactly-once | Exactly-once는 분산 시스템에서 사실상 불가능 (Two Generals). 항상 at-least-once + 멱등으로 푼다. |
7. 체크리스트
섹션 제목: “7. 체크리스트”- URL Frontier에서 front queue와 back queue가 각각 어떤 제약(우선순위·정중함)을 푸는지 그림으로 설명할 수 있는가?
- robots.txt 규칙(
User-agent,Disallow,Allow,Crawl-delay)을 모두 알고, 우선순위를 설명할 수 있는가? - HTTP 429 응답을 받았을 때 크롤러가 해야 하는 동작 3가지(Retry-After 준수, 도메인 rate 감속, 재시도 한도)를 말할 수 있는가?
- Bloom filter의 false positive가 크롤러에서 어떤 결과(누락 vs 중복)를 만드는지 설명할 수 있는가?
- URL 정규화 규칙(스킴 소문자, fragment 제거, 추적 파라미터 제거, 파라미터 정렬) 4가지 이상을 댈 수 있는가?
- At-least-once delivery 환경에서 멱등성이 왜 필수인지, 멱등성을 깨뜨리는 코드 패턴을 한 가지 들 수 있는가?
- Crawler trap의 대표 5가지(쿼리 파라미터 폭발, 무한 캘린더, 세션 ID URL, 무한 redirect, 동적 검색 결과)를 식별하고 방어 전략을 제시할 수 있는가?
- Fetcher / Parser / Storage를 왜 분리하고 무엇을 큐로 잇는지, 각각의 자원 특성을 설명할 수 있는가?
8. 추가 학습 키워드
섹션 제목: “8. 추가 학습 키워드”- Mercator (1999, Compaq SRC) — 현대 분산 크롤러 아키텍처의 원형 논문
- Heritrix — Internet Archive의 오픈소스 크롤러
- Apache Nutch — Hadoop 기반 분산 크롤러
- Common Crawl — 공개 웹 크롤 덤프, AI 학습용 표준
- Frontera / url-frontier — crawler-commons의 URL Frontier API 표준
- SimHash, MinHash, LSH — near-duplicate detection
- PageRank, HITS — URL 우선순위 알고리즘
- Politeness vs Coverage trade-off — IR 교과서에서 다루는 근본 제약
- robots.txt RFC 9309 (2022) — 30년 만의 공식 표준화
- hiQ Labs v. LinkedIn — 크롤링 합법성에 대한 미국 항소법원 판결 (2022)
- JavaScript rendering at scale — Puppeteer/Playwright 클러스터 운영
- Focused Crawling — 특정 주제만 따라가는 크롤러 (topical crawler)
9. 내가 직접 확인해볼 것
섹션 제목: “9. 내가 직접 확인해볼 것”실습 A: robots.txt 파서 작성
-
https://www.google.com/robots.txt,https://www.naver.com/robots.txt를 받아 보고Disallow규칙 차이를 비교한다. -
Python
urllib.robotparser로 robots.txt를 파싱하고can_fetch(useragent, url)로 5개 URL을 검사해 본다.import urllib.robotparserrp = urllib.robotparser.RobotFileParser()rp.set_url("https://www.naver.com/robots.txt")rp.read()print(rp.can_fetch("*", "https://www.naver.com/")) # 예상: Trueprint(rp.can_fetch("*", "https://www.naver.com/mail/")) # 예상: Falseprint(rp.crawl_delay("*")) # 예상: None or 정수합격선:
can_fetch결과가 실제 robots.txtDisallow규칙과 일치하면 통과.
실습 B: URL 정규화 함수
-
아래 정규화 함수를 작성해 5개 테스트 케이스를 모두 통과시킨다.
from urllib.parse import urlparse, urlunparse, urlencode, parse_qslimport reTRACKING_PARAMS = {"utm_source","utm_medium","utm_campaign","utm_term","utm_content","fbclid","gclid","mc_eid","ref"}def normalize_url(url: str) -> str:p = urlparse(url.lower())# 기본 포트 제거netloc = re.sub(r":(80|443)$", "", p.netloc)# 추적 파라미터 제거 + 알파벳 정렬qs = sorted((k, v) for k, v in parse_qsl(p.query) if k not in TRACKING_PARAMS)return urlunparse((p.scheme, netloc, p.path.rstrip("/") or "/", "", urlencode(qs), ""))# 테스트assert normalize_url("HTTP://Example.com:80/Path/?utm_source=x&id=42#s") == "http://example.com/path/?id=42"assert normalize_url("https://foo.com/a?z=1&a=2") == "https://foo.com/a?a=2&z=1"assert normalize_url("http://foo.com/page/") == "http://foo.com/page"합격선: 3개 assert 모두 통과.
실습 C: Bloom Filter 메모리 측정
-
pybloom-live로 Bloom filter를 만들고 false positive rate 1%, 0.1%일 때 메모리 사용량을 측정한다.from pybloom_live import BloomFilterimport sysfor fp_rate in [0.01, 0.001]:bf = BloomFilter(capacity=1_000_000, error_rate=fp_rate)size_mb = sys.getsizeof(bf.bitarray) / (1024**2)print(f"FP={fp_rate:.1%}: {size_mb:.1f} MB")# 예상 출력: FP=1.0%: ~1.1 MB, FP=0.1%: ~2.2 MB (1M 항목 기준)합격선: FP 0.1%는 FP 1% 대비 메모리가 약 2배임을 수치로 확인.
실습 D: 단일 머신 크롤러 (100줄 이내)
- 도메인당 1초 간격, 깊이 3, URL 정규화, 중복 제거를 만족하는 크롤러를 작성하고 자기 블로그/사내 사이트(외부 사이트 절대 금지)에서 실행한다.
- 합격선: 100개 URL 크롤 후 중복 없음, robots.txt 거부 URL 0건 fetch.
실습 E: 논문 재현
- Mercator 논문(
https://www.cs.cornell.edu/courses/cs685/2002fa/mercator.pdf)의 Figure 1을 보고 front/back queue + heap 구조를 직접 그림으로 재현한다. - 합격선: front queue N개, back queue M개, heap이 보이며 URL 흐름을 화살표로 설명 가능.
실습 F: AWS 비용 계산
- Fetcher 1대(t3.small, 10Mbps 지속) + Parser 2대 + Redis 기반 Frontier 구성으로 1만 URL 크롤을 시뮬레이션한다. NAT Gateway 아웃바운드 비용을 계산한다.
- 합격선: 페이지 평균 100KB 기준 → 1만 URL = 1GB outbound → NAT Gateway 비용 약 $0.045. 계산 과정을 직접 도출할 수 있으면 통과.
실습 G: Common Crawl 집계 (선택)
- Common Crawl의 WARC 파일 1개를 다운받아
warcio라이브러리로 페이지 수, 평균 응답 크기, 상위 10개 도메인을 집계한다. - 합격선: 집계 결과 출력 + “왜 특정 도메인이 상위에 오는가”를 설명할 수 있음.
10. 5줄 요약
섹션 제목: “10. 5줄 요약”- URL Frontier는 우선순위(front queue) 와 정중함(host당 back queue + heap) 두 제약을 이중 큐로 분리해 동시에 만족시킨다 (Mercator 패턴).
- Politeness는 robots.txt 명시 규칙 + Mercator 휴리스틱(직전 응답시간 ×10) + HTTP 429/503 기반 동적 backoff의 3중 구조다.
- URL 중복 제거는 정규화 → Bloom filter(메모리 1차) → 영구 저장소(정확) 단계로, 콘텐츠 중복은 SimHash로 푼다.
- Fetcher / Parser / Storage는 자원 특성이 달라 큐(Kafka/SQS)로 분리해 독립 스케일링하고, 컴포넌트 간 통신은 항상 at-least-once + 멱등으로 설계한다.
- Crawler trap(쿼리 폭발, 무한 캘린더, 세션 ID, redirect 루프)은 깊이/길이/파라미터 수 제한과 도메인별 상한으로 방어하며, 이는 K8s work queue·외부 API polling·로그 수집 파이프라인에 그대로 전이되는 보편 패턴이다.
최종 수정: 2026-05-19