Apache Tika는 Apache Software Foundation의 문서 파싱 툴킷입니다. 거의 모든 형식의 파일을 넣으면, 일반 텍스트와 정규화된 메타데이터 딕셔너리를 돌려줍니다. 프로젝트 README에는 1,000개가 넘는 파일 형식을 지원한다고 적혀 있고, Tika는 이를 위해 PDF는 PDFBox, Office 문서는 Apache POI, HTML은 jsoup, ODT는 ODF 리더처럼 전문 라이브러리를 내부에 묶어 제공합니다. 그래서 파싱 시점에 외부에서 무엇을 더 받아올 필요 없는 단일 fat jar로 배포됩니다. 데이터 파이프라인에서 보면 화려하지는 않지만 꼭 필요한 첫 단계입니다. 검색 인덱스 앞단, 전자증거개시(e-discovery) 검토 세트, LLM 코퍼스 앞에서 뒤죽박죽 섞인 파일 더미를 균일한 형태로 바꿔 주는 역할이죠. 결국 두 가지 일입니다. 바이트 스트림이 무엇인지 판별하고, 거기서 텍스트와 메타데이터를 꺼내는 것.
오랫동안 써 본 도구 중 가장 손이 덜 가는 편이었습니다. jar 하나, java -jar tika-app-3.3.2.jar --text file.pdf, 설정 파일도 없고, 모델 가중치도 없고, 설치 후 추가 작업도 없었는데, 같은 호스트에서 같은 날 다른 Java 도구들을 멈춰 세운 최신 JDK에서도 문제없이 동작했습니다. 하지만 제가 확인하고 싶었던 건 카탈로그에 적힌 지원 목록이 아니었습니다. 더 좁고 검증 가능한 질문이었죠. 입력이 거짓말을 하면 Tika는 실제로 무엇을 하느냐? 그래서 각 블록마다 고유한 마커 토큰을 심은 통제된 테스트 세트를 만들고, 같은 논리 문서를 아홉 가지 매체 형식으로 렌더링한 뒤, 잘못된 확장자, 없는 확장자, 파일명 자체가 없는 경우, 0바이트 파일, 중간에 잘린 바이너리까지 전부 던져 봤습니다.
흥미로운 동작은 탐지 단계에서 나옵니다. PDF의 확장자를 .txt로 바꿔서 Tika에게 물었더니 application/pdf라고 답했습니다. 그다음 파일명 자체를 지우고 원시 바이트를 stdin으로 흘려보냈더니 같은 답이 나왔습니다. 제가 만든 콘텐츠 탐지 가능 형식 다섯 가지 전체에서, 이것은 20개의 고유한 논리 조건 모두에서 성립했습니다. 형식마다 파일명 조건 세 가지와 파일명이 없는 스트림 조건 하나씩이었죠. 하니스는 스트림 케이스를 서로 다른 라벨로 세 번 실행해 원시 실행 수는 30회가 되었지만, 그 반복은 독립적인 증거가 아닙니다. PDF와 RTF는 인식 가능한 바이트를 드러내고, DOCX는 컨테이너가 보이며, HTML과 XML은 마크업이나 루트 콘텐츠로 판별됩니다. 방식은 달라도, 이 테스트 세트에서는 결과가 같았습니다. 확장자는 내용을 덮지 못했습니다. 반면 텍스트 계열은 다릅니다. Markdown은 파일명이 틀리거나 사라지는 순간 text/plain으로 무너집니다. 여기서는 .md가 전부였어요.
이후의 모든 숫자에는 두 가지 전제가 있습니다. 제가 테스트한 것은 Apache Tika 3.3.2이고, 2026년 7월 27일 기준으로 이것이 여전히 최신 안정 릴리스였습니다. 4.0.0 계열은 Maven Central에 alpha와 beta 빌드만 존재했습니다. 같은 날 확인했을 때 프로젝트는 GitHub 스타가 대략 3.9k였고, 라이선스는 Apache-2.0입니다. 상업적으로도 거의 아무 제약이 없는 편이죠. 그리고 OCR은 전혀 테스트하지 않았습니다. 스캔한 페이지도 없고, 이미지 전용 PDF도 없었습니다. 제가 사용한 머신에는 Tesseract와 poppler가 설치되어 있지 않아서 OCR 경로는 시작하기도 전에 막혔습니다. 여기에는 OCR 수치가 없습니다. 말 그대로, 없습니다.
포장지 마케팅을 걷어낸 뒤의 Tika
흔한 오해는 Apache Tika가 문서 변환기라는 것입니다. DOCX를 넣으면 제목과 표가 그대로 보존된 깔끔한 Markdown을 돌려준다고 생각하죠. 하지만 Tika는 그런 도구가 아니고, 그 점을 빨리 이해할수록 더 좋은 평가를 받습니다.
이번에 테스트한 경로는 세 단계로 볼 수 있습니다. 콘텐츠 타입 감지기, 바이트를 적절한 파서에 넘기는 디스패처, 그리고 CLI의 --text 출력 핸들러입니다. 이 출력 계약은 평평한 텍스트와 별도로 접근 가능한 메타데이터를 내보냅니다. 여기에는 Title 객체도, ListItem도, 재구성된 표 그리드도 없습니다. Tika는 XHTML/SAX 기반 출력을 포함한 다른 핸들러와 API도 제공하지만, 저는 그것들은 테스트하지 않았습니다. 따라서 아래의 구조 관련 결론은 모두 tika-app --text에 관한 것이며, 툴킷 어디에도 구조화된 이벤트 스트림이 없다는 뜻은 아닙니다.
한계처럼 들리지만, 한쪽 면에서는 맞습니다. 그러나 동시에 Tika가 잘못 분류할 여지를 줄여 준다는 뜻이기도 합니다. 오히려 그 점이 더 중요한 장점일 때가 많죠.
탐지는 문서화된 순서대로 진행됩니다. 시그니처 바이트를 먼저 보고, 그다음 XML 루트를 살피고, 그다음 파일명 glob을 확인하고, 마지막으로 사용자가 직접 지정한 타입을 참고합니다(Tika의 탐지 문서에 잘 정리되어 있습니다). 타입이 정해진 뒤에야 디스패처가 해당 바이트를 맞는 내장 파서에 넘깁니다. PDFBox, POI, jsoup, 텍스트 계열용 TextAndCSVParser 같은 것들입니다.
이 탐지 후 파싱 분리는 내부 잡담이 아닙니다. 파싱에는 실패해도 타입 판별은 맞게 할 수 있기 때문이죠. 문제들이 생기기 시작하면, Tika가 제일 실용적으로 빛나는 지점이 바로 여기입니다.
세팅: jar 하나, 명령 하나, 까다롭지 않은 JVM
설치는 다운로드 한 번이면 끝입니다. Maven Central의 tika-app-3.3.2.jar는 약 67MB짜리 fat jar로, 모든 파서를 한데 묶고 있습니다. 그 뒤에는 java -jar tika-app-3.3.2.jar --text file.pdf만 쓰면 됩니다. 설정 파일도 없고, 모델 가중치도 없고, 설치 후 추가 단계도 없고, brew install 체인을 따라갈 필요도 없습니다.
JDK 쪽은 조금 놀라웠습니다. 저는 전체 실험을 OpenJDK 26.0.1에서, 즉 최신 비-LTS 빌드에서 돌렸고, --version, --text, --metadata, --detect 모두 호환성 경고 없이 종료 코드 0을 반환했습니다. 이 점을 따로 언급할 가치가 있는 이유는, 같은 호스트에서 같은 날 Apache Nutch도 돌려 봤는데 JDK 26에서는 크롤링 사이클이 아예 실행되지 않았기 때문입니다. 최신 JDK에서 SecurityManager가 제거된 여파로 21 이하의 LTS가 필요했죠. Tika는 상관하지 않았습니다. JVM 도구 때문에 계속 골치 아팠다면, 적어도 Tika는 그 문제로 발목 잡히지 않습니다.
설치 관점에서 솔직한 한계도 있습니다. CLI는 호출할 때마다 새 JVM을 띄우므로 콜드 스타트가 실제로 존재합니다. 제 하니스에서 131번 실행한 작업은 대부분 JVM 워밍업에 가까운 한 분 정도가 걸렸습니다. 대량 처리를 한다면 jar를 셸 루프로 감싸기보다는 라이브러리나 서버 모드를 쓰는 편이 낫습니다. 그리고 “의존성 없이 된다”는 말에도 경계선이 있습니다. PDF 텍스트 레이어 추출은 외부 바이너리가 전혀 필요 없지만, OCR에는 tesseract와 poppler가 필요합니다. 텍스트 레이어 PDF, DOCX, ODT, RTF, HTML, XML, TXT, Markdown, CSV는 둘 다 없는 호스트에서 잘 파싱됐습니다. 하지만 스캔 문서는 그렇지 않았을 것이고, 저는 그걸 가장하지 않았습니다.
같은 날 테스트한 형제 라이브러리 unstructured와 비교하면 그 차이는 더 뚜렷합니다. unstructured는 전자 PDF 경로 자체가 막혀 있었는데, PDF 모듈을 import하는 순간 추론 스택(torch 등)을 로드해야 해서, 전략 분기 이전 단계에서 이미 실패했기 때문입니다. “fast” 전략도 그걸 피하지 못했습니다. 반면 Tika는 같은 PDF의 텍스트 레이어를 그냥 java -jar로 읽어 냈습니다.
거짓 확장자 테스트: 파일명보다 내용을 믿는 MIME 타입 판별

