기업형 계약 분석 엔진 만들기 — 청킹, HWP 파서, 버전 비교, 플레이북 (7장)
6,000자 컷 — 계약서 뒷부분이 통째로 안 읽히고 있었다
계약서를 AI에게 분석시키는 코드를 열어보니, 이런 줄이 있었다.
prompt = f"""다음 계약서를 분석해주세요.
계약서 전문:
---
{contract_text[:6000]}
---
"""
[:6000] — 앞 6,000자만 잘라서 LLM에 넘기고 있었다. 짧은 근로계약서라면 문제없지만, 기업 간 용역계약서나 공급계약서는 보통 2만~4만 자다. 뒤쪽 70~80%, 그러니까 손해배상·관할법원·비밀유지 같은 정작 중요한 조항이 대부분 들어있는 구간이 아예 AI 눈에 안 들어가고 있었다. 그런데 화면엔 "분석 완료"라고만 뜬다. 사용자는 자기 계약서의 3/4가 검토조차 안 됐다는 걸 알 방법이 없었다.
조항 경계로 자르고, 병렬로 분석하고, 다시 합친다
해결책은 텍스트를 통째로 넣는 대신, 조항 단위로 쪼개서 나눠 분석한 뒤 합치는 것이었다.
_ARTICLE_RE = re.compile(r"(?m)^\s*(제\s*\d+\s*조(?:\s*의\s*\d+)?)")
def split_into_chunks(text: str, max_chars: int = 6000) -> list[str]:
positions = [m.start() for m in _ARTICLE_RE.finditer(text)]
# "제12조", "제12조의2" 같은 조항 시작 지점을 찾아서
# 그 경계에서만 자른다 (조항 중간이 잘리지 않게)
...
단순히 6,000자마다 뚝뚝 자르면 조항 하나가 반으로 쪼개질 수 있다. 제N조라는 패턴을 정규식으로 찾아서, 그 경계에서만 자르도록 했다. 자른 조각들은 asyncio.gather로 동시에 분석해서 시간을 절약했다.
여기서 진짜 까다로웠던 건 "누락 조항" 판정이었다. 원래는 청크 하나를 분석할 때 "이 계약 유형에 보통 있어야 하는데 없는 조항"까지 같이 판정했는데, 분할된 조각 하나만 보고 "이 조항이 없다"고 판단하면, 사실은 다른 조각에 있는 조항을 없다고 오판할 수 있다. 그래서 청크 단계에서는 누락 판정을 아예 금지시키고, 모든 조각을 다 합친 뒤에 딱 한 번 전체 조항 목록과 체크리스트를 대조해서 누락을 판정하도록 순서를 바꿨다.
한글 파일(HWP) — 외부 라이브러리 없이 직접 뜯어봤다
국내 기업·공공기관 계약서 상당수가 .hwp 파일이다. 기존 파서는 PDF, DOCX, TXT만 지원했다. HWP를 지원하려면 두 가지 형식을 다뤄야 했다.
HWPX(2014년 이후 한글의 개방형 표준)는 사실 ZIP 압축 파일 안에 XML이 들어있는 구조라, 파이썬 표준 라이브러리(zipfile, xml.etree)만으로 풀 수 있었다.
with zipfile.ZipFile(file_path) as z:
for name in sections: # Contents/section0.xml, section1.xml ...
root = ET.fromstring(z.read(name))
for elem in root.iter():
if elem.tag.rsplit("}", 1)[-1] == "t": # 텍스트 태그
text_parts.append(elem.text)
HWP 5.0(예전 바이너리 형식)은 훨씬 까다로웠다. OLE 복합문서라는 구조 안에, 압축된 바이너리 레코드들이 들어있다. olefile 라이브러리로 구조를 열고, zlib으로 압축을 풀고, 레코드를 하나씩 순회하면서 "이건 문단 텍스트 레코드다" 싶은 것만 골라서 UTF-16 문자로 디코딩했다. 문서 하나를 읽으려고 바이너리 포맷 스펙을 뒤져야 했던 몇 안 되는 순간이었다.
리드라인 — 상대방이 뭘 바꿨는지 자동으로 찾아내기
기업 계약 실무에서 진짜 반복되는 일은 "상대방이 보내온 수정본에서 뭐가 바뀌었는지" 확인하는 것이다. 이걸 위해 두 버전의 조항을 비교하는 엔진을 만들었다.
매칭은 3단계로 한다 — ① 조항번호가 같으면 그걸로 매칭, ② 없으면 제목 유사도로, ③ 그래도 안 되면 본문 유사도로. 매칭된 조항끼리는 단어 단위로 diff를 떠서 "이 부분이 삭제됐고, 이 부분이 추가됐다"를 보여준다. 그리고 리스크 점수 변화를 계산해서 이 변경이 우리에게 유리해졌는지 불리해졌는지까지 자동으로 판정했다 — 예를 들어 손해배상 한도가 100%에서 300%로 바뀌면 명백히 불리하고, 위험한 조항 하나가 통째로 삭제됐으면 유리한 쪽으로 분류하는 식이다.
플레이북 — "법적으로 안전한가"가 아니라 "우리 회사 기준에 맞는가"
지금까지 만든 위험도 판정은 전부 "일반적인 법적 리스크"를 보는 것이었다. 그런데 기업 법무팀이 실제로 궁금한 건 "우리 회사 표준 조항과 얼마나 다른가"다. 그래서 회사별로 "이 항목은 이런 기준을 충족해야 한다"는 규칙을 등록하고, 계약서를 그 규칙과 대조해서 준수/미달/누락으로 판정하는 시스템을 추가했다. 위반 시 상대방에게 요구할 구체적인 협상 문구까지 같이 생성하게 했다.
이 장에서 남는 것
계약서 분석이라는 기능 하나에도 실제로는 텍스트 분할 알고리즘, 바이너리 파일 포맷 파싱, 문자열 매칭·diff 알고리즘, 규칙 기반 판정 시스템 — 각각 성격이 완전히 다른 네 가지 엔지니어링이 들어갔다. "AI로 계약서 분석해준다"는 한 줄짜리 기능 설명 뒤에, 이 정도 두께가 숨어있다는 걸 직접 만들어보고서야 실감했다.
다음 편 예고
서비스 둘러보기 — 지금까지 나온 화면들. 시리즈 마지막 편이자 에필로그다.
이 글은 개인 개발 프로젝트의 기록이며, 실제 코드는 일부 축약·각색되었습니다.