AI

[RAG] OCR 대신 LLM — 서버보다 API 호출이 쌌다

jykim23 2026. 8. 5. 23:46
반응형

부제: OCR을 켤지 판단만 하고, 정작 문서를 읽는 건 OCR 엔진이 아니라 LLM에게 시켰다

문서를 RAG에 넣으려면 먼저 텍스트로 바꿔야 한다.
pymupdf 같은 파서로 시도하는 게 제일 싸지만, 스캔본이나 이미지형 PDF는 파서가 텍스트를 거의 못 뽑는다.
이럴 때 OCR이 필요한데, 그 OCR을 뭘로 돌릴지가 문제였다.

OCR이 필요한지는 결과 차이로 안다

파서(pypdf)만으로 될지, OCR까지 필요할지는 미리 알 수 없다. 그래서 판단 기준을 뒀다.

1. pypdf로 페이지별 텍스트를 뽑아본다
2. 한 페이지라도 0자면 → 이미지 기반 PDF로 보고 OCR 필요
3. 샘플 페이지에 가벼운 OCR을 돌려서, len(OCR 결과) > len(pypdf 결과) × 2.0이면 → 파서가 놓친 텍스트가 많다는 뜻이니 OCR 필요
4. 그 외(텍스트가 충분)는 → pypdf 그대로 써도 됨

즉 파서 결과와 OCR 결과가 크게 벌어질수록 "이 문서는 이미지 속에 텍스트가 있다"는 신호다. 이미지 파일은 이 판단 자체를 생략하고 무조건 OCR 필요로 취급한다.

OCR이 필요하면, 진짜 OCR 대신 LLM을 불렀다

여기서부터가 진짜 결정이다. "OCR이 필요하다"고 판단된 문서를, 어떤 OCR로 읽을지 실측 비교했다. 한국어 의료 문서 5페이지 기준이다.

| 항목 | Tesseract | PaddleOCR v5 | EasyOCR | Gemini 2.5 FL | Gemini 3.1 FL |
|---|---|---|---|---|---|
| 속도(전체) | 27초 | 431초 | 731초 | 41초 | 27초 |
| 메모리 | +142MB | +909MB | +554MB | 0 | 0 |
| 한국어 정확도 | 최하 | 최고 | 중간 | 최고 | 최고 |
| 표 구조 | 파괴 | 행 단위 | 파괴 | JSON bbox | 마크다운 표 |
| 비용(100p) | 무료 | 무료 | 무료 | $0.09 | $0.16 |

Tesseract는 빠르지만 정확도가 최하고 표를 다 부순다.
정확도가 높은 PaddleOCR·EasyOCR는 각각 431초·731초(Tesseract의 16~27배)가 걸리고, 메모리도 수백MB를 더 먹는다. 이 정도면 전용 서버를 따로 띄워 상시 운영해야 한다는 뜻이다.

저울질한 건 "OCR 엔진 자체가 무료냐"가 아니라 "이 엔진을 서비스에 얹는 부담이 뭐냐"였다.
PaddleOCR는 API 비용이 0원이지만, 메모리 900MB짜리 컨테이너를 문서가 오든 안 오든 상시 띄워둬야 한다. EasyOCR도 마찬가지고, 처리 시간도 731초로 Tesseract의 27배다.
Gemini 3.1 Flash Lite는 100장당 $0.16가 들지만 상주 인프라가 없다 — 쓴 만큼만 내고, 안 쓰면 0원이다. 속도는 Tesseract급(27초)인데 정확도는 최고, 표까지 마크다운으로 보존한다.

상시 띄워야 하는 무료 서버와, 종량제인데 무료 서버 성능도 넘어서는 API 사이에서 후자를 택했다. 그래서 "OCR이 필요하다"는 판단이 나온 문서는 OCR 엔진이 아니라 LLM(VLM)으로 보내기로 했다.
(운영비를 달러로 직접 비교한 건 아니다 — 상주 컨테이너 부담 자체를 지지 않아도 된다는 점과, 그런데도 품질·속도가 안 밀린다는 점 둘이 겹쳐서 굳이 서버를 안 띄우는 쪽을 택한 것에 가깝다.)