8개 형식 각각에 대해, 올바른 확장자, 일부러 틀린 확장자, 확장자 없음, 그리고 stdin으로 넣은 파일명 없는 바이트 스트림을 사용했습니다. 이것은 32개의 고유한 논리 조건입니다. 원래 하니스는 동일한 스트림 바이트를 파일명 라벨별로 한 번씩 다시 실행해서 원시 실행 수는 48회였지만, stdin에는 파일명이 없으므로 그 세 줄은 하나의 조건으로 합쳐집니다.
| 테스트 파일 | 실제 타입 | 바꿔 부른 이름 | 올바른 확장자 | 거짓 확장자 | 확장자 없음 | 파일명 없는 원시 스트림 |
|---|---|---|---|---|---|---|
application/pdf | .txt | ✅ | ✅ | ✅ | ✅ | |
| DOCX | OOXML wordprocessingml | .jpg | ✅ | ✅ | ✅ | ✅ |
| RTF | application/rtf | .html | ✅ | ✅ | ✅ | ✅ |
| HTML | text/html | .csv | ✅ | ✅ | ✅ | ✅ |
| XML | application/xml | .txt | ✅ | ✅ | ✅ | ✅ |
| 일반 텍스트 | text/plain | .pdf | ✅ | ✅ | ✅ | ✅ |
| Markdown | text/markdown | .pdf | ✅ | ❌ text/plain | ❌ text/plain | ❌ text/plain |
| CSV | text/csv | .txt | ✅ | ❌ text/plain | ❌ text/plain | ❌ text/plain |
(스트림 열은 세 가지 확장자 조건을 하나로 합칩니다. 파일명이 없으면 glob이 읽을 정보도 없기 때문입니다.)
콘텐츠 탐지 가능 형식 다섯 가지 — PDF, DOCX, RTF, HTML, XML — 는 20개의 고유 조건 중 20개 모두에서 실제 타입을 맞췄습니다(중복 스트림 실행을 포함하면 하니스 원시 실행 30회 모두 성공). report.txt라고 이름 붙은 PDF는 여전히 PDF였습니다. photo.jpg로 바뀐 DOCX도 여전히 DOCX였고요. 둘 다 파일명이 필요 없었습니다. 그렇다고 이 다섯 형식이 모두 고정 바이트 시그니처만 쓴다는 뜻은 아닙니다. PDF와 RTF는 인식 가능한 헤더가 있고, DOCX는 ZIP 기반 컨테이너이며, HTML/XML은 마크업이나 루트 콘텐츠로 감지됩니다. 이 테스트 세트에서는 거짓 확장자가 이기지 못했습니다.
그다음은 텍스트 계열입니다. Markdown은 .md 확장자가 보이게 읽힐 때만 text/markdown으로 판별됐습니다. 이름을 바꾸거나, 확장자를 지우거나, 스트림으로 보내면 이번 테스트에서는 text/plain으로 떨어졌습니다. CSV도 이 작고 의도적으로 제한된 그리드에서는 비슷했습니다. text/csv는 .csv glob에서만 나왔습니다. 고유 조건 기준으로 보면 Markdown과 CSV는 네 가지 조건 중 하나에서만 각자의 구체 타입으로 인식됐고, 일반 텍스트는 이미 text/plain이었으니 무너질 것이 없었습니다. 원시 48회 실행 기록은 반복성 기록으로는 유용하지만, 분모를 키우는 근거는 아닙니다.
여기서 Tika에 유리한 한 가지는 분명합니다. 거짓 확장자가 이기지도 못합니다. .pdf로 바꾼 Markdown 테스트 파일은 application/pdf가 아니라 text/plain으로 돌아왔습니다. Tika는 거짓말을 믿지 않았고, 다만 진실을 확인하지 못했을 뿐입니다. 부모 타입으로 내려가는 것은 틀린 타입을 자신 있게 말하는 것보다 훨씬 낫습니다. 그리고 text/markdown이 text/plain의 문서화된 하위 타입이라는 점을 생각하면, 이 fallback은 임의적이 아니라 원칙적입니다.
CSV에는 하나의 주의점이 있습니다. Tika에는 통계 기반 CSV 감지기가 있고, 파싱 시점에는 TextAndCSVParser가 X-TIKA:Parsed-By 체인에 나타나는 것으로 확인됐습니다. 그런데 제가 만든 작은 2열 3행 그리드는 text/csv가 아니라 text/plain으로 처리됐습니다. 이것은 아주 작은 테스트 파일에서 얻은 단일 관찰입니다. 더 큰 CSV나 따옴표가 포함된 CSV라면 감지기가 반응할 수도 있습니다. CSV 콘텐츠 감지가 망가졌다고 주장하는 게 아니라, 이 그리드에서는 확장자가 text/csv를 만들어 냈다는 점을 말하는 것입니다.
실제 업로드 파이프라인에서 왜 중요한가
실제 상황은 업로드 라우터입니다. 사용자가 파일을 올리면 타입별로 분기한다고 합시다. PDF는 청구서 파서로, 스프레드시트는 원장(importer)으로, 나머지는 텍스트 인덱스로 보냅니다. 확장자를 믿으면 notes.txt라는 이름의 PDF가 엉뚱한 분기로 들어갑니다. 그건 아직 온건한 경우고, 더 위험한 버전은 친절한 확장자를 달고 온 폴리글롯 파일입니다.
여기서 테스트한 바이너리와 마크업 파일들은 파일명이 사라진 뒤에도 Tika가 내용 기준으로 라우팅했습니다. blob 저장소나 HTTP body 핸들러가 파일명을 버려도 유용하다는 뜻이죠. 다만 이 결과가 Tika의 긴 꼬리(long tail), 애매한 파일, 폴리글롯까지 보장하는 것은 아닙니다. 테스트한 텍스트 계열 파일들은 다르게 동작했습니다. 파이프라인이 파일명을 제거하면 Markdown과 CSV가 text/plain으로 들어왔고, 각자 고유한 미디어 타입에 맞춘 규칙은 더 이상 발동하지 않았습니다. 콘텐츠 탐지가 파일명을 다시 복원해 줄 것이라고 기대하지 말고, 원래 파일명을 사이카(sidecar) 메타데이터로 보관하세요.
심은 내용은 살아남았다. 하지만 --text는 구조를 평평하게 만들었다.
정확도는 두 번째 축이며, 깔끔하게 둘로 나뉩니다. 저는 하나의 정본 문서(제목, 본문 단락 두 개, 글머리표 목록, 번호 목록, 마무리 단락)를 HTML, Markdown, 일반 텍스트, DOCX, PDF, RTF, ODT, XML로 렌더링했고, 또 다른 표 문서를 HTML, Markdown, 텍스트, DOCX, CSV, XML로 만들었습니다. 총 14개의 매체 렌더링입니다. 각 블록에는 zztitle1, zzitem3, zztblcell_beta 같은 고유 토큰이 들어 있으므로, “살아남았다”와 “사라졌다”는 정확한 부분 문자열 검사로 판정할 수 있습니다.
마커 토큰 재현율은 14개 렌더링 모두에서 1.000이었습니다. 심어 둔 토큰이 하나도 빠지지 않았습니다. 태그가 붙은 표 셀도, 리스트 항목도, 제목도 모두 있었습니다. 각 매체별로 워밍업 이후 세 번씩 반복한 결과는 바이트 단위로 동일한 --text 출력을 반환했습니다. 다만 이 오라클은 마커가 없는 문자, 순서, 공백, 유니코드 정규화, 반복 콘텐츠, 링크, 헤더, 각주, 임베디드 객체에 대해서는 아무 말도 하지 않습니다. 블록 존재 여부를 보는 검사일 뿐, 문서 전체 충실도를 증명하는 것은 아닙니다.
평평한 텍스트 출력은 원본 구조의 대부분을 포기합니다.
다음은 HTML 표 문서를 --text로 뽑은 결과입니다.
Tool Throughput
zztblcell_alpha 120
zztblcell_beta 95
탭으로 연결된 줄입니다. 헤더 행이 헤더로 표시되지 않습니다. 그리드도 없고, 탭 말고 셀 경계도 없고, 이것이 한때 <table>이었다는 사실을 알 방법도 없습니다. DOCX 표도 똑같이 평평해집니다.
목록은 좀 더 미묘하고, 원본이 실제로 무엇이었는지에 따라 갈립니다.
| 원본에서 글머리표가 무엇이었나 | 매체 | --text가 돌려주는 것 |
|---|---|---|
문자 그대로의 기호 — 이 렌더링들은 모두 - 를 실제 텍스트로 썼음 | 일반 텍스트, Markdown, RTF, ODT, PDF | Tika가 문자를 그대로 통과시키므로 - 가 살아남음 |
진짜 구조 — HTML의 <li>, DOCX의 List Bullet 스타일 | HTML, DOCX | 마커는 완전히 사라지고 항목 텍스트만 남음: HTML에서는 탭 들여쓰기, DOCX에서는 아무 장식 없는 평문 한 줄 |
Tika는 텍스트로 받지 못한 마커를 다시 그려 주지 않습니다. 같은 내용이지만 출력 모습은 달라집니다.
Markdown 사례는 이를 가장 깔끔하게 보여 줍니다. 파이프 표가 들어 있는 .md 파일을 Tika에 넣으면 파이프가 그대로 돌아오는데, 겉보기에는 구조 보존처럼 보일 수 있습니다. 하지만 아닙니다. Tika는 그것을 텍스트로 파싱해서 바이트를 그대로 돌려줬을 뿐입니다. 그 표를 이해한 것은 아무것도 없었습니다.
따라서 측정된 계약은 더 좁습니다. 심어 둔 모든 마커는 살아남았지만, --text는 타입이 있는 요소나 다시 재구성 가능한 표 그리드를 보존하지 않았습니다. 이것을 파서 결함이라고 부르면 핵심을 놓칩니다. 평평한 추출은 의도적으로 요소 분류 문제를 피합니다. 대신 그런 타입이 필요한 downstream 소비자는 만족시킬 수 없습니다. 타입이 있는 블록이나 재구성된 표가 필요하다면, --text는 스택의 한 구성요소일 뿐 스택 전체가 아닙니다. Tika의 다른 핸들러는 더 많은 구조를 드러낼 수 있지만, 이번 실행 범위 밖이었습니다.
여기서의 모든 충실도 수치에는 늘 같은 주의점이 있습니다. 하나의 머신, 하나의 버전, 통제된 합성 테스트 파일에서 나온 값이라는 점입니다. 태그된 블록이 출력에 존재했음을 보여 줄 뿐, 지저분한 실제 코퍼스에서 문자 단위 정확도를 보장하지는 않습니다.
메타데이터: 정규화는 잘되고, 멋대로 지어내지는 않는다

