포스트

2026년 7월, “OCR → Document AI → LLM 구조화 추출” 스택이 재정의됐다: 표·PDF를 프로덕션에 넣는 현실적인 기준

2026년 7월, “OCR → Document AI → LLM 구조화 추출” 스택이 재정의됐다: 표·PDF를 프로덕션에 넣는 현실적인 기준

들어가며

현업에서 문서 자동화가 막히는 지점은 “텍스트를 읽느냐”가 아니라, (1) 레이아웃(heading/paragraph/reading order) 유지, (2) 표 구조(Table Structure Recognition) 재구성, (3) 최종 스키마에 맞춘 안정적인 structured extraction(JSON), 그리고 (4) 추출 결과를 원문 좌표로 trace/backlink 하는 “검증 가능성”입니다. 특히 스캔 PDF + 복잡한 표(merged cell, multi-line header, footnote) 조합은 여전히 실패율이 높고, LLM을 얹으면 “그럴듯한 JSON”이 나오지만 근거(좌표/셀 출처)가 없는 hallucination이 운영을 망칩니다(커뮤니티에서도 반복적으로 언급). (reddit.com)

언제 쓰면 좋은가

  • 대량 문서(수천~수만 페이지/일)에서 표/폼/계약서/청구서를 스키마로 적재해야 하고, 사람이 최종 검수하더라도 검수 비용을 10x 줄이고 싶을 때
  • OCR 품질이 흔들리는 환경(스캔본, 워터마크, 도장/서명)에서 layout + 표 + 키값을 같이 뽑아야 할 때
  • RAG 이전 단계로 PDF → Markdown/구조화가 필요하고, downstream QA 성능을 올리고 싶을 때(문서 변환 품질이 RAG 품질을 좌우한다는 평가 연구가 등장). (arxiv.org)

언제 쓰면 안 되는가

  • “정확히 정해진 템플릿”만 다루고 volume이 크지 않다면: 규칙 기반/템플릿 기반이 더 싸고 안정적
  • 규제/감사 때문에 좌표 기반 증빙이 필수인데, 사용하는 LLM/툴이 provenance(값→셀/박스 연결)를 제공하지 않는다면: 먼저 layout/table 모델 쪽을 강화해야 함
  • 비용 민감한 경우(특히 multi-page PDF를 VLM로 통째로 보내는 방식): OCR/레이아웃 단계에서 chunking + 후보 영역 좁히기가 선행돼야 함

🔧 핵심 개념

1) 2026년식 파이프라인: “Layout-first + LLM schema mapping”

최근 제품/문서에서 공통적으로 보이는 방향은 다음 3단 분해입니다.

  1. Layout Parse (문서 구조 복원)
    • 페이지를 block 단위(heading, paragraph, header/footer, table, figure 등)로 분해하고 reading order를 잡음
    • Google Document AI의 Gemini 기반 layout parser는 OCR 모델 + Gemini를 결합해 “정밀한 구조화”를 강조합니다. (docs.cloud.google.com)
    • Microsoft Document Intelligence는 layout 모델 출력에 대해 Markdown 형태 출력 및 좌표 정보를 제공하는 샘플들이 존재합니다. (learn.microsoft.com)
    • Mistral OCR 4는 bounding box, block label, confidence 등을 전면에 내세웁니다. (docs.mistral.ai)
  2. Table Structure Recognition (TSR) / 표 재구성
    • 표는 “텍스트”가 아니라 격자 구조 + header 계층 + merged cell + footnote의 조합
    • 흥미로운 포인트: 최신 연구 중에는 딥러닝 대신 OpenCV 기반 휴리스틱 + OCR로 고정밀 TSR을 만드는 접근(SPARTAN)이 소개됩니다. GPU 없이도 실무 품질을 얻고, 마지막에 LLM을 “스키마 매핑”에만 제한적으로 사용합니다. 이게 비용/설명가능성 측면에서 꽤 실전적입니다. (nature.com)
    • AWS Textract도 표에서 merged cell, column header, summary cell 같은 메타를 직접 다루는 쪽으로 문서가 정리되어 있습니다(즉, “표는 별도 객체”로 취급). (docs.aws.amazon.com)
  3. LLM Structured Extraction (스키마로의 안정적 매핑)
    • LLM은 “문서 이해”에 강점이 있지만, 프로덕션에서 필요한 건 엄격한 JSON schema + 재시도/검증 루프입니다.
    • 2026년 문서 이해 서베이에서도 MLLM 기반 접근이 OCR-free/OCR-based로 갈리며, robustness와 hallucination, fine-grained perception이 핵심 과제로 정리됩니다. (aclanthology.org)
    • 결론적으로, 실무에서는 “LLM이 문서를 처음부터 끝까지 읽고 JSON 생성”보다는 Layout/Tables에서 근거를 뽑고, LLM은 mapping/normalization에 집중시키는 쪽이 안전합니다.

