스터디 노트

Chapter 09

웹 크롤러 설계

웹을 BFS로 순회하는 크롤러. 예의성·신선도·함정 세 제약을 URL Frontier·DNS 캐싱·이중 중복 제거·트랩 방어 4부품으로 푼다.

이 챕터의 답

웹 크롤러의 핵심 부품 4가지

웹은 단방향 그래프이고, 크롤러는 그 그래프를 BFS로 순회한다. 단순한 BFS와 다른 점은 세 가지 제약 — 예의성(같은 호스트에 동시 요청 금지), 신선도(이미 본 페이지의 재방문), 함정(spider trap·중복 콘텐츠). 이 제약 때문에 큐 구조와 중복 검사가 별도 부품으로 분리된다.

  1. URL Frontier — Front queue(우선순위) + Back queue(호스트별 예의성)의 2단 큐.
  2. HTML Downloader + DNS — DNS 캐싱과 지역성으로 병목 제거, robots.txt 준수.
  3. 이중 중복 제거 — URL Seen?(Bloom filter) + Content Seen?(해시).
  4. 함정 방어 — URL 길이 제한·도메인 화이트리스트로 spider trap 차단.

1. 문제 이해 및 설계 범위 확정

웹 크롤러 설계는 부품 자체보다 제약 조건에서 도출된다. 어떤 용도로 쓰이느냐에 따라 우선순위·신선도 정책이 달라지므로, 용도부터 짚는다.

크롤러는 어디에 쓰는가

  • 검색 엔진 인덱싱 — Google·Bing 같은 검색 엔진이 사용자 쿼리에 매칭할 페이지를 미리 모아 인덱스를 구축하기 위해 웹을 정기적으로 크롤한다. 새로운 페이지 발견과 기존 페이지의 변경 감지 둘 다 필요(Googlebot이 대표적).
  • 웹 아카이빙 — 사이트의 특정 시점 스냅샷을 저장해 원본이 사라진 뒤에도 그 시점의 모습을 다시 볼 수 있게 한다. 학술·역사·법적 증거 보존 용도(Internet Archive의 Wayback Machine이 대표 예).
  • 웹 마이닝 — 공개된 웹 데이터를 자동 수집해 분석에 쓴다. 가격 비교, 뉴스·SNS 트렌드 분석, 학술 논문 메타데이터 수집, 금융 시장 데이터 집계 같이 “많은 페이지에서 구조화된 정보를 뽑는” 모든 작업이 여기 포함된다.
  • 웹 모니터링 — 특정 콘텐츠나 브랜드명이 무단으로 게시되는지 정기적으로 감시한다. 저작권·상표권 침해, 위조 상품 판매, 유출된 내부 문서 검색 등에 쓰이며, 매칭이 발견되면 알림·법무 조치로 연결된다.

면접에서 확정할 질문

  • 크롤러의 주 용도(검색 인덱싱? 아카이빙?) → 우선순위·신선도 정책이 달라진다
  • 매월 페이지 수 / 페이지 평균 크기 → 저장·대역폭 추정
  • 새로 추가된 페이지·변경된 페이지도 다뤄야 하는가?
  • 대상은 HTML만? 이미지·PDF·동적 JS 페이지도?
  • 저장 기간 (며칠? 몇 년?)
  • 중복 콘텐츠는 어떻게 처리?

이 챕터의 표준 요구사항

항목계산
월간 다운로드 페이지10억 (1B)전제
초당 평균 QPS≈ 40010⁹ ÷ (30 × 86,400)
최대 QPS (×2)≈ 800피크 흡수 여유
평균 페이지 크기500 KB전제
월간 저장 용량≈ 500 TB10⁹ × 500KB
5년 누적 저장≈ 30 PB500TB × 60개월

→ 단일 노드는 물론 단일 데이터센터로도 빠듯한 규모다. 분산 처리, 객체 저장소(S3 등), 지역성이 필요하다.

훌륭한 크롤러의 5가지 속성

