728x90

장애 대응 중에 구글에서 에러 메시지 검색했는데, 상위 3개가 전부 "이 기사를 계속 읽으려면 구독하세요"였던 경험. 다들 한 번쯤 있을 거다. 나는 새벽 3시에 Kafka 컨슈머 랙 관련 아티클 찾다가 미디엄 페이월에 세 번 연속 막히고 나서 그냥 소스 코드를 읽기 시작했다.

2026년 8월 21일 Kagi 체인지로그에 짧게 한 줄이 올라왔다. "we've added a setting for removing paywalled links from search results automatically." Stocks 위젯 개편 소식에 딸려 나온 부록 같은 문장인데, HN에서 반응이 꽤 컸다. 왜 이 한 줄이 검색 품질 논의의 핵심인지, 그리고 인프라 하는 사람 입장에서 뭘 배울 수 있는지 정리해본다.

왜 지금 이게 화제인가

검색 결과에 페이월이 섞이는 건 단순한 불편이 아니라 정보 비용의 문제다. 구독 안 한 사이트가 상위에 뜨면, 사용자는 클릭 → 로딩 → 오버레이 확인 → 뒤로가기라는 4단계를 낭비한다. 트러블슈팅 중이면 이 왕복이 체감상 훨씬 크다.

그런데 왜 구글은 이걸 안 해줄까. 구조적인 이유가 있다.

  • 광고 기반 검색엔진은 퍼블리셔가 곧 생태계 파트너다. 유료 언론사 링크를 자동으로 빼는 건 그 관계를 정면으로 건드린다.
  • 구글은 오히려 반대 방향으로 갔다. NewsArticle 스키마의 isAccessibleForFree 필드는 페이월 콘텐츠를 배제하기 위한 게 아니라, 클로킹으로 오해받지 않게 정상 색인시키기 위한 장치에 가깝다.
  • Kagi는 구독료로 굴러간다. 사용자가 유일한 고객이니까 "이거 빼주세요"를 그냥 들어줄 수 있다. 인센티브 구조가 다르다.

이건 기술 난이도의 문제가 아니라 비즈니스 모델의 문제였다는 뜻이다. Kagi가 AI 기능 전체 off 토글(settings/ai)을 넣은 것도 같은 맥락이다. 체인지로그에서 직접 "we want to stay true to putting you in control of your search engine"이라고 썼다.

페이월을 어떻게 판별하나 — 동작 원리

Kagi가 정확히 어떤 방식을 쓰는지는 공식적으로 공개된 게 없다. 체인지로그 한 줄이 전부다. 다만 이 문제를 푸는 방법은 업계적으로 몇 가지로 수렴한다. 내가 사내 문서 크롤러 만들 때 겪은 것과 겹치는 부분이 많다.

1) 구조화 데이터 (가장 싸고 가장 안 믿음직함)

schema.org의 isAccessibleForFree: false. 퍼블리셔가 스스로 붙이는 값이라 신뢰도는 자진신고 수준이다.

curl -s https://example-news.com/article/123 \
  | grep -o '"isAccessibleForFree":[^,}]*'

# 출력 예시
"isAccessibleForFree":false

붙어 있으면 확실하지만, 안 붙어 있다고 무료라는 보장이 전혀 없다. 재현율(recall)이 형편없다.

2) 도메인 리스트 (실용적, 유지보수 지옥)

NYT, WSJ, Bloomberg, Medium... 알려진 페이월 도메인을 리스트로 관리한다. 단순하지만 두 가지가 깨진다. 하나는 같은 도메인 안에서 무료 기사와 유료 기사가 섞이는 경우(NYT 오피니언 일부, Medium의 non-member 스토리), 다른 하나는 미터링 페이월이다. "이번 달 3개까지 무료"는 서버가 아니라 브라우저 로컬스토리지 상태에 달려 있어서 크롤러 입장에서 판정 자체가 불가능하다.

3) 콘텐츠 시그널 (제일 정확, 제일 비쌈)

본문 텍스트에서 패턴을 잡는다. 실제로 돌려볼 수 있는 최소 버전:

