왜 space-ocr는 다른 LLM OCR과 다른가: 검증 가능한 구조화 추출
LLM에 직접 OCR을 시키는 방식과 space-ocr의 차이. 추출된 모든 값에 페이지에서 검증한 박스와 match_ratio가 붙고, 그대로 조회 가능한 행으로 저장된다.
영수증이나 청구서 한 장을 GPT-4o, Gemini, Claude에 던지면서 합계와 거래처, 품목 내역을 뽑아 달라고 할 수 있다. 대부분은 그럴듯한 JSON이 돌아온다. 문제는 이걸 대량으로 신뢰하려 할 때 시작된다. 모델이 돌려주는 건 문자열이고, 문자열에는 위치가 없다. 합계가 48,200으로 나왔다면 페이지의 어느 픽셀을 읽은 것일까? 그 숫자가 실제로 문서에 인쇄되어 있었을까, 아니면 모델이 그럴듯한 값을 채워 넣은 걸까? LLM 호출만으로는 페이지를 직접 다시 읽어 보기 전까지 이 질문에 답할 수 없다.
바로 이 간극이 범용 LLM을 OCR 도구로 쓰는 것과 space-ocr를 쓰는 것의 차이 전부다. space-ocr는 LLM에 반대하는 물건이 아니다. 내부적으로는 OCR 엔진(Google Cloud Vision)과 구조화를 담당하는 Gemini를 함께 쓴다. space-ocr가 더하는 것은 모델을 둘러싼 층이다. 반환하는 모든 값을 OCR 엔진이 페이지에서 실제로 본 것과 대조하고, 점수를 매기고, 조회 가능한 행으로 저장한다. 이 두 가지, 곧 검증할 수 있는 값별 출처와 별도 데이터베이스 없이 조회할 수 있는 구조화된 출력이야말로 LLM 호출이 사용자에게 떠넘기는 부분이다.
LLM 직접 OCR vs space-ocr
| LLM 직접 호출 (GPT-4o / Gemini / Claude) | space-ocr | |
|---|---|---|
| 값별 위치 | 없음, 텍스트만 반환 | 모든 값에 박스(0–1000 격자 위의 xmin, ymin, xmax, ymax)와 네 점짜리 방향 사각형 |
| 값별 검증 | 없음, 문자열을 그대로 믿어야 함 | match_ratio: 값의 문자 중 몇 개가 페이지에서 감지된 심볼에 실제로 있었는지, bbox_source 라벨과 함께 |
| 값을 맥락에서 확인 | 문서를 직접 다시 읽어야 함 | 앱에서 셀을 클릭하면 원본 이미지의 해당 영역이 그대로 표시됨 |
| 출력 형태 | 프롬프트에 좌우되어 실행마다 달라지는 JSON | 고정 스키마: fields를 한 번 정의(또는 templateId 전달)하면 업로드마다 한 행으로 저장 |
| 저장과 조회 | 직접 구축해야 함 | 결과는 시트의 행이며 GET /view(where, sort, select, limit, offset)로 조회, OCR 재실행 없음, 과금 없음 |
| 문자 체계 | 모델과 프롬프트에 따라 다름 | 일본어, 한국어, 중국어, 영어 등을 자동 감지, 언어 파라미터 없음 |
| 설정 | 직접 만든 프롬프트, 재시도, 파싱, 검증 파이프라인 | Bearer 키로 HTTPS 호출 한 번 |
'검증됨'이 실제로 무슨 뜻인지 짚어 둔다. 과장하기 쉬운 대목이라서다. 언어 모델은 좌표를 만들어 내지 않는다. 각 값의 텍스트와 단어 토큰 힌트를 반환할 뿐이고, 그다음 엔진이 그 텍스트를 Google Cloud Vision이 페이지에서 감지한 심볼과 한 글자씩 대조한다. 박스는 그 실제 심볼 위에 놓이고, 값에는 match_ratio가 매겨진다. match_ratio는 값의 문자 중 페이지의 심볼에서 찾아낸 비율이다. match_ratio가 높으면 심볼에 매칭된 신뢰할 만한 값이고, 낮으면 bbox_source를 통해 표시되어 사람에게 넘길 수 있다. 이는 모델이 절대 틀리지 않는다는 약속이 아니다. 토큰 힌트는 여전히 어긋날 수 있다. 각 값을 그냥 믿는 대신 페이지와 대조해 점수를 매긴다는 뜻이다.
{
"vendor": {
"value": "ACME Trading Co.",
"bbox": { "xmin": 120, "ymin": 84, "xmax": 512, "ymax": 118 },
"vertices": [
{ "x": 120, "y": 84 }, { "x": 512, "y": 84 },
{ "x": 512, "y": 118 }, { "x": 120, "y": 118 }
],
"match_ratio": 1.0,
"bbox_source": "vision_symbol_match"
}
}박스는 이미지 크기와 무관한 0–1000 격자 위에서 반환되므로, 화면에 그릴 때는 픽셀로 환산한다: pixel_x = xmin / 1000 * image_width. 네 점짜리 사각형은 기울거나 회전된 스캔을 따라가며, 좌상단, 우상단, 우하단, 좌하단 순서로 정렬된다. 모델 응답만으로는 이 중 어느 것도 얻지 못하므로, 필요한 감사 추적은 전부 손으로 짜맞춰야 한다.
값에서 조회 가능한 표로
LLM 직접 호출은 JSON에서 끝난다. 그걸 여전히 저장해야 하고, '이번 분기에 40,000을 넘는 청구서는 어느 것인가?'를 묻고 싶어지는 순간 먼저 데이터베이스와 조회 계층부터 구축하게 된다. space-ocr에서는 결과가 이미 고정 스키마를 가진 시트의 한 행이므로, 조회는 API 호출 한 번이다. GET /view에 where=total>=40000, sort=-invoice_date, select=vendor,total, 그리고 페이징용 limit과 offset을 붙이면 된다. 서버 측에서 실행되고, OCR을 다시 돌리지 않으며, 과금되지 않는다. 시트는 CSV로 내보낼 수 있다(BOM이 붙은 UTF-8이라 일본어, 한국어, 중국어 텍스트와 통화가 Excel에서 제대로 열리고, 품목 배열은 각자의 행으로 펼쳐진다).
LLM 직접 호출이 더 나은 경우
범용 LLM은 한 번만 읽으면 되거나, 느슨한 요약이 필요하거나, 문서가 무엇을 뜻하는지 추론해야 할 때 맞는 선택이다. '이 계약서는 무슨 내용인가?'는 모델에게 물을 질문이지, 좌표까지 붙은 OCR에 물을 질문이 아니다. 문서를 대량으로 처리하면서 각 값이 검증 가능하고, 일관되게 구조화되고, 조회 가능해야 한다면 space-ocr를 쓰면 된다. 매입 채무 자동화, 경비 정산, 명함을 CRM에 넣기, 쌓인 영수증 디지털화 같은 일이다. 정직하게 말하면 space-ocr는 검증과 저장 계층을 얹은 LLM 기반 OCR이지, 모델 자체와 경쟁하는 물건이 아니다.
과금은 스캔당 종량제이며, 선택형 월정액 플랜과 매월 제공되는 무료 스캔이 있고, 스캔 결과가 비어 있으면 과금하지 않는다. 현재 요금은 요금 페이지에서 확인할 수 있다.