Apache Nutch는 Apache Software Foundation이 만든 크롤러로, 2004년에 개발이 시작되었습니다. Hadoop 위에서 돌아가는 JVM 기반 시스템이며, 단일 스트리밍 명령이 아니라 반복 루프 구조로 짜여 있습니다. 시드 URL은 영구 데이터베이스에 inject되고, 그다음 generate → fetch → parse → updatedb 흐름이 계속 반복됩니다. 프로토콜, 파서, URL 필터, 스코어링을 위한 플러그인 슬롯도 갖추고 있습니다. 보통 결과물은 CSV가 아니라 Solr나 Elasticsearch 같은 검색 인덱스로 흘러갑니다.
저는 Nutch 1.22를 통제된 로컬 테스트 사이트에 적용해 봤습니다. 이 사이트는 모든 요청을 서버 쪽에서 기록하므로, 결과는 크롤러가 뭐라고 주장하느냐가 아니라 서버가 실제로 확인한 내용 기준으로 판단했습니다. 이번 실행은 네 가지 경계에 따라 갈렸습니다. JDK 버전, http.agent.name, 크롤링 범위, 그리고 plugin.includes에 parse-js가 들어가 있는지 여부입니다. 테스트한 설정으로 전체 사이클을 여러 번 돌려봤지만, JDK를 바꾸거나 에이전트 식별자를 비워 두면 쓸 만한 페이지를 가져오기 전에 멈췄습니다.
JDK 경계는 크롤링이 시작되기도 전에 드러났습니다. Nutch 1.22는 JDK 26.0.1에서 바로 출발하지 못했습니다. 첫 번째 Hadoop 작업이 Java가 SecurityManager 경로를 제거한 뒤 Subject.getSubject() 안에서 죽었기 때문입니다. Nutch는 Hadoop 3.4.2를 포함하고 있는데, 이 문제를 고친 패치는 Nutch 1.22가 나온 지 7일 뒤에 공개된 Hadoop 3.4.3에 들어갔습니다. 한편 parse-js는 브라우저를 띄우지 않고도 JavaScript 파일 안의 두 리터럴을 0/2에서 2/2로 되살려 냈습니다.
Nutch의 용도와 한계
Nutch는 스크레이퍼가 아닙니다. 구조화된 필드를 뽑아내는 게 주목적이 아닙니다. 대량의 URL을 찾아서 가져오고, 그 URL과 상태를 담는 영속 데이터베이스(crawldb)를 유지하고, 다른 도구가 인덱스로 바꿀 세그먼트를 넘겨주는 역할을 합니다. 이름표와 가격표가 있는 표를 기대하고 카탈로그를 넣으면, 표 대신 crawldb가 나옵니다.
이 구조가 뒤의 내용을 거의 다 설명해 줍니다. Nutch는 단일 바이너리 크롤러 시대보다 약 20년 앞서 만들어졌고, Hadoop이 풀고자 했던 문제, 즉 한 대의 머신에 다 담기지 않는 규모의 페이지를 크롤링하는 용도로 설계되었습니다. 12페이지짜리 테스트를 노트북에서 돌리는 건 책장 하나 옮기려고 화물열차를 부르는 것과 비슷합니다. 열차의 성격을 이해하는 데는 유익하지만, 자전거의 민첩함을 기대하면 공정하지 않습니다.
현재 릴리스는 1.22이며, 2026년 2월 17일에 공개되었습니다. 라이선스는 Apache-2.0이고, 제가 2026년 7월 27일 확인했을 때 저장소는 3,272 stars, 8 open issues 상태였으며, master에는 그 4일 전까지 커밋이 올라와 있었습니다. 즉, 방치된 프로젝트가 아니라 유지보수되는 프로젝트입니다. 이 점은 JDK 문제를 볼 때도 반영해야 합니다. 방치가 아니라, 패키징 시점이 일주일 정도 앞서 닫혀 있었던 셈이기 때문입니다.
버전 매트릭스: JDK 24+, Hadoop 3.4.2, 그리고 두 줄짜리 수정
막히는 지점은 세 가지 버전의 맞물림이며, 그중 사용자가 건드릴 수 있는 건 Nutch를 어떤 JDK에서 돌리느냐뿐입니다. 호스트의 기본 JDK에서 첫 번째 Hadoop 작업은 초기화 단계에서 곧바로 실패했습니다.
java.lang.UnsupportedOperationException: getSubject is not supported
at java.base/javax.security.auth.Subject.getSubject(Subject.java:277)
at org.apache.hadoop.security.UserGroupInformation.getCurrentUser(UserGroupInformation.java:588)
at org.apache.nutch.crawl.Injector.inject(Injector.java:473)
종료 코드는 255였습니다. 가져온 페이지는 0개였습니다. bin/nutch inject는 네트워크에 닿기도 전에 Hadoop LocalJobRunner를 초기화하는데, 여기서 현재 사용자가 누구인지 확인하려고 Subject.getSubject()를 호출합니다. 그런데 **JEP 486**가 JDK 24에서 SecurityManager를 영구 제거하면서 이 호출은 무조건 예외를 던지게 바뀌었습니다. 제가 사용한 호스트 JDK는 OpenJDK 26.0.1이었으니, 이미 그 선을 한참 넘은 상태였습니다.
예전 우회 방법도 통하지 않습니다. 예전에는 옛 동작을 다시 켜는 역할을 하던 -Djava.security.manager=allow를 붙여도, Nutch 코드가 로드되기 전에 VM이 바로 막아 버립니다.
Error occurred during initialization of VM
java.lang.Error: A command line option has attempted to allow or enable the Security
Manager. Enabling a Security Manager is not supported.
종료 코드는 1이고, 의도된 막다른 길입니다. 이 플래그는 기능과 함께 같이 사라졌습니다.
근본 원인은 Nutch 1.22에 묶인 Hadoop 버전에 있습니다. getSubject 문제는 HADOOP-19212로 추적되며 Hadoop 3.4.3과 3.5.0에서 해결됐습니다. 반면 Nutch 1.22는 hadoop-common-3.4.2를 포함합니다. Nutch 1.22는 2026년 2월 17일에 나왔고, Hadoop 3.4.3은 약 일주일 뒤에 공개되었습니다.
이 문제는 Solr나 Hadoop 클러스터 의존성 문제도 아닙니다. Nutch는 뭔가 하려면 Hadoop 클러스터와 실행 중인 Solr가 꼭 필요하다고 생각하는 경우가 많습니다. 하지만 그렇지 않습니다. 로컬 모드에서는 Hadoop의 프로세스 내 LocalJobRunner가 돌아가므로 HDFS 데몬도, YARN도, 클러스터도 필요 없습니다. inject → generate → fetch → parse → updatedb 전체 사이클이 다른 설치 없이 단일 머신에서 돌아갑니다. JDK 장벽은 순전히 번들 라이브러리 버전 문제이며, 이런 인프라 얘기가 나오기도 전에 먼저 막아 버립니다.
실제로 측정한 버전 매트릭스는 다음과 같습니다.
| 사용한 JDK | 명령 | 결과 |
|---|---|---|
| OpenJDK 26.0.1 | bin/nutch inject | 실패, rc=255 — UnsupportedOperationException: getSubject is not supported |
| OpenJDK 26.0.1 | bin/nutch inject + -Djava.security.manager=allow | 실패, rc=1 — VM 시작 자체 거부 |
| OpenJDK 17.0.20 (LTS) | bin/nutch inject | 성공, rc=0 — Total new urls injected: 1 |
해결법은 두 줄입니다. LTS JDK를 설치하고, Nutch가 그 JDK를 보게 하면 됩니다.
brew install openjdk@17
export NUTCH_JAVA_HOME=/opt/homebrew/opt/openjdk@17
keg-only라 시스템 기본 JDK는 건드리지 않습니다. Nutch의 자체 CI도 Java 17을 대상으로 하고 있고, 프로젝트는 공식적으로 1.22가 Java 11에서 동작하는 마지막 릴리스이며 1.23부터는 Java 17이 필요하다고 밝혔습니다. 즉, LTS JDK는 임시 땜질이 아니라 지원되는 구성입니다. 문제는 Nutch가 지원하는 버전과 2026년 brew install openjdk가 기본으로 주는 버전이 달랐고, 그 둘이 첫 명령에서 정면 충돌했다는 점입니다.
이후 내용은 모두 OpenJDK 17.0.20에서 실행했으며, 이 환경에서는 전체 사이클이 매끄러웠습니다.
설치 규모 측정: 396MB와 모든 걸 막는 한 가지 속성
“무겁다”는 말은 누구나 할 수 있지만, 숫자가 없으면 감이 안 옵니다. 실제로 압축을 푼 Nutch 1.22 배포본이 담고 있는 내용은 다음과 같습니다.
| 항목 | Nutch 1.22 바이너리 배포본 |
|---|---|
| 압축 해제 크기 | 약 396 MB |
lib/의 JAR 수 | 188개 (약 113 MB) |
| — 그중 번들 Hadoop 스택 | 13개 |
| 플러그인 디렉터리 | 78개 |
| 해당 디렉터리 안의 JAR 수 | 533개 |
| 설정 파일 | 35개 |
bin/의 스크립트 | 2개 — crawl, nutch |
비교하자면, 현대적인 Go 크롤러인 katana는 JVM도 없고 외부 JAR도 없는 약 50MB짜리 단일 바이너리로 배포됩니다.
그리고 아무도 미리 알려주지 않는 관문이 하나 있습니다. 제공되는 nutch-site.xml은 비어 있고, http.agent.name은 기본값이 빈 문자열입니다. 이 값이 비어 있으면 첫 크롤은 경로를 하나도 가져오지 못했고, 다음 로그가 남았습니다.
ERROR Fetcher: No agents listed in 'http.agent.name' property.
이 속성 하나만 설정하면, 다른 건 건드리지 않아도 정상적으로 fetch가 진행되었습니다. 값이 비어 있을 때는 명령이 페이지를 가져오지 못한 채 끝났고, 위 에이전트 이름 오류가 로그에 찍혔습니다. 조용히 실패한 건 아니었습니다.
최소로 필요한 설정은 세 가지였습니다. conf/nutch-site.xml(에이전트 이름, 플러그인 세트, 범위), conf/regex-urlfilter.txt(호스트 범위 지정), 그리고 시드 URL 파일입니다. 나쁜 숫자는 아닙니다. 다만 crawler run <url>보다 파일이 세 개 더 필요할 뿐입니다.
무엇을 찾았나: 중요한 것은 플러그인 토글