#!/usr/bin/env bash
# paywall-probe.sh — 페이월 의심 시그널 간이 탐지
URL="$1"
BODY=$(curl -sL -A "Mozilla/5.0" --max-time 10 "$URL")

echo "== body length: $(echo "$BODY" | wc -c) bytes"
echo "$BODY" | grep -o '"isAccessibleForFree":[^,}]*' | head -1
echo "$BODY" | grep -io -E 'subscribe to (continue|read)|구독하고 계속|paywall|메타 회원 전용' \
  | sort -u | head -5
$ ./paywall-probe.sh https://some-news.example/a/1
== body length: 48213 bytes
"isAccessibleForFree":false
Subscribe to continue
paywall

실제 검색엔진 규모에서는 이걸 색인 시점에 미리 계산해서 플래그로 저장해둔다. 쿼리 시점에 페이지를 다시 긁을 수는 없으니까. 그래서 이 기능은 "필터"라기보다 "이미 있던 메타데이터를 노출하는 토글"에 가깝다고 보는 게 맞다. Kagi는 이전부터 검색 결과에 페이월 배지를 붙여왔는데, 그 판정 결과를 재활용해 아예 결과에서 빼는 스위치를 추가한 것으로 보인다.

비유하자면

Nginx의 deny 리스트를 쿼리마다 실시간 평가하느냐, 아니면 빌드 타임에 map 파일로 구워두느냐의 차이다. 후자가 검색엔진이 택할 수밖에 없는 방식이고, 대신 플래그가 낡을 수 있다는 부작용이 따라온다. 어제까지 무료였던 기사가 오늘 유료로 바뀌어도 다음 크롤까지는 그대로다. 반대도 마찬가지고.

실무 관점 — 도입 판단과 흔한 함정

켜기 전에 생각할 것

나는 켜놓고 쓰는 쪽인데, 무조건 추천하진 않는다. 정리하면 이렇다.

  • 켜면 좋은 경우: 기술 트러블슈팅, 문서 검색, 스택오버플로/깃허브/공식 docs 위주 탐색. 페이월 소스가 거의 없는 영역이라 부작용이 적다.
  • 끄는 게 나은 경우: 특정 사건의 1차 보도를 찾을 때. 국내외 주요 언론 단독 기사는 대부분 유료다. 필터를 켜면 원본이 사라지고 그걸 받아쓴 어그리게이터만 남는다. 이게 제일 무섭다. 신뢰도 낮은 재탕 소스로 결과가 채워지는 역효과.
  • 구독 중인 사이트가 있으면: 필터는 내 구독 상태를 모른다. WSJ 구독 중인데 WSJ이 통째로 사라지는 상황이 생긴다. 이럴 땐 전역 필터 대신 Kagi의 도메인별 랭킹 조정(Raise/Lower/Block)을 쓰는 게 낫다.

흔한 함정 1: 필터를 켜면 결과가 아예 안 나온다

페이월 필터 + 다른 필터(렌즈, 블록 도메인)를 겹치면 결과 0건이 뜬다. 특히 좁은 쿼리에서 자주 본다. 게다가 체인지로그를 보면 관련 버그가 실제로 있었다.

Blocked domains are used as sources in Quick Answer  #11257 @bausauce

차단 도메인이 일반 결과에선 빠지는데 Quick Answer의 소스로는 들어가던 이슈다. 필터링 계층이 여러 군데(랭킹, 결과 렌더링, AI 요약 파이프라인)에 흩어져 있으면 이런 누락이 반드시 생긴다. 우리가 만드는 시스템도 똑같다. WAF에서 막았는데 내부 서비스 메시 경유 트래픽은 안 막히는 것과 구조적으로 동일한 실수다.

흔한 함정 2: 크롤러 관점에서 페이월 판정 자체가 실패

직접 파이프라인을 만들어본 사람이면 이 에러를 만난다. Kagi도 체인지로그에 같은 문구를 버그로 올려놨다.

Upstream connect error or disconnect/reset before headers. reset reason: connection termination  #10680 @TheToby

Envoy 기반 프록시에서 나오는 전형적인 메시지다. 크롤러가 타임아웃/리셋을 맞으면 그 페이지는 "본문 없음"으로 처리되고, 본문 길이가 짧다는 이유로 페이월로 오판될 수 있다. 실제로 같은 체인지로그에 이런 것도 있다.