2) OCR-free vs OCR-based: 무엇이 다른가

  • OCR-based: 먼저 텍스트/박스를 얻고, 그 위에서 entity extraction.
    • 장점: 좌표/근거 연결이 쉽고, 오류 원인 분석이 가능
    • 단점: OCR 오류가 downstream으로 전파
  • OCR-free(혹은 VLM end-to-end): 이미지→바로 구조/텍스트 생성
    • 장점: 복잡한 시각 단서(폰트, 선, 레이아웃)를 한 번에 반영 가능
    • 단점: “그럴듯한 생성”이 나오기 쉬워 검증/감사가 어렵고, 표의 provenance가 약해지기 쉬움(현업이 싫어하는 지점)

💻 실전 코드

아래 예제는 “스캔/비정형 PDF(표 포함)”를 1) Layout/Table 파싱 → 2) LLM 스키마 매핑 → 3) 검증/재시도로 구성합니다. 핵심은 LLM에게 원문 전체를 주지 않고, layout parser가 뽑은 table/paragraph chunk만 주는 것입니다.

시나리오: 공급업체가 보내는 월간 정산 PDF에서 invoice_id, vendor_name, period, line_items(표)를 추출해 DB 적재.

0) 의존성/환경

1
2
3
4
python -m venv .venv
source .venv/bin/activate
pip install pydantic httpx tenacity python-dotenv
# (각 클라우드/벤더 SDK는 사용하는 서비스에 맞게 추가)

.env:

1
2
3
4
GOOGLE_PROJECT_ID=...
GOOGLE_LOCATION=...
GOOGLE_PROCESSOR_ID=...     # Document AI layout parser processor
OPENAI_API_KEY=...          # 스키마 매핑용 LLM (예시)

1) Layout/Table 파싱 (Google Document AI Layout Parser 예시)

Google은 복잡한 PDF에는 OCR parser 대신 layout parser를 권장하고, table annotation을 켤 수 있습니다. (docs.cloud.google.com)
(아래 코드는 “실행 가능한 형태”의 뼈대이며, 실제 호출 파라미터/SDK는 환경에 맞게 조정하세요.)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# file: pipeline_extract.py
import os, json
from dataclasses import dataclass
from typing import Any, Dict, List, Optional

import httpx
from tenacity import retry, stop_after_attempt, wait_exponential
from pydantic import BaseModel, Field, ValidationError

GOOGLE_PROJECT_ID = os.environ["GOOGLE_PROJECT_ID"]
GOOGLE_LOCATION = os.environ["GOOGLE_LOCATION"]
GOOGLE_PROCESSOR_ID = os.environ["GOOGLE_PROCESSOR_ID"]

PDF_PATH = "sample_settlement.pdf"

@dataclass
class Block:
    kind: str                # "paragraph" | "table" | ...
    text: str
    page: int
    bbox: Optional[Dict[str, Any]] = None
    table_html: Optional[str] = None   # 표는 가능한 한 구조(셀/행/열)를 유지한 형태로 보관

def call_layout_parser(pdf_bytes: bytes) -> Dict[str, Any]:
    """
    실제로는 Google Document AI SDK를 권장.
    여기서는 'layout parser 결과 JSON을 얻는다'는 형태로 추상화.
    """
    raise NotImplementedError("Use Document AI SDK / REST for your environment")