테스트 사이트에는 의도적으로 서로 다른 세 가지 종류의 엔드포인트가 있었고, Nutch의 동작은 그에 맞춰 아주 분명하게 갈렸습니다.
- Class A — 일반 HTML 링크(4개 페이지, 그리고 3단계 깊이의 체인)
- Class B — 연결된 JavaScript 파일 내부의 문자열 리터럴로만 존재하는 엔드포인트: 하나는 함수 인자로
fetch('/api/js-endpoint-7'), 다른 하나는 할당문const other = "/api/js-endpoint-8" - Class C — JavaScript가 실행된 뒤 DOM에 삽입되어야만 생기는 엔드포인트
서버 쪽 히트 로그를 기준으로 결과를 세 번 반복해 보면 다음과 같습니다.
| 플러그인 설정 | Class A (HTML 링크) | Class B (JS 파일 리터럴) | Class C (런타임 DOM) |
|---|---|---|---|
기본 제공 — parse-(html|tika) | 4/4 (재현율 1.0) | 0/2 (재현율 0.0) | 도달 못함 |
parse-js 포함 — parse-(html|tika|js) | 4/4 (재현율 1.0) | 2/2 (재현율 1.0) | 도달 못함 |
세 번 반복해도 결과는 같았습니다. 결정론적입니다.
Class B의 변화는 쉽게 놓치기 쉽습니다. Nutch는 브라우저를 실행하지 않고도 parse-js 플러그인의 정규식 기반 스캔으로 JavaScript 안에 박힌 두 엔드포인트를 전부 찾아냈습니다. app.js 파일 자체는 두 설정 모두에서 가져왔는데, Nutch가 <script src>를 무조건 outlink로 취급하기 때문입니다. 따라서 차이는 파일 내용 안에서 URL처럼 생긴 문자열을 읽어내느냐에 달려 있습니다. 플러그인을 켜면 두 리터럴 형식 모두 잡아냅니다.
이 테스트에서 Nutch의 기본 설정과 katana의 표준 모드는 같은 Class A 세트에 닿았고, Nutch에 parse-js를 켠 경우와 katana에 -jc를 준 경우는 브라우저 없이 Class A와 B를 모두 잡아냈습니다. 다만 이 글에는 katana의 버전과 전체 명령이 적혀 있지 않으므로, 이것은 엄밀한 제품 벤치마크라기보다 맥락에 가까운 비교입니다.
Class C는 아주 분명한 한계선입니다. 어떤 정적 플러그인 설정으로도 닿지 못했습니다. 스크립트 실행 후에만 생기는 엔드포인트를 살리려면 실제로 스크립트를 돌려야 하기 때문입니다. Nutch의 순수 Java JS 실행 프로토콜인 protocol-htmlunit으로도 바꿔 봤습니다. 로드와 실행은 크래시 없이 됐지만, 같은 4라운드 하니스에서는 1라운드만 끝나고 시드 페이지와 app.js만 가져왔으며 A/B/C 어디에도 닿지 못했고, 2라운드에서는 0 records selected for fetching이 기록되었습니다. 이건 HtmlUnit 기능을 제대로 맞추지 못한 실험이지, 그 자체에 대한 최종 판정은 아닙니다. 여기서 분명히 말할 수 있는 건 더 좁습니다. JS 실행 프로토콜로 바꾸는 건 단순한 드롭인 변경이 아니다는 점, 그리고 제가 테스트한 모든 설정에서 Class C는 끝내 닿지 못했다는 점입니다.
크롤 제어와 실패 동작
깊이는 플래그가 아닙니다. Nutch에는 --depth 3 같은 옵션이 없습니다. 깊이는 generate → fetch → parse → updatedb를 몇 라운드 돌리느냐입니다. 라운드 R이 라운드 R-1에서 발견한 프론티어를 가져오기 때문입니다. 깊이 체인은 이걸 정확하게 보여줍니다.
| 실행한 라운드 수 | 도달한 가장 깊은 경로 |
|---|---|
| 2 | /depth/1 |
| 3 | /depth/2 |
| 4 | /depth/3 |
깔끔하고 기계적이지만, 깊이는 파라미터가 아니라 스크립트 안에서 관리하는 루프 횟수라는 뜻입니다.
이제 함정입니다. Nutch의 기본값은 db.ignore.external.links=false이고, 여기에 넉넉한 +. URL 필터가 붙어 있습니다. 즉, 기본 Nutch 크롤은 시드 호스트 밖 링크도 따라갑니다. 저는 한 페이지에 범위 안 경로 하나와 다른 호스트로 가는 링크 하나를 심었고, 크롤은 외부 호스트를 실제로 가져갔습니다. 두 개의 독립된 신호가 서로 맞아떨어졌습니다. Nutch의 crawldb는 이를 db_fetched로 표시했고, 다른 호스트의 서버 카운터에도 히트가 찍혔습니다.
범위를 유지하려면 명시적으로 설정해야 하며, 아래 두 방법 모두 실제로 잘 먹혔습니다.
| 설정 | crawldb에 외부 호스트 존재 | 외부 호스트 서버 히트 | 범위 내 유지? |
|---|---|---|---|
db.ignore.external.links=false (기본값) | db_fetched | +1 | 아니오 |
db.ignore.external.links=true | 없음 | 0 | 예 |
regex-urlfilter.txt의 호스트 규칙 (+^http://127.0.0.1: 다음에 -.) | 없음 | 0 | 예 |
한 사이트만 크롤링한다면, 첫 실전 실행 전에 둘 중 하나는 꼭 설정하세요. 한 가지 방법론상의 주의점도 있습니다. 이번 테스트는 로컬 서버 부하에 민감하므로, 위 세 행은 테스트용 자원을 아무도 건드리지 않은 상태에서 나온 결과입니다. 동작 자체는 기계적으로 명확하고 두 개의 독립 신호로 뒷받침되지만, 특정 행의 값은 여러 번 평균낸 게 아니라 한 번의 깔끔한 실행 결과입니다.
사이트맵은 별도 단계입니다. 크롤 예절은 지켜집니다. 일반 크롤에서는 /robots.txt를 가져왔지만, 사이트맵은 별도 명령이 필요합니다.
| 방식 | /sitemap.xml 요청 여부 | 사이트맵에만 있던 엔드포인트 |
|---|---|---|
| 일반 크롤 | 요청하지 않음 | 0/2 |
crawldb를 대상으로 명시적으로 실행한 bin/nutch sitemap | 가져옴 | 항목 2/2 주입, 전체 재현율 |
katana의 인라인 -kf known-files 모드, 같은 테스트, IP 호스트 기준 | 기록 없음 | 0/2 |
known-files를 인라인으로 가져오는 크롤러와는 다른 모델이며, 명령 하나를 더 써야 하지만 결과는 완전히 얻습니다.
작은 동작 두 가지도 잘 버텼습니다.
에러 처리: 500과 404로 이어지는 페이지를 크롤해도 전체 라운드는 문제없이 끝났고, 네 개의 Class A 페이지는 모두 가져왔으며, 각 실패는 서로 다른 상태로 기록되었습니다.
| 페이지에서 링크된 실패 응답 | crawldb에 기록된 상태 |
|---|---|
| 500 | db_unfetched (재시도 가능) |
| 404 | db_gone |
무너지지 않았습니다.
정중한 크롤링(politeness): 큐당 스레드 1개로 맞췄을 때, 같은 호스트에 대한 fetch 사이 간격은 설정값을 따랐습니다.
fetcher.server.delay | 같은 호스트 fetch 간 중앙값 간격 |
|---|---|
| 1.0초 | 1.009초 (최소 1.006초) |
| 0.0 | 0.002초 |
이 옵션은 말 그대로 그대로 동작합니다. 기본값은 5.0초이며, 꽤 보수적입니다. 남의 서버를 크롤하도록 만든 도구라면 오히려 맞는 선택이기도 합니다.
배치 비용, 초 단위로 보면
Nutch의 모든 명령은 매번 새 JVM을 띄웁니다. 이 한 가지 사실이 fetch 자체보다 타이밍 프로파일을 더 크게 좌우합니다.
| 단계(라운드당) | 중앙값 초 |
|---|---|
inject (1회) | 1.81 |
generate | 3.93 |
fetch | 2.82 |
parse | 1.78 |
updatedb | 1.81 |
| 전체 1라운드 | 12.14 |
실질적인 작업 하한선 — JVM 시작과 Hadoop 초기화, 즉 가장 단순한 작업 단계의 비용 — 은 약 1.77초입니다. 이 값을 라운드당 네 개 명령에 곱하고, 초기 inject를 더하면 전체 크롤 시간은 다음과 같습니다.
| 도구 | 12페이지 테스트의 depth-4 크롤 | 프로세스 수 |
|---|---|---|
| Nutch | 약 45초 (두 설정에서 45.8초와 45.0초 측정) | 대략 17번의 JVM 실행, 그중 대부분은 네트워크 작업을 하지 않음 |
katana standard 모드, 같은 테스트 | 약 13초 | 1개 프로세스 |
이 차이는 fetch 처리량 때문이 아닙니다. 두 도구 모두 같은 몇 페이지를 요청했습니다. 구조의 차이입니다. Nutch는 각 단계가 MapReduce 작업처럼 설계되어 있어서 단계마다 고정 프로세스 비용을 치릅니다. 작은 로컬 크롤에서는 설정 비용이 압도적입니다. 더 긴 작업에서는 이 고정 비용이 전체에서 차지하는 비율이 줄어들어야 하지만, 이번 테스트는 Nutch와 katana의 교차 지점이나 비율이 뒤집히는 규모까지는 재지 않았습니다.
장단점
장점
- 반복 실행에서도 결정론적인 정적 발견: HTML Class 4/4, depth 체인 3/3, 세 번의 반복 모두 동일.
parse-js는 브라우저 없이 JavaScript 파일 리터럴 엔드포인트(2/2)를 되살려 내며, 함수 인자형과 할당형을 모두 잡음.- 크롤을 완전히 가둘 수 있는 두 가지 검증된 범위 제어(
db.ignore.external.links, 호스트regex-urlfilter)가 있음. bin/nutch sitemap으로 사이트맵을 읽으면 일반 크롤이 놓친 엔드포인트를 2/2로 모두 회복.- 실패에 강함: 500과 404를 서로 다른 crawldb 상태로 처리하면서도 크롤은 계속 진행됨.
- 이번 로컬 실행에서는 같은 호스트 간격이 설정한 1.0초 지연과 일치했으며, 기본값은 5.0초.
- Apache-2.0, 적극적으로 유지보수 중, 78개 플러그인, 라운드 간 URL 상태를 추적하는 영속 crawldb.
- 클러스터, HDFS, Solr 없이 로컬 모드로 실행 가능.
단점
- JDK 24 이상에서는 SecurityManager 제거에 걸려 실행되지 않음(26.0.1에서 실패를 측정함) — 번들 Hadoop 3.4.2가 상위 패치보다 앞서 있고 우회 플래그도 사라졌기 때문에, LTS JDK 고정은 선호가 아니라 필수 전제임.
- 압축 해제 기준 약 396MB, 라이브러리 JAR 188개, 플러그인 디렉터리 78개, 설정 파일 35개.
- 명령마다 새 JVM을 띄우므로 단계당 약 1.77초의 고정 오버헤드가 있고, 12페이지 depth-4 크롤은 약 45초인 반면 동일 조건의 단일 바이너리 크롤러는 약 13초.
- 기본값이 외부 호스트 링크까지 따라가므로, 한 사이트 안에 머무르려면 별도 설정이 필요함.
http.agent.name은 빈 상태로 제공되며, 설정하지 않으면 fetcher가 실행을 거부함.- depth 플래그가 없어, 깊이는 사용자가 직접 루프 횟수로 관리해야 함.
- 런타임 DOM 엔드포인트는 테스트한 모든 설정에서 닿지 못했고, JS 실행 프로토콜로 바꾸는 것도 단순한 드롭인 변경이 아니었음.
- 로컬 단일 호스트와 작은 테스트 자원만 사용했습니다. 분산/HDFS 모드, Solr 인덱싱, hostdb, resume, 증분 재크롤 스케줄링은 이번 검토 범위 밖이므로, 여기서는 미검증 사항으로 봐야 합니다.
누가 써야 하고, 누가 돌아서야 하나
크롤 자체가 어려운 문제일 때 Nutch는 제값을 합니다. 검색 인덱스를 만들거나, 여러 도메인에 걸친 대규모 크롤을 하거나, URL별 상태와 재시도 의미를 가진 영속 URL 데이터베이스가 필요하거나, 나중에 작업을 여러 머신으로 분산할 가능성이 있다면, 이것은 다른 대안들보다 훨씬 먼저 그 일을 해 온 인프라입니다. 플러그인 시스템 덕분에 프로토콜, 파서, 필터, 스코어링 동작을 포크 없이 바꿀 수 있습니다. 정중한 크롤 기본값은 유지보수자가 남의 서버에 예의 있게 굴기 위해 꽤 깊이 고민했다는 인상을 줍니다.
반대로, 몇 페이지에서 구조화된 데이터를 뽑아내고 싶은 거라면 돌아서세요. Nutch는 가져오고 파싱한 뒤 crawldb와 세그먼트를 넘기고, 인덱서까지는 사용자 몫이라고 기대합니다. 대상이 클라이언트 렌더링 SPA라면 돌아서세요. 제가 돌린 모든 설정에서 Class C는 끝내 닿지 못했습니다. 팀이 JVM을 전혀 쓰지 않는다면 돌아서세요. Java 툴체인, LTS JDK 고정, 396MB의 JAR를 스택에 새로 얹어야 하기 때문입니다. 그리고 작업이 “한 사이트를 주 1회, 4단계 깊이로 크롤링”이라면, 크롤보다 라운드 루프와 설정 파일에 더 많은 시간을 쓰게 될 가능성이 큽니다.
대부분의 스크레이퍼 구매자에게는 바로 그 마지막 경우가 현실입니다. 그렇다고 해서 Nutch가 나쁘다는 뜻은 아닙니다. 그냥 도구와 업무가 맞지 않는 것입니다. 더 넓은 스펙트럼을 보고 싶다면, 오픈소스 스크레이퍼 정리와 최고의 웹 스크래핑 GitHub 프로젝트에서 더 가벼운 쪽을 자세히 다루고 있습니다.
대안들, 그리고 우리 스택이 어디에 들어가는지
먼저 공정하게 말하면, Nutch는 무료이고 Apache 라이선스이며, 자체 호스팅이 가능하고, 요청당 비용 없이 영구적으로 운영할 수 있습니다. 이건 진짜 강점이고, 아래 내용이 그 가치를 지우지는 않습니다.
관련 리뷰: Browsertrix Crawler 리뷰.
오픈소스 안에서의 비교는 무엇을 최적화하느냐에 따라 달라집니다. 크롤 제어와 request-first 철학을 가진 Python 프레임워크가 필요하다면 Scrapy가 많은 프로젝트에 더 맞는 대안입니다. 다만 이 글은 같은 기준으로 설치 크기를 재지는 않았습니다. 브라우저 없이 돌아가는 작고 단단한 Go 크롤러를 찾는다면 Colly도 살펴볼 만합니다. 핵심이 URL 발견이 아니라 페이지를 LLM에 넣을 수 있는 콘텐츠로 바꾸는 것이라면 Crawl4AI가 겨냥하는 층이 다릅니다.
Thunderbit 같은 관리형 서비스는 fetch, rendering, extraction을 API 뒤로 넘기고, Nutch는 크롤 상태와 인프라를 직접 잡게 합니다. Thunderbit는 이번 테스트 자원에서 실행하지 않았으므로, 이것은 재현율이나 동적 페이지 성능에 대한 주장이 아니라 소유 모델 비교입니다.
트레이드오프는 소유권 대 오버헤드이며, 이건 전혀 미묘하지 않습니다. Nutch는 완전한 통제권, 영속 crawldb, 설계상 클러스터 확장성, 그리고 marginal cost 0을 주는 대신 JVM, LTS JDK 고정, 396MB의 JAR, 라운드 루프, 그리고 자체 인덱싱 계층을 요구합니다. 관리형 API는 첫 호출부터 구조화된 출력을 주고 인프라도 필요 없지만, 호출당 과금과 크롤 프론티어에 대한 낮은 통제력을 감수해야 합니다. 일이 “5천만 페이지를 인덱싱하는 것”이라면 Nutch의 모델이 맞고 API는 비현실적입니다. 일이 “목요일까지 200개 상품 페이지에서 구조화된 레코드를 뽑아오는 것”이라면 반대입니다.
웹 데이터 추출을 위해 Thunderbit 사용해 보기
결론
계속되는 다중 도메인 크롤을 운영 중이고 JVM 인프라도 이미 갖추고 있다면 Apache Nutch를 검토할 가치가 있습니다. 이번 테스트 자원에서는 반복 실행 전반에 걸쳐 정적 발견이 결정적이었고, parse-js는 두 개의 리터럴 JavaScript 엔드포인트를 찾아냈으며, 실패는 crawldb에 그대로 남았고, 관찰된 요청 간격은 설정한 지연과 맞아떨어졌습니다.
도입 비용은 솔직하게 계산해야 합니다. Nutch 1.22는 여기서 JDK 26.0.1에서 실패했습니다. 이번 리뷰에서 실제로 검증된 LTS 구성은 OpenJDK 17.0.20이며, Java 21은 테스트하지 않았습니다. 그다음 http.agent.name을 설정하고, 범위를 명시적으로 정하고, 이번 작은 로컬 실행에서 관찰된 단계당 약 1.77초의 고정 하한을 감안해야 합니다. 이 거래가 의미 있는지는 크롤의 기간, 범위, 그리고 영속 상태가 필요한지에 달려 있습니다.
웹 데이터 추출을 위해 Thunderbit 사용해 보기 Get Started Free
자주 묻는 질문
Apache Nutch가 왜 "getSubject is not supported" 오류로 실패하나요?
JDK 24 이상에서는 JEP 486 때문에 Subject.getSubject()가 무조건 예외를 던집니다. 그런데 번들 Hadoop 3.4.2는 여전히 이 메서드를 호출합니다. 그래서 첫 Hadoop 작업이 페이지를 가져오기 전에 죽고, 예전의 -Djava.security.manager=allow 우회도 더 이상 VM을 시작시키지 못합니다. 검증된 Java 17 구성을 사용하고 NUTCH_JAVA_HOME을 설정하세요. Java 21은 지원될 수도 있지만, 이 리뷰에서는 전체 사이클을 돌려보지 않았습니다.
Nutch 1.22는 어떤 Java 버전에서 실행해야 하나요?
가장 안전한 답은 Java 17입니다. Nutch 자체 CI도 이를 대상으로 하고 있고, 제 테스트(OpenJDK 17.0.20)에서도 깔끔하게 동작했습니다. Java 11도 1.22에서는 여전히 지원됩니다. 다만 프로젝트는 1.23부터 Java 17이 필요하다고 밝혔습니다. JDK 24 이상은 실행되지 않습니다. keg-only Homebrew 설치(brew install openjdk@17)에 NUTCH_JAVA_HOME을 더하면 시스템 기본 JDK는 건드리지 않습니다.
Nutch는 JavaScript가 많은 사이트도 크롤링할 수 있나요?
부분적으로는 가능합니다. 그리고 이 차이가 중요합니다. parse-js 플러그인을 켜면 Nutch는 연결된 JavaScript 파일 내부의 문자열 리터럴로만 존재하던 두 엔드포인트를 모두 찾아냈습니다. 브라우저 없이 2/2였습니다. 기본 플러그인 세트에서는 둘 다 찾지 못했습니다. 하지만 JavaScript가 실행된 뒤 DOM이 바뀌어야 나타나는 엔드포인트는 제가 테스트한 모든 정적 설정에서 도달하지 못했고, HtmlUnit 프로토콜로 바꾼 것도 제 실행에서는 단순한 드롭인 변경이 아니었습니다. 클라이언트 렌더링 앱이라면 JS 실행 프로토콜과 실제 설정 작업이 필요하거나, 다른 도구를 쓰는 편이 낫습니다.
Nutch를 쓰려면 Hadoop과 Solr를 따로 설치해야 하나요?
아닙니다. 로컬 모드에서는 Hadoop의 프로세스 내 LocalJobRunner가 실행되므로 클러스터, HDFS 데몬, YARN이 필요 없습니다. inject → generate → fetch → parse → updatedb 전체 사이클이 다른 것 없이 한 대의 머신에서 동작합니다. Solr는 일반적인 인덱싱 대상이지만, 크롤 자체에는 필요하지 않습니다. 다만 Hadoop JAR가 번들로 들어 있다는 점(13개, 버전 3.4.2)이 바로 JDK 호환성 문제가 생기는 이유이기도 합니다.
Nutch가 다른 웹사이트까지 크롤링하지 못하게 하려면 어떻게 해야 하나요?
기본값이 막아주지 않기 때문에 명시적으로 설정해야 합니다. Nutch 1.22는 db.ignore.external.links=false와 넉넉한 URL 필터를 함께 제공하며, 제 테스트에서는 기본 크롤이 다른 호스트로 가는 링크를 따라가 실제로 가져왔습니다. nutch-site.xml에서 db.ignore.external.links=true를 설정하거나, conf/regex-urlfilter.txt에 호스트 규칙을 추가하세요(예: +^https://example\.com/ 다음에 -.). 두 방법 모두 테스트에서 크롤을 완전히 해당 사이트 안에 가뒀고, Nutch의 crawldb와 다른 서버의 요청 로그로 확인했습니다.