Extract API returns empty data for an entire batch when one page times out  #11176 @fredcy

배치 하나가 타임아웃 나면 배치 전체가 빈 데이터로 돌아오는 버그. 이게 페이월 판정 파이프라인에 물려 있으면 무고한 도메인 수백 개가 한꺼번에 페이월로 낙인찍힌다. 교훈은 명확하다. "데이터 없음"과 "차단됨"을 절대 같은 값으로 취급하지 말 것. 3-state로 가야 한다.

# 나쁜 설계
paywalled = (len(body_text) < 500)

# 최소한 이렇게
if fetch_status != 200:
    state = "UNKNOWN"      # 필터에서 제외하지 않음
elif has_paywall_signal(body):
    state = "PAYWALLED"
else:
    state = "FREE"

운영에서 이 원칙을 어겨서 사고 난 적이 있다. 헬스체크 엔드포인트가 타임아웃 났는데 그걸 "unhealthy"로 처리해서 정상 파드를 전부 내려버린 케이스. 도메인은 다르지만 실수의 모양은 똑같다.

함정 3: 필터가 켜졌는지 본인이 잊는다

제일 흔하고 제일 짜증난다. "왜 이 검색어에 결과가 없지?" 하고 30분 삽질했는데 알고 보니 몇 달 전에 켜둔 토글 때문. 이건 Kagi만의 문제가 아니라 모든 사용자 설정형 필터의 숙명이다. 대응책은 하나다. 계정 설정을 주기적으로 덤프해서 어딘가에 기록해두는 것.

# 필터 없는 결과를 확인할 때는 렌즈를 명시적으로 지정
# (2026-08-21 체인지로그: 렌즈 URL이 숫자 → 이름으로 바뀜)
https://kagi.com/search?q=kafka+consumer+lag&lens=forums

# 포럼 렌즈는 애초에 페이월 소스가 거의 없다 →
# 페이월 필터를 전역으로 켜는 대신 렌즈로 좁히는 게 부작용이 적다

개인적으로는 이 방식을 더 선호한다. 전역 토글은 "빼기"고, 렌즈는 "고르기"다. 빼기는 뭐가 빠졌는지 안 보이지만 고르기는 내가 뭘 골랐는지 URL에 남는다. 디버깅 가능성 측면에서 후자가 훨씬 낫다.

대안 정리

  • 브라우저 확장: uBlock 필터로 오버레이만 제거. 검색 결과는 그대로라 정보 손실이 없지만, 서버사이드 페이월(본문 자체를 안 내려주는 경우)엔 무력하다.
  • archive.today / Wayback: 링크는 보이되 우회. 저작권 이슈는 각자 판단.
  • Kagi 도메인 랭킹 조정: 완전 배제 대신 순위만 낮추기. 가장 균형 잡힌 선택이라고 본다.
  • SearXNG 자체 호스팅: 필터 로직을 직접 짜고 싶으면 이쪽. 대신 결과 품질과 레이트리밋 관리가 온전히 내 몫이 된다.

정리

한 줄 요약: 페이월 필터는 기술적 돌파구가 아니라 "사용자가 고객인 검색엔진"만 만들 수 있는 기능이다.

  • 기술 검색 위주인 사람 → 켜라. 부작용 거의 없고 체감 개선이 크다.
  • 뉴스·리서치를 검색으로 하는 사람 → 켜지 마라. 1차 소스가 통째로 사라지고 재탕 기사만 남는다.
  • 언론사 구독 중 → 전역 토글 대신 도메인별 랭킹 조정으로.
  • 비슷한 필터를 직접 만들 사람 → 판정 실패와 차단을 분리해라. UNKNOWN 상태 없이 만들면 크롤 실패가 그대로 오탐이 된다. 이게 이 글에서 가져갈 유일한 설계 교훈이다.