속성의미설계에 미치는 영향
규모 확장성수십억 페이지를 다룬다분산 크롤·병렬 다운로더
안정성악성 페이지·서버 오류·spider trap 견딤예외 처리·재시도·격리
예의성같은 호스트에 단시간 다중 요청 금지호스트별 큐·요청 간 delay
확장성새 콘텐츠 타입(이미지·PDF) 추가 용이파이프라인을 모듈 단위로
신선도이미 본 페이지도 변경되면 재방문재방문 스케줄러 + 변경 감지

이 5가지는 서로 충돌한다 — 예의성을 지키면 규모가 느리고, 신선도를 높이면 대역폭이 부족하다. “모두 최대”가 아니라 호스트별 적정 속도를 찾는 큐 구조가 답이 된다.

2. 고수준 설계

크롤러의 본질은 그래프 순회다. 페이지가 노드, 하이퍼링크가 단방향 엣지. 평범한 BFS와 다른 점은 “이미 본 노드 체크”에 예의성·신선도·중복까지 얹혀 있다는 것.

기본 알고리즘 — BFS

function crawl(seedURLs):
frontier ← Queue()
frontier.enqueueAll(seedURLs)
while frontier not empty:
url ← frontier.dequeue()
html ← download(url)
if contentNotSeen(html):
store(html)
links ← extract(html)
for link in links:
if urlNotSeen(link) and passesFilter(link):
frontier.enqueue(link)

frontier는 단순 큐가 아니라 예의성·우선순위·신선도를 품은 특별한 큐(URL Frontier). contentNotSeenurlNotSeen도 별도 부품으로 분리된다.

9개 부품으로 분해

Seed URLs

시작점. 도메인별/주제별로 선택해 그래프 도달 범위 최대화

URL Frontier

다음에 뭘 가져올지 결정. 예의성+우선순위+신선도를 결정하는 정책 엔진

HTML Downloader

Frontier에서 URL 꺼내 HTML 가져옴. DNS 조회·robots.txt 확인

DNS Resolver

도메인 → IP 변환. 동기 호출이라 캐싱 필수 — 종종 병목

Content Parser

HTML 검증·파싱. 잘못된 페이지 걸러내 스토리지 낭비 방지

Content Seen?

이미 저장된 콘텐츠와 동일? 해시(또는 SimHash) 비교

Content Storage

HTML 본문 영구 저장. 30PB 규모이므로 객체 저장소(S3 등)

URL Extractor

HTML에서 <a href> 링크 추출. 상대 경로는 절대 경로로 변환

URL Filter + URL Seen?

확장자/도메인 필터 + Bloom filter로 중복 URL 제거

색이 같은 부품끼리 한 묶음 — 파랑=시드, 노랑=다운로드, 보라=파싱·저장, 초록=링크 회수. 이어지는 섹션 3~8에서 각각 자세히 다룬다.

한눈에 보는 데이터 흐름

Seed URLs
URL Frontier (예의성 + 우선순위 + 신선도)
HTML Downloader ← DNS Resolver
Content Parser → Content Seen? → Content Storage
URL Extractor → URL Filter → URL Seen?
↺ Frontier에 다시 enqueue

추출한 새 URL이 다시 Frontier로 돌아가는 피드백 루프가 크롤러의 핵심 구조. 중복 제거와 함정 방어가 없으면 같은 곳을 영원히 돌게 된다.

DFS는 왜 안 쓰나

웹 그래프는 깊이가 무한대일 수 있다(동적 URL, 캘린더 페이지 등). DFS는 한 호스트의 깊은 곳에 갇혀서 다른 도메인을 못 본다. BFS는 가까운 깊이부터 넓게 훑어 커버리지·예의성·중단/재시작 모두 다루기 쉽다.

3. URL 저장/방문 관리 (URL Frontier)

Frontier는 “다음에 어떤 URL을 가져올지”를 결정하는 부품이다. 평범한 큐와 달리 우선순위·신선도·대용량 저장 세 가지 요구를 동시에 만족해야 한다. (예의성 큐 구조는 6번 ‘예의’ 섹션에서 robots.txt와 함께 다룬다.)

우선순위 — 모든 페이지가 동등하지 않다