def blocks_from_layout(doc: Dict[str, Any]) -> List[Block]:
    blocks: List[Block] = []

    # 포인트: LLM에게 원문 PDF를 통째로 주지 말고,
    # layout 단계에서 paragraph/table 단위로 '증거 조각'을 만든다.
    for page_idx, page in enumerate(doc.get("pages", []), start=1):
        # paragraphs
        for p in page.get("paragraphs", []):
            blocks.append(Block(
                kind="paragraph",
                text=p.get("text", "").strip(),
                page=page_idx,
                bbox=p.get("bbox"),
            ))

        # tables (가능하면 table을 HTML/CSV/Markdown 등으로 직렬화)
        for t in page.get("tables", []):
            blocks.append(Block(
                kind="table",
                text=t.get("text", "").strip(),
                page=page_idx,
                bbox=t.get("bbox"),
                table_html=t.get("html"),  # 예: 셀 구조 포함
            ))
    return blocks

2) LLM로 “스키마 매핑”만 수행 (JSON schema 강제 + 재시도)

핵심은 LLM 입력을 (a) 표/문단 블록 + (b) 목표 스키마 + (c) 추출 규칙으로 제한하는 겁니다.
또한 결과는 pydantic으로 검증하고 실패 시 재시도합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
class LineItem(BaseModel):
    item_name: str
    quantity: float
    unit_price: float
    amount: float

class Settlement(BaseModel):
    invoice_id: str
    vendor_name: str
    period: str  # "2026-06" 같은 정규화된 값
    currency: str = Field(default="USD")
    line_items: List[LineItem]
    evidence: Dict[str, Any]  # 각 필드의 page/bbox 등 provenance

@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=1, max=8))
def llm_schema_map(blocks: List[Block]) -> Settlement:
    # 입력은 "관련성이 높은 table/paragraph"만 추리면 더 좋다.
    tables = [b for b in blocks if b.kind == "table" and (b.table_html or b.text)]
    paras  = [b for b in blocks if b.kind == "paragraph" and b.text]

    prompt = {
        "task": "Extract settlement fields into strict JSON.",
        "rules": [
            "Do NOT guess. If a value is missing, return empty string and add a warning in evidence.",
            "All numeric fields must be parseable as floats.",
            "period must be normalized to YYYY-MM if possible.",
            "For each top-level field, include evidence: page and bbox if available, and whether it came from a table cell or paragraph."
        ],
        "schema": Settlement.model_json_schema(),
        "inputs": {
            "tables": [
                {"page": t.page, "bbox": t.bbox, "table_html": t.table_html, "fallback_text": t.text[:2000]}
                for t in tables
            ],
            "paragraphs": [
                {"page": p.page, "bbox": p.bbox, "text": p.text[:1000]}
                for p in paras
            ],
        },
    }

    # 예시: OpenAI Responses API 호출로 가정(프로젝트에 맞게 교체)
    # 실제로는 "structured output" / "json_schema" 강제 기능을 쓰는 편이 운영에 유리.
    import openai
    client = openai.OpenAI()

    resp = client.responses.create(
        model="gpt-4.1-mini",
        input=[{"role": "user", "content": json.dumps(prompt, ensure_ascii=False)}],
        # 실제 구현에서는 json_schema 강제 옵션 사용 권장
    )

    text = resp.output_text
    data = json.loads(text)  # 모델이 JSON만 출력하도록 강제해야 안전
    try:
        return Settlement.model_validate(data)
    except ValidationError as e:
        # 검증 실패는 재시도 트리거(tenacity)
        raise RuntimeError(f"Schema validation failed: {e}") from e

3) 실행 및 예상 출력

1
2
3
4
5
6
7
8
9
10
def main():
    pdf_bytes = open(PDF_PATH, "rb").read()
    doc = call_layout_parser(pdf_bytes)
    blocks = blocks_from_layout(doc)

    settlement = llm_schema_map(blocks)
    print(settlement.model_dump_json(indent=2, ensure_ascii=False))

if __name__ == "__main__":
    main()

예상 출력(요지):

1
2
3
4
5
6
7
8
9
10
11
12
13
{
  "invoice_id": "INV-2026-06-1138",
  "vendor_name": "ACME Logistics LLC",
  "period": "2026-06",
  "currency": "USD",
  "line_items": [
    {"item_name": "Linehaul", "quantity": 12, "unit_price": 85.5, "amount": 1026.0}
  ],
  "evidence": {
    "invoice_id": {"page": 1, "bbox": {"x1":0.12,"y1":0.08,"x2":0.32,"y2":0.10}, "source":"paragraph"},
    "line_items": {"page": 2, "bbox": {"x1":0.05,"y1":0.22,"x2":0.95,"y2":0.78}, "source":"table"}
  }
}