덧붙이면, Kagi 체인지로그는 사용자 피드백 번호(#11296 같은)를 항목마다 달아둔다. 요청 → 이슈 번호 → 릴리즈 노트가 한 줄로 이어지는 구조인데, 사내 플랫폼팀 운영해본 사람이면 이게 얼마나 유지하기 어려운지 안다. 기능보다 이 운영 방식이 더 인상적이었다.

참고 자료

728x90
728x90

1. 지금 로그에서 실제로 벌어지는 일

Hacker News에 올라온 "A year of fighting scrapers on my 1.5 million-page website" 글의 제목은 사실 부제가 더 정확하다. 내 사이트 트래픽의 99%가 봇. 처음 이 문장을 보면 과장 같지만, 페이지 수가 많고 SEO로 롱테일 유입이 잡히는 사이트를 운영해본 사람은 대부분 고개를 끄덕인다.

내가 겪은 패턴은 이랬다. DB 사이즈 대비 페이지가 많은(카테고리 × 필터 × 페이지네이션 조합으로 URL이 기하급수로 늘어나는) 서비스에서, 어느 날부터 origin CPU가 평소의 3배로 붙어 있었다. APM에는 특정 느린 쿼리가 하나 튀는 것도 아니고, 그냥 전체적으로 골고루 요청이 늘어났다. 캐시 히트율은 오히려 떨어졌다. 이게 핵심 신호다.

사람 트래픽은 인기 페이지에 몰린다. 상위 1% URL이 트래픽의 대부분을 먹는다. 그래서 CDN 캐시가 먹힌다. 반면 스크래퍼는 사이트맵을 따라 전 페이지를 균등하게 순회한다. 150만 페이지를 다 긁으면 캐시 히트율이 바닥을 치고, 모든 요청이 origin으로 내려간다. "트래픽은 2배인데 DB 부하는 8배"가 되는 이유가 여기 있다.

가장 먼저 할 일은 남 얘기 그만 듣고 자기 로그를 세는 것이다.

# Nginx access log에서 UA별 요청 수 Top 10
awk -F'"' '{print $6}' /var/log/nginx/access.log \
  | sed 's/.*\(GPTBot\|ClaudeBot\|Bytespider\|Amazonbot\|bingbot\|Googlebot\).*/\1/' \
  | sort | uniq -c | sort -rn | head

  482119 Bytespider
  203847 GPTBot
   91204 ClaudeBot
   77650 Amazonbot
   41220 bingbot
   38904 Googlebot

여기서 이름을 대놓고 밝히는 놈들은 그래도 양심적인 편이다. 진짜 문제는 아래 명령에서 드러난다. UA가 평범한 Chrome인데 한 IP에서 수만 건이 나오는 경우.

# /24 단위로 묶어서 요청 수 집계 (분산 스크래퍼 탐지)
awk '{print $1}' /var/log/nginx/access.log \
  | cut -d. -f1-3 | sort | uniq -c | sort -rn | head -5

   58210 47.128.23
   41773 8.219.104
   19022 143.198.77
    9871 172.176.9
    3204 121.134.55

앞의 두 개는 클라우드 사업자 대역이었고, UA는 전부 최신 Chrome이었다. 사람이 클라우드 IP에서 크롬으로 5만 페이지를 볼 일은 없다.

2. robots.txt와 User-Agent 차단이 무너지는 지점

robots.txt는 규약이지 강제가 아니다. 이건 원래 그렇게 설계된 것이다. RFC 9309(Robots Exclusion Protocol)도 준수를 "권고"한다. 즉 robots.txt는 "예의를 아는 크롤러에게 부하를 줄여달라고 부탁하는 문서"이고, 예의 없는 쪽에는 어디를 긁으면 알찬지 알려주는 지도가 된다.

그래도 넣어라. 공개적으로 선언된 AI 크롤러 중 일부는 실제로 준수한다고 명시하고 있다(OpenAI GPTBot, Anthropic ClaudeBot 등이 공식 문서에서 robots.txt 준수를 밝힌다 — 정확한 UA 문자열과 IP 대역은 각 사 공식 문서 확인 필요). 법적/커뮤니케이션 측면에서도 "명시적으로 거부했다"는 기록이 남는다.

# robots.txt — 선언은 하되, 이걸로 끝났다고 믿지 말 것
User-agent: GPTBot
Disallow: /

User-agent: ClaudeBot
Disallow: /

User-agent: Bytespider
Disallow: /

User-agent: *
Crawl-delay: 10
Disallow: /search
Disallow: /*?sort=
Disallow: /*?page=

UA 차단이 무너지는 과정은 거의 정해진 각본이다.

  1. Bytespider UA를 차단한다 → 트래픽 뚝 떨어진다 → 뿌듯하다.
  2. 2주 뒤 같은 ASN에서 UA가 Mozilla/5.0 ... Chrome/121.0.0.0 Safari/537.36으로 바뀐 트래픽이 같은 패턴으로 들어온다.
  3. UA로는 이제 구분이 안 된다. 정상 유저와 문자열이 완전히 동일하다.
  4. IP를 차단한다 → 다음 주엔 주거용 프록시(residential proxy) 대역에서 IP 하나당 20~30건씩 쪼개서 들어온다.

이 지점에서 발상을 바꿔야 한다. "누구냐"로 막는 게임에서 "어떻게 행동하냐"로 막는 게임으로. UA는 클라이언트가 자기 입으로 하는 자기소개다. 거짓말하는 데 비용이 0원이다. 반면 행동 패턴은 스크래퍼의 목적(전 페이지 수집) 자체에서 나오기 때문에 위조하려면 수집 효율을 포기해야 한다. 방어자는 이 비대칭을 이용해야 한다.

3. 탐지·완화 계층 설계와 각 방식의 성격

탐지 신호를 겹쳐 쌓기

단일 신호로 판정하면 무조건 오탐이 난다. 점수를 합산하는 구조로 가야 한다. 내가 쓰는 신호는 대략 이렇다.

  • ASN / IP 대역: 클라우드 사업자(AWS, GCP, Azure, Alibaba, Hetzner, DigitalOcean 등) 대역에서 오는 브라우저 UA 트래픽은 강한 의심 신호. 단, 국내 서비스라면 기업 프록시·모바일 캐리어 NAT·앱 내 웹뷰 크롤링 때문에 무조건 차단은 위험하다.
  • 요청 분포의 균등성: 세션 내 URL이 서로 링크로 연결되지 않은 곳을 무작위로 훑고 있으면 봇. 사람은 목록 → 상세 → 뒤로 → 상세 패턴을 그린다.
  • 부수 리소스 미요청: HTML만 받고 CSS/JS/이미지/폰트를 전혀 안 받는 클라이언트. 브라우저를 자칭하면서 이러면 거짓말이다.
  • 헤더 지문: Accept-Language 부재, Accept*/*, Sec-Fetch-* 헤더 없음, HTTP/1.1 고정(최신 크롬은 h2/h3). TLS JA3/JA4 지문이 UA와 불일치하는 경우도 강한 신호다.
  • 요청 간격의 규칙성: 표준편차가 지나치게 작은 요청 간격. 사람은 이렇게 못 한다.

완화 수단 비교

수단막는 대상비용한계
Rate Limit (IP/세션)단일 IP 고속 크롤러매우 낮음분산 프록시에 무력
캐시 계층 강화부하만 (트래픽은 그대로)낮음, 효과 확실롱테일 페이지 캐시 히트 어려움
PoW 챌린지 (Anubis류)JS 미실행 대량 스크래퍼중간 (UX·SEO 영향)헤드리스 브라우저는 통과
타르핏 / 지연 응답수집 효율 자체커넥션 점유 주의내 리소스도 붙잡힘
CDN Bot Management광범위블랙박스, 오탐 시 디버깅 난이도

여기서 PoW 챌린지가 최근 오픈소스 진영에서 많이 언급된다. Anubis가 대표적이고, GNOME·kernel.org 같은 곳에서 도입했다는 이야기가 커뮤니티에 널리 돌았다(도입 현황과 현재 버전은 프로젝트 공식 문서 확인 필요). 원리는 단순하다.

요청이 오면 HTML 대신 작은 JS를 내려준다. 클라이언트는 조건을 만족하는 해시를 찾을 때까지 브루트포스를 돌린다. 답을 맞히면 쿠키를 발급하고 통과시킨다.

비유하면 입장 전에 팔굽혀펴기 20회다. 사람 한 명에겐 몇백 밀리초 짜증이지만, 150만 페이지를 긁으려는 쪽에는 150만 번의 팔굽혀펴기다. 스크래퍼의 경제성을 깨는 게 목적이지, 완벽히 막는 게 목적이 아니다. 실제로 헤드리스 크롬을 붙인 상대는 PoW를 그냥 푼다. 다만 헤드리스 브라우저는 curl보다 수십 배 비싸므로, 그 자체가 방어다.

Nginx에서 당장 적용 가능한 형태

http {
  # 봇성 신호에 따라 존을 분리
  map $http_user_agent $bot_ua {
    default            0;
    ~*(GPTBot|ClaudeBot|Bytespider|Amazonbot|CCBot)  1;
  }
  map $bot_ua $limit_key {
    1  $binary_remote_addr;   # 자칭 봇: 강하게
    0  "";                    # 나머지는 아래 존에서 처리
  }

  limit_req_zone $limit_key            zone=botzone:10m rate=6r/m;
  limit_req_zone $binary_remote_addr   zone=perip:10m   rate=20r/s;

  server {
    location / {
      limit_req zone=botzone burst=3 nodelay;
      limit_req zone=perip  burst=40;
      limit_req_status 429;
      proxy_pass http://app;
    }
  }
}

중요한 건 limit_req_status 429다. 기본값은 503인데, 503은 "서버 장애"로 해석돼서 정상 검색엔진 크롤러가 색인을 빼버릴 수 있다. 429는 "속도만 줄여라"라는 뜻이라 훨씬 안전하다. 그리고 CDN 뒤에 있다면 $binary_remote_addr는 CDN IP이므로 real_ip_headerset_real_ip_from 설정이 먼저다. 이거 안 하면 전 세계 트래픽을 CDN 엣지 IP 몇 개로 묶어서 레이트리밋하게 되고, 서비스가 통째로 429를 뱉는다.

4. 실무 관점: 트레이드오프와 흔한 함정

함정 1: real_ip 설정 누락으로 전체 429

가장 자주 보는 사고다. 로그에 이런 게 도배된다.

2024/03/11 14:22:07 [error] 1274#1274: *88213 limiting requests,
excess: 40.812 by zone "perip", client: 172.68.34.9,
server: example.com, request: "GET /product/8812 HTTP/2.0",
host: "example.com"

client:가 실제 사용자 IP가 아니라 CDN 엣지 IP(위 예시는 Cloudflare 대역 형태)로 찍히면 100% 이 문제다. 해결은 아래처럼.

set_real_ip_from 173.245.48.0/20;   # CDN 대역 전체를 넣어야 함
# ... (사업자 공식 IP 목록을 스크립트로 갱신)
real_ip_header CF-Connecting-IP;    # 또는 X-Forwarded-For
real_ip_recursive on;

CDN IP 목록은 바뀌므로 하드코딩하고 방치하면 안 된다. 사업자별 공식 IP 목록 엔드포인트를 크론으로 받아 include 파일로 떨구는 게 정석이다.

함정 2: PoW 챌린지가 SEO와 접근성을 때린다

Anubis류를 전 경로에 걸면 Googlebot·네이버 Yeti도 챌린지를 만난다. JS를 실행하는 크롤러도 있지만 렌더 예산이 있어서 색인 속도가 떨어질 수 있고, 실행하지 못하는 쪽은 그냥 페이지를 못 본다. 실무 처방은:

  • 검증된 검색엔진은 역방향 DNS + 정방향 확인으로 화이트리스트. UA만 믿으면 안 된다.
  • 챌린지는 비싼 경로에만. 검색·필터·정렬 파라미터가 붙은 URL, API 엔드포인트, 페이지네이션 깊은 곳.
  • 정적 상세 페이지는 캐시로 버티고 챌린지 없이 둔다.
  • JS 미지원 클라이언트(스크린리더 환경, 구형 브라우저)를 위한 우회 경로를 반드시 남긴다.
# 자칭 Googlebot 검증 (rDNS 왕복 확인)
$ dig +short -x 66.249.66.1
crawl-66-249-66-1.googlebot.com.
$ dig +short crawl-66-249-66-1.googlebot.com
66.249.66.1        # 일치하면 진짜

$ dig +short -x 47.128.23.77
ec2-47-128-23-77.ap-southeast-1.compute.amazonaws.com.
# UA는 Googlebot인데 EC2 → 위조 확정

함정 3: ASN 통째 차단이 정상 사용자를 자른다

"AWS 대역 전부 차단"은 시원하지만 위험하다. 국내 환경에서 실제로 걸리는 것들: 카카오톡·라인 링크 미리보기 봇, 슬랙 unfurl, 결제 PG사 서버 콜백, 파트너사 API 클라이언트, 기업 VPN, 그리고 자사 모니터링/합성 모니터링. Synthetic 체크가 죽으면서 "장애 알림이 안 왔다"가 아니라 "알림이 계속 온다"로 나타나기도 한다.

그래서 순서를 지켜야 한다. 탐지만 켜고 로그 기록(dry-run) → 며칠 관찰 → 챌린지/지연 → 마지막에 차단. 처음부터 403을 쏘는 건 자기 발등 찍기다.

함정 4: 타르핏은 상대만 붙잡는 게 아니다

일부러 응답을 30초 지연시키거나 무한 스트림을 내려주는 타르핏은 심리적 만족도가 높다. 하지만 그 커넥션은 내 워커/파일디스크립터도 점유한다. Nginx처럼 이벤트 기반이면 그나마 싸지만, 그 뒤에 스레드 기반 앱 서버가 있다면 자기 자신을 DoS 하게 된다. 타르핏은 반드시 엣지에서, 커넥션 상한을 걸고 쓴다.

실제 효과가 가장 컸던 것: 캐시와 URL 설계

솔직히 말하면, 화려한 챌린지보다 이게 효과가 컸다. 150만 페이지의 정체는 대개 파라미터 조합 폭발이다.

  • 정렬·필터 파라미터 조합 URL에 rel="canonical"을 걸고 robots에서 차단
  • 파라미터를 화이트리스트 방식으로 정규화 → 캐시 키 수를 한 자릿수로 줄임
  • 깊은 페이지네이션(예: page=500 이상)은 아예 404 또는 챌린지
  • 익명 사용자에겐 무조건 CDN 캐시 응답. 로그인 쿠키가 있을 때만 origin

스크래퍼가 여전히 오더라도 전부 엣지 캐시에서 끝나면 origin은 조용하다. 트래픽 비용은 남지만 DB는 산다. 트래픽 비용까지 잡으려면 그때 차단 단계로 간다.

5. 정리

한 줄 요약: UA와 robots.txt로 "누구냐"를 묻는 방어는 이미 끝났다. 행동 패턴으로 점수를 매기고, 캐시로 부하를 흡수하고, PoW·레이트리밋으로 상대의 수집 단가를 올리는 다층 방어로 가야 한다.

단계별로 이렇게 권한다.

  • 페이지 수천 개 이하, 부하 여유 있음 → robots.txt 선언 + 자칭 봇 UA 레이트리밋(429)까지. 그 이상은 과투자다.
  • 페이지 수만~수십만, origin CPU가 튀기 시작 → 캐시 키 정규화와 파라미터 통제가 1순위. 차단보다 먼저.
  • 페이지 수십만 이상 + 트래픽 비용이 아프다 → ASN 스코어링, JA3/JA4 지문, CDN Bot Management 검토. 여기서 오탐 관리 프로세스(dry-run → 챌린지 → 차단)를 반드시 문서화.
  • 커뮤니티/위키/문서 사이트, 익명 읽기 트래픽이 핵심 → Anubis류 PoW 챌린지가 가장 가성비 좋은 구간으로 보인다. 단 SEO 경로 예외 처리가 전제.

마지막으로 하나. 이건 끝나는 싸움이 아니다. 원문 제목이 "1년간의 싸움"인 이유가 그거다. 차단 규칙은 만드는 순간부터 낡기 시작하니, 규칙 자체보다 탐지 지표를 대시보드에 상시 띄워두는 것이 더 중요하다. 나는 "캐시 히트율", "origin 요청 중 부수 리소스 미요청 비율", "클라우드 ASN 비중" 세 개를 그래프로 보고 있다. 이 셋 중 하나가 꺾이면 새 스크래퍼가 들어온 것이다.

참고 자료

728x90

+ Recent posts