케이스 스터디
CASE STUDY — 자동화

계약서 처리 자동화 — OCR 엔진을 고르는 기준과, 값을 지어내지 않는 검증

도입 법무 / 총무구축 7일
WebhookOCR / 문서추출PostgresSlack

계약서가 들어올 때마다 담당자가 PDF를 열어 당사자·금액·기간을 일일이 시스템에 입력했습니다. 양이 몰리면 입력이 밀리고, 옮겨 적는 과정에서 오타가 났습니다.

이 일은 '읽어서 옮기는' 단순 반복입니다. 그래서 읽는 단계는 OCR과 추출에, 검증 단계는 규칙에 맡기고, 사람은 예외만 보게 했습니다.

contract-intake.workflowLIVE
UP계약서 업로드OCR정보 추출VL항목 검증DB시스템 등록

처리 단계

  1. 업로드된 계약서를 OCR로 텍스트화합니다.
  2. 당사자·금액·기간 등 핵심 항목을 자동 추출합니다.
  3. 필수 항목 누락·형식 오류를 검증해 플래그합니다.
  4. 정상 건은 시스템에 등록하고, 예외 건만 담당자에게 넘깁니다.

OCR 엔진을 어떻게 고르나 — 이 케이스에서 가장 많이 받는 질문

이전 버전의 이 글에는 스택에 그냥 OCR이라고만 적혀 있었습니다. 그런데 독자가 실제로 알고 싶은 단 하나가 바로 '어떤 OCR이냐' 입니다. 정답은 하나가 아니고, 아래 네 축으로 갈립니다.

  • 한글 정확도 — 비라틴 문자 지원 여부는 엔진마다 크게 다릅니다. 영문 벤치마크 숫자는 한글 계약서에 그대로 적용되지 않습니다. 반드시 실제 계약서 표본으로 직접 재보세요.
  • 표 인식 — 계약서에는 금액·기간이 표 안에 들어 있는 경우가 많습니다. 그런데 표 구조 인식과 비라틴 문자 인식을 동시에 잘하는 엔진은 드뭅니다. 한글 계약서는 정확히 그 조합입니다. n8n 커뮤니티에서도 표가 많은 문서에서는 범용 OCR보다 문서 구조 전용 서비스가 낫다는 관찰이 반복적으로 올라옵니다.
  • 비용 구조 — 페이지당 과금인지 문서당인지, 무료 구간이 있는지. 월 100건과 월 10,000건은 전혀 다른 선택이 됩니다. 도입 전에 예상 물량 × 단가를 먼저 계산하세요.
  • 데이터 반출 가능 여부국내 기업 담당자가 가장 먼저 묻는 질문입니다. 계약서를 외부 API로 보내도 되는지가 조직 정책상 막혀 있으면 나머지 세 축은 볼 필요가 없습니다. 그 경우 로컬에서 도는 OCR + 로컬 LLM 조합만 후보가 됩니다. 저희 온프렘 케이스가 그 구성을 다룹니다.
엔진을 먼저 고르지 마세요. 위 네 축 중 '데이터 반출 가능 여부'부터 확인하면 후보가 절반으로 줄고, 그다음 실제 계약서 20~30장으로 항목별 정확도를 직접 재면 결론이 납니다. 이 문서는 특정 엔진을 권하지 않습니다 — 엔진과 가격은 자주 바뀌고, 조직 정책은 저희가 알 수 없기 때문입니다. 워크플로우의 OCR 노드를 비워둔 이유가 이것입니다.

스캔본(이미지 PDF)은 되나

이게 두 번째로 많이 받는 질문입니다. 텍스트 레이어가 있는 PDF와, 종이를 스캔해 이미지만 들어 있는 PDF는 완전히 다릅니다. 후자는 일반 PDF 텍스트 추출 노드로는 아무것도 나오지 않습니다. 빈 문자열이 나오는데 에러는 아니어서, 그대로 두면 '추출 성공, 내용 없음'으로 조용히 통과합니다.

  • 먼저 계약서가 어느 쪽인지 확인하세요. PDF를 뷰어에서 열어 본문 텍스트를 드래그해 선택할 수 있으면 텍스트 레이어가 있는 것입니다.
  • 이미지 PDF라면 반드시 OCR 단계가 필요합니다. 텍스트 추출만으로는 되지 않습니다.
  • 아래 검증 규칙이 이 상황의 안전망입니다. 추출이 빈 값이면 필수 항목 누락으로 잡혀 needs_review가 되므로, 빈 계약서가 등록되는 일은 일어나지 않습니다.