크롤할 페이지가 수십억이라면 어느 페이지를 먼저 볼지 결정해야 한다. 판단 신호:

  • PageRank — 인링크가 많은 페이지 우선
  • 트래픽 양 — 사람들이 많이 보는 페이지 우선
  • 갱신 빈도 — 자주 바뀌는 페이지 우선 (뉴스 vs 위키)

Prioritizer가 위 신호들을 점수화해 우선순위 등급별 Front queue로 분배한다. 점수가 높은 큐는 자주, 낮은 큐는 가끔 dequeue된다.

Front queue (우선순위 큐)
우선순위 등급별 큐 + 선택자
f1 → high (PageRank 高)
f2 → medium
f3 → low

Selector가 가중치에 따라 큐를 골라 다음 단계(예의성 큐)로 라우팅

신선도 — 이미 본 페이지도 재방문

웹은 끊임없이 바뀐다. 한 번 인덱싱했다고 끝이 아니다. 재크롤링 정책은 보통:

  • 변경 히스토리에 기반한 적응형 스케줄
  • 중요도(우선순위)와 동일 축으로 재방문 빈도 결정
  • 변경 안 된 페이지는 점점 간격 늘리기 (지수적 백오프)

Frontier 저장소 — 디스크 + 메모리 하이브리드

수억~수십억 개 URL이 큐에 쌓이면 메모리에 다 못 담는다. 일반적인 패턴은:

  • 대부분의 URL은 디스크에 직렬화해 보관
  • 큐의 enqueue/dequeue 양끝만 메모리 버퍼
  • 주기적으로 디스크 ↔ 메모리 교체

이렇게 하면 큐 크기는 거의 무제한, 처리 속도는 거의 메모리 큐 수준.

4. HTML 다운로드 및 파싱

Frontier에서 받은 URL을 실제 HTML로 가져오고, 받아 온 HTML을 검증·파싱해 이후 단계로 넘기는 부품들. DNS 조회가 거의 항상 병목이고, 분산·지역성·짧은 타임아웃이 처리량을 좌우한다.

DNS — 동기 호출이라 거의 항상 병목

호스트명을 IP로 바꾸는 DNS 조회는 보통 10ms~수백 ms 블로킹이다. 초당 수백 페이지를 받으려면 DNS만으로도 한계가 온다. 대응책:

  • DNS 캐시 — 호스트→IP 매핑을 메모리에 보관 (TTL 며칠)
  • 비동기 DNS — getaddrinfo는 동기이므로 직접 비동기 리졸버 사용
  • 주기적 갱신 — TTL 만료된 항목은 백그라운드 cron으로

Content Parser — 받은 HTML 검증·파싱

다운로드된 HTML이 항상 정상은 아니다. 파서 단계에서 다음을 처리한다.

  • 인코딩·MIME·크기 검사 — 깨진 파일·HTML 아닌 응답을 걸러 저장소 낭비 방지
  • DOM 파싱 — 본문·메타 태그·링크 추출용 트리 구축
  • 본문 추출(boilerplate removal) — 광고·내비게이션 제거 (상세 설계 8번에서 더)

분산 + 지역성

한 노드 한 데이터센터로는 30PB 트래픽을 못 받는다. 크롤러를 여러 서버에 분산하고, 각 서버는 자기 우선 영역(URL 공간)을 담당.

  • 지리적 지역성 — 일본 사이트는 일본 데이터센터에서 크롤(왕복 지연 ↓)
  • 짧은 타임아웃 — 응답 안 오는 페이지는 빨리 포기해 워커 점유 시간 단축

5. 중복 URL 제거 (URL Seen + Content Seen)

BFS의 “이미 방문한 노드 체크”에 대응하는 부품. 그런데 웹에서는 한 번이 아니라 두 번 중복을 검사한다.

왜 두 번인가

검사언제왜 필요
URL Seen?새 URL을 Frontier에 넣기 직전같은 URL을 두 번 다운로드하지 않기 위해 (대역폭 절약)
Content Seen?HTML을 받은 직후, 저장하기 전다른 URL이지만 같은 내용인 경우 저장 중복 방지 (29% 중복 보고)