메타데이터 레이어가 있는 모든 매체에 알려진 author, title, creation-date 값을 심어 두고, 무엇이 돌아오는지 확인했습니다.
| 매체 | author → dc:creator | title → dc:title | created → dcterms:created |
|---|---|---|---|
HTML (<meta name=author>, <title>) | ✅ | ✅ | 내장되지 않음 |
| DOCX (core properties) | ✅ | ✅ | ✅ 정확히 2021-03-15T09:30:00Z |
| PDF (info dict) | ✅ | ✅ | 존재하긴 하지만 생성기가 자체적으로 찍은 타임스탬프였음 — 점수화하지 않음 |
ODT (meta.xml) | ✅ | ✅ | ✅ 정확히 2021-03-15T09:30:00Z |
| TXT / MD / CSV / RTF / XML | 메타데이터 레이어 없음 | — | — |
author와 title은 메타데이터가 있는 4개 중 4개 매체에서 모두 복원됐고, 더 중요한 점은 정규화되어 들어온다는 것입니다. HTML의 <meta name="author">, DOCX의 core property, PDF의 /Author 항목, ODT의 dc:creator 요소가 모두 같은 dc:creator 키로 들어옵니다. 소비자는 네 개가 아니라 하나만 작성하면 됩니다.
created는 솔직히 조금 흔들립니다. DOCX와 ODT는 제가 넣은 정확한 2021년 타임스탬프를 돌려줬습니다. PDF도 생성 날짜를 돌려주긴 했지만, 그건 제가 의도한 값을 넣은 것이 아니라 생성 라이브러리가 빌드 시점에 찍은 날짜였으므로, 존재는 인정하되 복원된 것으로는 점수화하지 않았습니다. 그리고 메타데이터 레이어가 없는 형식들은 아무것도 내놓지 않았습니다. 그게 맞는 답이니까요. Tika는 본문 텍스트에서 저절로 author를 추측하지 않습니다.
일부러 망가뜨려 보기, 그리고 거기서 나오는 분류 요령
네 가지 적대적 입력을 준비했습니다. 0바이트 파일. 본문이 잘려 나간 정상 PDF 헤더. 중간이 끊긴 DOCX ZIP. 그리고 멀티바이트 문자가 들어 있지만 BOM도 없고 인코딩 선언도 없는 UTF-8 파일. 이것들은 Tika의 임계값이 아니라 로컬 테스트 파일 형태입니다.
생성된 테스트 파일, 원시 JSON, jar 체크섬, 환경 매니페스트를 뒷받침하는 하니스는 여기 공개 링크로 제공되지 않았습니다. 따라서 외부 독자는 정확한 분모를 독립적으로 재현할 수 없습니다. 아래 표는 제3자가 검증한 증거가 아니라 보고된 관찰치로 보셔야 합니다.
| 입력 | --text / --json | 발생한 오류 | --detect |
|---|---|---|---|
| 0바이트 파일 | 종료 1, stdout 비어 있음 | ZeroByteFileException: InputStream must have > 0 bytes | 종료 0 → 파일명이 있으면 text/plain, 스트림이면 application/octet-stream |
| 잘린 PDF | 종료 1, stdout 비어 있음 | TikaException: TIKA-198: Illegal IOException from PDFParser | 종료 0 → application/pdf |
| 잘린 DOCX | 종료 1, stdout 비어 있음 | POI 치명적 오류: "XML document structures must start and end within the same entity" | 종료 0 → OOXML 타입 |
| UTF-8, BOM 없음, 선언 없음 | 종료 0 | 없음 | 종료 0 → text/plain, charset UTF-8 |
추출은 크게 실패하며, 이런 실패는 바깥 모양이 같습니다. 0바이트 파일, 잘린 PDF, 잘린 DOCX는 모두 예외와 종료 코드 1, 빈 stdout을 남겼습니다. CLI는 실패를 조용한 빈 결과로 삼키지 않습니다. 이 경우 프로세스는 안전합니다. 멈추지도 않고, 세그폴트도 없지만, 호출자는 빈 문자열만 볼 게 아니라 종료 상태와 stderr를 확인해야 합니다.
탐지와 파싱은 분리되어 있습니다. 잘린 바이너리 두 개 모두에서 --detect는 온전한 앞부분 내용을 바탕으로 기대한 타입을 종료 0으로 반환했고, 그다음 파서가 깨진 본문에서 실패했습니다. 따라서 파이프라인은 파싱 실패 전후에 탐지를 별도 분류 신호로 쓸 수 있습니다. detect-first가 기본값으로 좋은지는 배포 방식에 달렸습니다. 이번 테스트는 detect-first와 parse-only를 벤치마크하지 않았고, 새 CLI JVM 두 번 띄우는 방식이 대량 처리에서는 손해일 수도 있습니다.
문자셋 탐지는 잘 작동합니다. BOM도 없고 인코딩 선언도 없는 UTF-8 파일은 UTF-8로 디코딩되었고 日本語テスト가 그대로 들어왔습니다. 다만 메타데이터 딕셔너리를 읽는 분들께 한 가지 주의가 있습니다. 순수 ASCII 테스트 파일은 charset=ISO-8859-1로 보고되는데, ASCII 바이트만 놓고 보면 UTF-8과 구분되지 않습니다. 놓친 게 아니라 동률인 셈입니다.
Tika와 unstructured: 같은 파일 형식, 다른 역할
둘 다 같은 연구 세션에서 다뤘지만, 이것은 대칭적인 벤치마크가 아니라 출력 계약의 분류입니다. 도구들은 서로 다른 결과 기준으로 점수를 받았습니다.
관련 리뷰: Unstructured 리뷰.
| Apache Tika | unstructured | |
|---|---|---|
| 무엇을 기준으로 측정했나 | 콘텐츠 충실도: 뭔가가 빠졌는가? | 요소 분류 충실도: 각 블록이 맞는 타입으로 분류됐는가? |
| 결과 | 14개 렌더링 모두에서 심은 마커가 전부 존재 | 별도의 분류 테스트에서 일반 텍스트 표는 Table 재현율 0.000, 동사가 들어간 한 제목은 narrative text로 분류됨 |
| 반환된 타입 있는 요소 | 없음 — 구조가 전혀 돌아오지 않음 | Title, NarrativeText, ListItem, Table — 바로 Tika가 하지 않는 일 |
| OCR | 내 호스트에서 차단됨, tesseract 없음 | 내 호스트에서 차단됨, tesseract 없음 |
타입 있는 요소와 관측된 분류 오류가 있는 출력. downstream 소비자가 무엇을 필요로 하느냐에 따라 고르시면 됩니다. 검색 인덱스나 LLM 컨텍스트 윈도우라면 평평한 텍스트로 충분할 수 있습니다. 요소 타입에 따라 분기한다면 Tika의 --text 경로는 그 계약을 제공할 수 없습니다.
둘 다 스캔 문서 수치는 없습니다.
장점과 단점
장점
- 콘텐츠 타입 탐지가 5개 콘텐츠 탐지 가능 파일에서 거짓 파일명을 20/20개의 고유 조건에서 무시했고, 중복 스트림 실행도 같은 결과를 보임.
- 심어 둔 모든 마커가 14개 모든 매체 렌더링에서 살아남았고, 태그된 표 셀과 리스트 항목도 모두 보존됨.
- 세 번의 로컬 재실행에서 반복 가능: 이 환경 내에서는 각 매체가 바이트 단위로 동일한 텍스트를 반환함.
- 형식 간 메타데이터 정규화가 잘 됨 — 소스 형식이 달라도
dc:creator/dc:title/dcterms:created로 통일되며, 메타데이터가 있는 4개 매체 모두에서 복원됨. - 내가 테스트한 형식들에 대해서는 실제로 의존성이 거의 없음: PDF 텍스트 레이어, DOCX, ODT, RTF, HTML은 외부 바이너리 없이 jar 하나로 파싱됨.
- OpenJDK 26에서도 깔끔하게 실행됨 — LTS 전용 제약 없음.
- 잘린 바이너리에서도 탐지가 맞게 유지되어(종료 0) 파싱 실패 시 믿을 만한 분류 신호를 제공함.
- Apache-2.0, 성숙했고, 활발히 유지 관리됨.
단점
- Markdown과 CSV의 식별은 전적으로 파일명 확장자에 의존함; 시그니처가 없는 18개 셀 중 10개는 파일명이 사라지거나 틀리면
text/plain으로 무너짐. --text는 요소 타입을 반환하지 않음; 표 그리드는 탭으로 연결된 줄로 평탄화되고 구조적 리스트 마커는 사라짐.- 빈 입력과 손상된 입력에서는 예외를 던짐; 이 두 경우는 추출 호출만으로는 동일하게 보임.
- CLI 모드에서는 67MB jar와 호출당 JVM 콜드 스타트가 있음.
- OCR과 스캔 이미지 PDF는 여기서 전혀 테스트하지 않음 — tesseract와 poppler가 없어서 그 경로에 대해 어떤 주장도 하지 않음.
- 여기의 모든 숫자는 한 머신, 한 버전의 합성 정답 기준임. 실제 코퍼스 정확도, 암호화 파일, 임베디드/재귀 문서, 대규모 처리량은 측정하지 않았음.
누구에게 적합하고, 누구에게는 아닌가
Tika는 이미 손에 있는 파일을 기계가 인덱싱할 수 있는 텍스트와 메타데이터로 바꿔야 할 때 잘 맞습니다. 검색 인덱싱, e-discovery, 아카이브 처리, LLM에 넣을 코퍼스 준비, 업로드 파이프라인의 콘텐츠 타입 검증 계층 등입니다. 더 똑똑한 도구 앞단에서 first-stage 분류와 정규화 단계로 쓰기 좋습니다. 테스트한 타입을 감지하고, 평평한 텍스트를 추출한 뒤, 파이프라인이 놓치면 안 되는 콘텐츠에 대해서는 명시적인 체크와 함께 넘기는 식이죠.
타입 있는 요소, 재구성된 표, 문서 레이아웃이 필요하다면 — 혹은 적어도 --text에만 기대지 말아야 합니다. 문서가 스캔본이라면, tesseract를 설치하고 직접 수치를 내기 전까지는 쓰지 않는 편이 낫습니다. 저는 그 수치가 없습니다. 대량 작업이라면 CLI 대신 라이브러리나 서버 모드로 대표 문서를 벤치마크하세요. 이번 작은 파일 하니스에서는 프로세스 시작 비용이 눈에 보였지만, 처리량과 자원 비용은 측정하지 않았습니다.
많이들 놓치는 한 가지: 저장 계층이 파일명을 지워 버리고 Markdown이나 CSV를 다룬다면, Tika가 그것들을 일반 텍스트와 구분해 줄 것이라고 기대하지 마세요. 원래 이름을 보관하세요.
대안과 Thunderbit의 위치
먼저 공정하게 정리하면, 여기서의 비교는 품질이 아니라 입력에 관한 것입니다. Tika는 파일을 파싱하는 무료 Apache-2.0 셀프호스팅 툴킷입니다. 디스크나 버킷에 이미 있는 파일용이죠. 페이지를 가져오지도 않고, JavaScript도 실행하지 않고, 봇 방지도 처리하지 않으며, 그런 척도 하지 않습니다.
바로 그 경계에서 관리형 웹 추출 서비스, 우리 Thunderbit을 포함해, 아키텍처에 들어올 수 있습니다. Thunderbit은 라이브 페이지를 가져오고, Tika는 이미 소유하고 있는 파일을 파싱합니다. 이 글은 그런 서비스와 Tika를 직접 벤치마크하지 않았고, 같은 입력에 대한 대체재도 아닙니다.
깔끔한 구분은 이렇습니다. 내 소유의 문서에는 Tika, 가져와야 하는 웹 페이지에는 관리형 추출 API. 실제로는 둘을 함께 쓰는 파이프라인도 많습니다. 웹 쪽은 크롤링과 추출, 돌아온 PDF와 DOCX 첨부파일은 Tika로 처리하는 식이죠.
오픈소스 생태계 전반을 비교 중이라면, 제가 정리한 오픈소스 스크래퍼 종합 비교, GitHub에서 가장 유용한 스크래핑 프로젝트 모음, 브라우저 기반 Markdown 접근을 다룬 Crawl4AI 리뷰, 그리고 더 넓은 범위의 스크래핑 도구 총정리도 참고하실 수 있습니다. 노코드 경로로는 AI로 웹사이트를 스크래핑하는 방법도 있습니다.
결론
Apache Tika를 써야 할까요? 네, 당신의 일이 이질적인 파일들을 평평한 텍스트와 정규화된 메타데이터로 바꾸는 것이고, 파이프라인이 절대 놓쳐선 안 되는 필드나 마커를 검증할 수 있다면 그렇습니다.
이번 실행에서 가장 강했던 부분은 탐지기였습니다. 파일명 없는 스트림까지 포함해, 콘텐츠 탐지 가능 파일 다섯 가지에서 20/20개의 고유 조건 모두 기대한 타입을 돌려줬습니다. 심은 마커는 14개 렌더링 전체에서 모두 살아남았고, 출력은 세 번의 로컬 재실행에서도 바이트 단위로 같았습니다. 쓸모 있는 증거입니다. 그래도 합성된 증거라는 점은 변하지 않습니다. 이번 JDK에서 jar 하나로, 테스트한 비-OCR 경로에는 외부 바이너리 없이 실행된 점도 배포를 놀라울 만큼 단순하게 만들었습니다.
하지만 기대치는 정확히 맞춰야 합니다. 넣는 모든 표는 탭으로 연결된 줄로 돌아옵니다. 구조적 리스트 마커는 모두 사라집니다. Markdown과 CSV는 파일명이 사라지는 순간 정체성을 잃습니다. 빈 파일과 손상된 파일은 같은 형태의 실패를 내고, 둘을 구분하려면 별도의 detect 호출이 필요합니다. 그리고 많은 Tika 사용자가 가장 궁금해하는 OCR에 대해서는 저는 보여 드릴 게 없습니다. 실행할 수 없었고, 추정도 하지 않겠습니다.
그 범위 안에서는 Tika가 아주 믿음직스럽게, 화려하지 않게 일합니다. 상자에 붙은 라벨이 아니라 바이트를 읽습니다. 다만 바이트가 어떤 모양이었는지는 묻지 마세요.
웹 데이터 추출용 Thunderbit 사용해 보기 Get Started Free
자주 묻는 질문
Apache Tika는 확장자가 틀려도 파일 형식을 제대로 감지하나요?
이번에 테스트한 콘텐츠 탐지 가능 파일 다섯 가지에 대해서는 그렇습니다. PDF, DOCX, RTF, HTML, XML은 오해를 불러일으키는 확장자, 확장자 없음, 파일명 없는 스트림을 포함한 20개의 고유 논리 조건 모두에서 기대한 미디어 타입으로 판별됐습니다(중복 스트림 실행을 포함하면 원시 실행 30회). .txt로 이름 붙인 PDF도 여전히 application/pdf로 감지됐습니다. 반면 Markdown과 작은 CSV 테스트 파일은 파일명 정보에 의존했고, 그것이 없거나 틀리면 text/plain으로 떨어졌습니다.
Tika가 표와 문서 구조를 보존하나요?
이번에 테스트한 --text 모드에서는 아닙니다. 표 그리드는 셀이나 헤더 의미 없이 탭으로 연결된 줄로 돌아왔고, HTML <li>나 DOCX List Bullet 스타일 같은 구조적 리스트 마커는 사라졌습니다. 심어 둔 모든 마커는 14개 렌더링 모두에서 살아남았지만, 그것이 완전한 콘텐츠 충실도를 증명하는 것은 아니고, --text는 요소 타입을 제공하지도 않습니다. 타입이 있는 요소나 재구성된 표가 필요하다면 다른 Tika 출력 핸들러를 시험해 보거나, 다른 도구를 함께 쓰세요.
Apache Tika로 스캔한 PDF에 OCR을 할 수 있나요? Tika는 Tesseract를 통해 OCR을 지원하지만, 저는 테스트하지 않았고, 여기의 어떤 결과도 그 기능에 대한 주장이 아닙니다. 제 테스트 호스트에는 Tesseract와 poppler가 없어서 OCR과 스캔 이미지 경로는 시작되기 전에 막혔습니다. 이번 테스트에는 OCR 수치가 전혀 없습니다. OCR이 사용 사례라면 tesseract를 설치하고 직접 벤치마크하세요. 그 부분은 여기서 검증되지 않았다고 보시면 됩니다.
Tika는 비어 있거나 손상된 파일을 어떻게 처리하나요?
조용히 넘기지 않고 크게 실패합니다. 0바이트 파일은 ZeroByteFileException을 던지고, 잘린 PDF는 PDFParser에서 TikaException을 던지며, 잘린 DOCX는 POI XML 오류를 냅니다. 세 경우 모두 종료 코드 1과 빈 stdout을 남기므로, 추출 호출만 봐서는 빈 파일과 손상 파일을 구분할 수 없습니다. 하지만 탐지는 강합니다. --detect는 잘린 바이너리 두 개 모두에서 올바른 타입을 종료 0으로 반환했기 때문에, 파싱 비용을 들이기 전에 유용한 분류 단계가 됩니다.
이번 Tika 테스트에서 다루지 않은 것은 무엇인가요? 네 가지를 명시적으로 다루지 않았습니다. OCR과 스캔 이미지(막혔고, 테스트 안 함). 실제 코퍼스 정확도 — 모든 결과는 심은 마커 토큰을 사용한 통제된 합성 테스트 파일이므로, 지저분한 실제 문서에서의 정확도라기보다 알려진 라벨에 대한 충실도를 측정한 것입니다. 자원 비용, 처리량, 피크 메모리도 측정하지 않았습니다. 그리고 “1,000개 파일 형식”이라는 주장 중 긴 꼬리 부분도요: 저는 대표적인 의존성 없는 9개 형식만 테스트했고, 전체 카탈로그를 다 본 것은 아닙니다. 여기의 모든 것은 Tika 3.3.2, OpenJDK 26.0.1, macOS arm64, 단일 머신 기준입니다.