값을 지어내지 않는다 — 검증 규칙

추출은 절대 100%가 되지 않습니다. 그래서 이 워크플로우의 핵심 원칙은 하나입니다. 필수 항목이 하나라도 비었거나 형식이 틀리면 자동 등록하지 않고, 이유와 함께 사람에게 넘깁니다. 계약서에 잘못된 금액이 들어가는 것은 금액이 비어 있는 것보다 훨씬 나쁩니다.

필수 항목: party_a · party_b · amount · start_date · end_date

금액 파싱   ₩ , 원 쉼표 공백을 제거한 뒤 숫자인지 확인
            "11,000,000원" → 11000000
            "별도 협의"     → 실패 → needs_review

날짜 파싱   2026-01-31 / 2026.01.31 / 2026/01/31 / 20260131 모두 허용
            2026-02-31    → 존재하지 않는 날짜 → 실패 → needs_review

교차 검증   종료일 < 시작일  → needs_review
2026-02-31 같은 날짜를 잡는 게 생각보다 중요합니다. 대부분의 언어에서 이런 날짜는 에러 없이 3월 3일로 조용히 넘어갑니다. 계약 종료일이 이틀 밀린 채로 시스템에 등록되고, 아무도 모릅니다. 그래서 이 워크플로우는 파싱한 날짜를 다시 문자열로 되돌려 원본과 같은지 대조합니다.

추출 스키마

CREATE TABLE IF NOT EXISTS contracts (
  id             bigserial PRIMARY KEY,
  source_file    text,
  party_a        text,
  party_b        text,
  amount         numeric,
  start_date     date,
  end_date       date,
  auto_renew     boolean NOT NULL DEFAULT false,
  status         text    NOT NULL DEFAULT 'needs_review',  -- registered|needs_review
  review_reasons jsonb   NOT NULL DEFAULT '[]'::jsonb,
  created_at     timestamptz NOT NULL DEFAULT now()
);

CREATE INDEX IF NOT EXISTS contracts_status_idx ON contracts (status, created_at DESC);

review_reasons왜 걸렸는지가 그대로 남습니다. 담당자가 해당 건을 열었을 때 무엇을 확인해야 하는지 바로 알 수 있고, 시간이 지나면 이 컬럼을 집계해 어떤 항목이 가장 자주 실패하는지 볼 수 있습니다. 그게 다음 개선 지점입니다.

필요한 것

  • n8n — 업로드를 외부에서 받아야 하므로 인터넷에서 접근 가능한 주소가 필요합니다.
  • Postgres 데이터베이스 하나 — 계약 테이블 하나.
  • OCR / 문서추출 서비스 — 위 네 축으로 고른 것. 무료 구간이 있는 서비스로 표본 테스트부터 하는 것을 권합니다.
  • Slack Bot User OAuth Tokenchat:write, channels:read. 검토 필요 건 알림용입니다.
  • 실제 계약서 표본 20~30장 — 정확도를 재려면 반드시 필요합니다. 이 표본 없이 엔진을 고르면 도입 후에 다시 고르게 됩니다.
⬇︎ 워크플로우 다운로드 (contract-intake.json)
Postgres 자격증명 하나, Slack 자격증명 하나면 검증·등록·예외 알림이 그대로 돕니다. OCR 노드는 위 기준으로 고른 엔진에 맞게 채우세요.
검증 범위를 정확히 밝힙니다. 검증 로직은 Node.js에서 8개 입력으로 실행해 전부 통과했습니다 — 정상 계약서, 기호가 붙은 금액, 구분자 없는 날짜(20260101), 금액 누락, 금액이 "별도 협의" 같은 문자열인 경우, 존재하지 않는 날짜(`2026-02-31`), 종료일이 시작일보다 빠른 경우, 당사자 누락. "11,000,000원"이 숫자 11000000으로 파싱되는 것도 확인했습니다. 노드 타입·버전은 기존 배포 워크플로우와 동일한 조합입니다. 확인하지 않은 것: 특정 OCR 엔진의 한글·표 인식 정확도 — 저희가 측정하지 않았으므로 이 문서에는 어떤 엔진의 정확도 수치도 적지 않았습니다. 본문의 12분 → 1분, −90%는 초기 도입 사례의 수치이며 이 워크플로우로 재측정한 값이 아닙니다. 마지막 검증: 2026-08-15.