⚡ 실전 팁 & 함정

Best Practice

1) LLM을 “최종 생성자”가 아니라 “정규화/매핑기”로 제한

  • Layout/Table 단계에서 구조를 최대한 복원하고(표는 셀 단위), LLM은 schema mapping + normalization만 담당시키면 hallucination을 크게 줄입니다.
  • 휴리스틱 기반 TSR(SPARTAN) 같은 접근이 다시 주목받는 이유가 “비용/설명가능성/튜닝 용이성”입니다. (nature.com)

2) provenance(근거) 설계를 1순위로

  • 값마다 page + bbox + source(table cell/paragraph)를 붙이세요.
  • 커뮤니티에서 지적하듯 “JSON은 예쁜데 출처 추적이 안 되면” 운영에서 바로 막힙니다. (reddit.com)

3) PDF → Markdown 변환을 RAG 품질 관점에서 측정

  • “텍스트 잘 뽑힘”이 아니라 “downstream QA 정확도”로 평가하는 연구가 나왔고, 변환/청킹/메타데이터 전략의 영향이 큽니다. (arxiv.org)
  • 즉, 추출 파이프라인은 단독 지표(WER)보다 업무 KPI(정산 검수 시간, QA 정답률)로 평가하세요.

흔한 함정/안티패턴

  • 스캔 PDF 전체를 VLM에 그대로 넣고 “표를 JSON으로” 한 방에 끝내기
    → 데모는 되지만, (a) 비용 폭발, (b) 페이지 수 늘면 컨텍스트/일관성 붕괴, (c) provenance 부재로 장애 대응 불가.
  • OCR 품질이 낮은데 LLM으로 “상식 보정”
    → LLM이 빈 칸을 채워 넣는 순간, 회계/정산/법무 문서에서 사고가 납니다. “Do NOT guess” 규칙 + 검증 루프 필수.
  • 표를 Markdown으로만 저장하고 셀 좌표를 버림
    → 나중에 “이 금액이 표의 어느 셀에서 왔냐” 질문에 답을 못 합니다.

비용/성능/안정성 트레이드오프 (2026년 7월 기준)

  • Google의 Gemini 기반 layout parser처럼 “OCR + LLM 결합”은 구조화 품질을 끌어올리지만, 대량 처리 시 단가/지연을 반드시 측정해야 합니다. (docs.cloud.google.com)
  • Mistral OCR 4는 bounding box/레이블/신뢰도와 멀티언어를 강점으로 내세우고, self-hosted 단일 컨테이너도 언급됩니다(배포/데이터 경계가 중요한 조직에 매력). (mistral.ai)
  • 반대로, AWS Textract나 Azure Document Intelligence처럼 “문서 전용” 제품은 표/레이아웃 객체가 비교적 명시적이라 감사/운영 친화적인 면이 있습니다. (docs.aws.amazon.com)

🚀 마무리

2026년 7월의 결론은 단순합니다. 문서 AI는 ‘LLM으로 읽는다’가 아니라 ‘Layout/Table을 먼저 복원하고, LLM은 스키마 매핑에 제한한다’가 프로덕션 승률이 높습니다. Google의 Gemini 기반 layout parser 같은 흐름, Mistral OCR 4의 bbox/블록/신뢰도 강조, 그리고 휴리스틱 TSR(SPARTAN) 재부상은 모두 같은 방향을 가리킵니다. (docs.cloud.google.com)

도입 판단 기준(실무 체크리스트):

  • 표가 핵심이면: TSR 품질 + merged cell 처리 + 좌표/셀 근거 제공 여부
  • 운영이면: provenance 설계(값→원문 근거) + 검증/재시도 + 관측가능성(로그/샘플링)
  • 비용이면: 페이지당 비용 + chunking 전략 + “LLM 투입 범위 최소화”

다음 학습 추천:

  • MLLM 기반 문서 이해의 방법/과제 정리(서베이)로 큰 그림 잡기 (aclanthology.org)
  • PDF 변환 품질이 RAG에 미치는 영향 평가 프레임워크 참고 (arxiv.org)
  • “프로덕션 아키텍처” 관점(마이크로서비스/스케일링/재처리) 사례 연구로 운영 설계 보강 (arxiv.org)
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.