판단은 가볍게, 읽기는 LLM으로 — 판단 도구는 안 바꿨다

정리하면 이 파이프라인엔 OCR이 두 군데서 쓰인다. ①"OCR이 필요한가"를 판단하는 가벼운 OCR(Tesseract, 결과 텍스트는 버리고 길이 비교에만 씀)과, ②실제로 문서를 읽어 텍스트를 뽑는 LLM(Gemini)이다. 둘은 다른 도구고, 비용 계산으로 대체된 건 ②쪽이다.

그런데 ①번 판단용 Tesseract를 원래 lang="kor+eng"로 돌리다가, 어느 시점에 lang="eng"로 낮췄다. 코드에 남은 주석이다.

# eng baseline (i18n): tesseract here is a routing heuristic, not
# the extractor — multilingual OCR is the VLM (Gemini) path.
ocr_text = pytesseract.image_to_string(images[idx], lang="eng")

한국어 사용자가 주력인 서비스인데 판단 도구에서 한국어 지원을 뺐다. 이게 가능했던 이유는 ①이 "얼마나 더 읽히는가"를 재는 자(尺)일 뿐, 실제로 사용자에게 돌아가는 텍스트를 만드는 건 처음부터 끝까지 ②(LLM)이기 때문이다. 판단 도구의 언어 실력이 결과물의 언어 실력을 결정하지 않는다.

판단이 좀 틀려도 되는 이유

판단용 OCR이 한국어(그리고 일본어·중국어·아랍어 같은 비-Latin 문자)를 못 읽으면, 텍스트가 일부만 박혀 있고 나머지가 이미지인 PDF에서 오판할 수 있다. 이미지 속 글자 분량을 실제보다 적게 재서, LLM으로 보내야 할 문서가 pypdf 경로로 새는 경우다.

완전히 텍스트가 없는 페이지(0자)는 이 갭과 무관하다 — 2번 규칙이 언어와 상관없이 무조건 LLM으로 보낸다. 갭이 영향을 주는 범위는 "텍스트가 일부 있고 나머지는 이미지인, 비-Latin 문자가 섞인 PDF"로 좁다.

판단용 언어팩을 다시 설치해 정확도를 올릴 수도 있었다. 하지만 하지 않았다.
판단이 틀려서 싼 경로로 새더라도, 그건 판단 정확도의 문제지 결과 품질의 문제가 아니다 — 최종적으로 텍스트를 만드는 LLM의 실력과는 무관하다. 언어팩을 무더기로 깔아 판단 도구를 보강하는 건, 이미 있는 능력(LLM의 멀티링궐 읽기)을 판단 도구에 중복으로 떠안기는 일이었다.

정리

  • 문서는 먼저 pypdf로 파싱하고, OCR 결과와 크게 다르면(2배 기준) OCR이 필요하다고 본다
  • "OCR이 필요하다"고 판단된 문서를 실제로 읽을 때, OCR 엔진이 아니라 LLM(Gemini)을 썼다 — 무료 OCR 서버는 상주 인프라(메모리 900MB+)가 필요했고, 종량제 API가 품질·속도에서도 안 밀렸다
  • 판단에 쓰는 가벼운 OCR과 실제로 읽는 LLM은 다른 도구다 — 판단 도구의 언어 지원(kor+engeng)을 낮춰도 결과 품질엔 영향이 없었다
  • 판단이 좀 틀려도(비-Latin 부분텍스트 PDF 오판) 그 갭은 판단 정확도에만 갇혀 있어, 판단 도구를 보강하지 않고 그대로 수용했다

OCR을 켤지 말지는 싼 도구로 판단하고, 정작 켜야 할 때는 OCR이 아니라 LLM을 불렀다.
무료라도 상주 서버를 얹는 부담과, 돈은 들어도 쓴 만큼만 내고 성능도 안 밀리는 API — 저울에 올린 건 이 둘이었다.

반응형