설정 순서 (30분 + 엔진 선정 시간)

  1. 표본부터 모으세요 — 실제 계약서 20~30장. 스캔본과 텍스트 PDF가 섞여 있으면 더 좋습니다.
  2. 데이터 반출 가능 여부 확인 — 조직 정책상 계약서를 외부 API로 보내도 되는지 먼저 확인합니다. 안 되면 후보가 로컬 엔진으로 좁혀집니다.
  3. 엔진 2~3개로 표본 테스트 — 무료 구간을 이용해 같은 표본을 넣고 항목별로 몇 개나 맞히는지 셉니다. 이 결과를 표로 남겨두세요.
  4. 테이블 생성 — 위 SQL을 실행합니다.
  5. Postgres·Slack 자격증명 등록 — 각각 Test로 확인하고, Slack 채널에 /invite @봇이름.
  6. 워크플로우 import — n8n → Workflows → ...Import from File.
  7. OCR 노드 채우기 — 고른 엔진의 엔드포인트·인증·요청 본문을 넣습니다. 응답이 party_a, party_b, amount, start_date, end_date를 반환하도록 매핑하세요.
  8. Activate 후 업로드 경로 연결Contract Upload Webhook의 Production URL을 업로드 폼에 연결합니다.
  9. 일부러 깨진 계약서로 테스트 — 금액이 지워진 계약서를 한 장 올려보세요. needs_review로 저장되고 Slack에 이유가 떠야 합니다. 정상 건만 테스트하면 이 워크플로우의 절반을 테스트하지 않은 것입니다.

예외 상황이 있으면 어떻게 되나

  • 스캔 품질이 나쁜 계약서 — 추출값이 비거나 깨져 나옵니다. 필수 항목 검증에 걸려 needs_review가 되므로 잘못된 값이 등록되지 않습니다.
  • 손글씨 서명란·도장 — 이 워크플로우는 서명 유무를 판단하지 않습니다. 서명 확인이 필요한 업무라면 사람 검토 단계를 반드시 남겨두세요.
  • 한 PDF에 여러 계약서 — 첫 계약서 기준으로만 추출됩니다. 분리 업로드를 전제로 하며, 자동 분할이 필요하면 별도 단계가 필요합니다.
  • HWP 첨부 — PDF가 아니면 이 워크플로우로 들어오지 않습니다. 업로드 단계에서 PDF 변환을 거치게 하거나, 해당 형식을 읽는 노드를 앞에 추가해야 합니다.
  • 금액이 '별도 협의'인 계약 — 실무에서 실제로 있습니다. 형식 오류로 잡혀 needs_review가 됩니다. 이런 계약이 많다면 `amount`를 필수에서 빼고 별도 상태로 관리하는 편이 낫습니다. 필수 항목 목록은 Validate Extracted Fields 노드 맨 위에서 바꿀 수 있습니다.
  • OCR 서비스가 다운된 경우 — 해당 실행이 에러로 끝납니다. 업로드는 이미 200을 응답했으므로 사용자에게는 성공으로 보입니다. 재처리 경로가 필요하면 앞선 정산·결제 케이스처럼 큐 구조를 도입하세요. 기본 구조에는 큐가 없습니다.
12분 → 1분
건당 처리 시간
−90%
입력 오류
예외만
사람 검토
0건
값 추정 등록
계약서 한 장 한 장 입력하던 게 거짓말 같아요. 이제 올리기만 합니다.총무팀 담당자

당신의 업무도
여기 들어갈 수 있어요.

가장 반복적인 업무 하나만 알려주세요. 자동화 시나리오를 그려드립니다.