웹의 약 29%가 중복 콘텐츠(미러 사이트·복붙 블로그·세션 ID 차이만 있는 URL 등)다. URL만 보고는 막을 수 없어 콘텐츠 비교까지 한다.

URL Seen? — Bloom filter

수십억 URL을 일일이 해시 테이블로 관리하면 메모리가 모자란다. Bloom filter는 “있을지도 모름 / 절대 없음”만 답하는 확률적 자료구조. 크롤러가 원하는 답은 후자(“없음” = 처음 본 URL = 큐에 넣음)다.

장점
  • 메모리 효율 — URL당 ~10비트
  • O(1) 조회·삽입
  • 분산 환경에서 직렬화·전파 쉬움
비용
  • 위양성(false positive) 가능 — 새 URL인데 “이미 봤다”고 판단할 확률 ε
  • 위음성은 없음 — 정말 본 URL을 “안 봤다”고 하진 않는다
  • 삭제 불가 (counting Bloom 등 변형으로 보완)

크롤러는 일부 URL을 놓치는 비용이 작은 편이라(웹은 어차피 무한대) 위양성을 감수한다. ε ≈ 1%여도 잃는 페이지는 1%뿐이고, 그만큼 메모리·속도를 번다.

Content Seen? — 해시 비교

HTML 본문에서 화이트스페이스·세션 토큰을 정규화한 뒤 MD5/SHA-1 해시로 키를 만든다. 키만 비교하면 페이지 전체를 비교하지 않아도 같음/다름 판정이 빠르다.

  • 완전히 같지는 않지만 거의 같은 페이지(템플릿 같음, 본문만 조금 다름)는 SimHashMinHash 같은 근접 해시로
  • 저장 절약 효과가 큼 — 30PB의 29%면 ~9PB 절약

저장소 — 객체 저장소 + 인덱스 DB

  • HTML 본문 — S3 같은 객체 저장소(저렴·무제한·다중 AZ)
  • 메타데이터·URL 인덱스 — 관계형/KV DB
  • 자주 쓰는 페이지 — 메모리 캐시·CDN으로 끌어올림

6. 예의 (robots.txt, rate limiting)

크롤러가 사이트에 미치는 부담을 줄이는 두 메커니즘. robots.txt는 사이트의 명시적 거부를 따르는 것, 예의성(politeness)은 허용된 경로라도 트래픽 모양을 조절해 서버에 부담을 안 주는 자율적 절제다.

로봇 제외 프로토콜 (robots.txt)

모든 사이트가 자신을 마음대로 크롤하라고 허락한 건 아니다. robots.txt는 사이트 루트에 놓인 텍스트 파일로, 어떤 경로를 누가 가져갈 수 있는가를 명시한다.

# amazon.com/robots.txt
User-agent: Googlebot
Disallow: /creatorhub/*
Disallow: /rss/people/*/reviews
Disallow: /gp/pdp/rss/*/reviews
Disallow: /gp/cdp/member-reviews/
  • 매 요청마다 robots.txt를 다시 받으면 비효율 → 호스트별 캐시(TTL: 수 시간~수 일)
  • 금지된 경로는 Frontier에 넣기 전에 필터링
  • 크롤러의 User-Agent를 명확히 표기하는 게 예의

예의성(politeness) — 호스트당 1 워커

여러 스레드가 동시에 동일 호스트에 요청을 쏘면 사실상 DoS가 된다. 크롤러는 호스트당 한 다운로드 스레드를 유지하고, 요청 사이에 적정 delay를 둔다. 구현은 큐를 둘로 나눠서 한다.

Front queue (우선순위 큐)
3번에서 다룬 우선순위 등급별 큐

Selector가 가중치에 따라 큐를 골라 Back queue로 라우팅

Back queue (예의성 큐)
호스트당 1개 큐 + 호스트 매핑 테이블
b1 → wikipedia.org
b2 → nytimes.com
b3 → github.com

Worker 스레드 w_k는 고정 큐에서만 URL을 꺼낸다 → 호스트당 동시 요청 1개로 자동 제한

Front queue가 “무엇이 중요한가”를, Back queue가 “언제·어디로 쏠 수 있는가”를 푼다.
robots.txt vs 예의성

robots.txt는 ‘여기 들어오지 마’라는 사이트의 명시적 거부에 따르는 것. 예의성은 허용된 경로라도 초당 100번 때리지 말자는 자율적 절제(트래픽 모양). 둘 다 ‘좋은 시민’의 요소지만 결이 다르다.

7. 장애 대응 및 확장성

분산 환경에서 노드는 언제든 죽고, 외부 사이트는 언제든 5xx를 뱉는다. 이 단계는 “죽어도 잃지 않고, 늘어도 쉽게 늘릴 수 있게” 만드는 부분.

분산 + 일관 해싱 (5장 연결)

  • 크롤러 노드 풀에 URL을 분배할 때 일관 해싱으로 노드 추가/제거 시 재배치 최소화
  • 노드별로 “내 호스트 책임 범위”를 두면 예의성 큐 관리도 자연스럽게 분산

크롤 상태 저장과 재시작

  • Frontier·URL Seen·진행 중인 페이지 목록을 주기적으로 체크포인트
  • 장애로 노드가 죽어도 마지막 체크포인트에서 재개 가능

예외·재시도 정책

  • 5xx 오류 → 지수 백오프로 재시도
  • 4xx 오류 → URL 영구 제외 또는 별도 큐로 격리
  • 타임아웃 → 짧게 설정해 워커 점유 시간 단축

데이터 검증

  • 받은 HTML의 인코딩·크기·MIME 검사 — 깨진 파일이 저장소로 흘러가지 않게
  • 악성 페이로드 차단(보안 필터)

가용성·일관성

  • 다운로더·파서 모두 stateless → 로드밸런서 뒤에서 수평 확장
  • 큐·저장소는 다중 AZ 복제
  • URL 인덱스·중복 체크는 강한 일관성, 콘텐츠 저장은 결과적 일관성으로 충분

8. 상세 설계와 최적화 포인트

기본 부품 외에 운영 중 실제로 발목을 잡는 함정들과, 시스템을 확장할 때 따라오는 주제들.

Spider trap — 무한 깊이 URL

크롤러를 무한 루프에 빠뜨리려고 만들어진(또는 우연히 그렇게 된) 페이지. 전형적인 예:

https://example.com/foo/bar/foo/bar/foo/bar/…
https://example.com/calendar/2026/05/24?next=2026/05/25
https://example.com/?session=abc123 (세션 ID 무한 변형)

대응:

  • URL 길이 제한 — 일정 길이 넘으면 자동 거부 (간단·효과적)
  • 도메인별 페이지 수 상한
  • 의심 도메인 수동 차단 — 자동 탐지 + 운영자 검수

데이터 노이즈 — 광고·스팸·스크립트 본문

페이지 안에는 의미 없는 광고·스크립트·내비게이션이 절반을 넘는다. 파서 단계에서 본문 추출(boilerplate removal)· 스팸 분류기를 거쳐 가치 없는 데이터를 일찍 버린다.

동적 페이지 (SSR vs 클라이언트 렌더링)

현대 웹은 JavaScript로 렌더링되는 페이지가 많아 HTML만 받아서는 본문이 비어 있다. 옵션:

  • Headless 브라우저(Chromium 등)로 JS 실행 — 정확하지만 10배 비쌈
  • 사이트가 제공한 SSR/사전 렌더 페이지 활용
  • 중요도 기반 선택 — 모든 페이지에 헤드리스를 쓰지 말고, 우선순위 큐에서 필요한 페이지만 JS 렌더링

데이터 분석

  • 처리량·실패율·호스트별 통계 → 우선순위·예의성 정책 자동 조정
  • 저장된 페이지로 검색 인덱스 빌드·머신러닝 학습 데이터 제공

확장 — 추가 콘텐츠 타입

  • 이미지·PDF·동영상 다운로더를 별도 모듈로 추가 — 파이프라인이 모듈형이라 붙이기 쉬움
  • 새 컨텐츠 타입마다 Parser·Storage·Seen? 변종을 더한다