"자료를 LLM에게 던져주면 문서 한 부쯤은 금방 뽑히겠지"라고 생각했던 순간부터, 실제 문제 해결은 시작되었습니다.
최근 참여한 아키텍처 컨설팅 프로젝트에서, 수십 개의 비즈니스 경계(Bounded Context)와 수백 개의 서비스를 정리한 대규모 서비스 정의서를 작성해야 했습니다. 입력 자료는 여러 시트로 구성된 서비스 카탈로그 엑셀과, 기존 분류 체계를 정리한 24쪽 분량의 워드 문서였습니다. 목표는 이 자료를 기반으로, 분류 축을 바꾼 새 정의서를 일관된 서식의 워드 문서로 만들어내는 것이었습니다.
처음에는 단순하게 접근했습니다. 자료를 LLM에 전달하고 "이 구조로 문서를 작성해줘"라고 요청하면 될 것이라고 생각했습니다. 하지만 실제 결과물은 예상과 전혀 달랐습니다. 첫 산출물은 무려 187쪽에 달했고, 분량뿐 아니라 서식·구조 모두 그대로 사용할 수 없는 상태였습니다.
이 글에서는 LLM으로 대규모 기술 문서를 생성하면서 부딪힌 분량 폭증과 서식 붕괴 문제를, 입력 분할·구조 재설계·생성 도구 전환·정합성 검증이라는 네 단계로 해결해 나간 과정을 정리하였습니다. 결과적으로 187쪽 실패본을, 사용 가능한 43쪽 최종본으로 만들기까지의 시행착오와, 그 과정에서 LLM 출력을 어떻게 코드와 연결하고 검증했는지를 함께 다룹니다.
1. 기술 선택 배경 — 왜 'LLM에게 통째로 맡기기'는 실패했는가
첫 시도는 가장 단순한 방식이었습니다. 입력 자료 전체를 LLM에 전달하고, 원하는 문서 구조를 자연어로 설명한 뒤 결과물을 받는 방식이었습니다. 그러나 이 방식은 세 가지 측면에서 한계를 드러냈습니다.
첫 번째는 분량 폭증이었습니다. 첫 산출물(v1)은 187쪽이었습니다. 원인을 분석해보니, LLM이 수백 개의 개별 서비스를 각각 별도의 섹션(헤딩)으로 펼쳐서 작성했기 때문이었습니다. 서비스 하나당 제목과 설명 문단이 붙으니 분량이 기하급수적으로 늘어났습니다. 사람이 기대한 것은 "서비스를 표의 행으로 압축한 형태"였지만, LLM은 명시적으로 통제하지 않으면 모든 항목을 최대한 풀어서 쓰는 경향이 있었습니다.
두 번째는 서식의 비일관성이었습니다. 표의 스타일이 섹션마다 달랐고, 헤딩 레벨이 들쭉날쭉했으며, 표지와 목차는 아예 생성되지 않거나 형식이 맞지 않았습니다. 문서 전체를 관통하는 일관된 서식 규칙이 없었습니다.
세 번째는 재현성 부재였습니다. 같은 자료로 다시 요청해도 섹션 구성과 분량이 매번 달라졌습니다. 대규모 문서에서 이는 치명적이었습니다. 한 번 검토를 마쳐도 다시 생성하면 구조가 흔들려, 검토 자체가 무의미해졌습니다.
결국 느낀 점은, LLM은 "내용을 생성하는 기술"이지 "대규모 문서를 구조적으로 조립하는 기술"은 아니라는 점이었습니다. 특히 분량과 서식의 책임을 LLM의 자연어 출력에 통째로 맡기는 한, 안정적인 산출물은 나올 수 없었습니다.
이후 접근 방향을 완전히 변경하였습니다. 다음 두 가지를 분리해 통제하기로 하였습니다.
-
구조·분량: 문서의 골격(어떤 항목을 표로 압축하고, 어떤 것을 섹션으로 둘지)을 사람이 명시적으로 정의
-
서식·렌더링: 표 스타일, 색상, 헤딩, 표지·목차 같은 시각적 요소를 코드가 일관되게 책임
즉, LLM은 "무엇을 쓸지(콘텐츠)"만 담당하고, "어떻게 구성하고 그릴지(구조와 서식)"는 사람과 코드가 통제하는 구조로 전환하였습니다.
2. 적용 과정 및 구현
전체 작업은 크게 네 단계로 진행하였습니다. 입력 분할, 구조 재설계, 생성 도구 전환, 정합성 검증입니다. 각 단계는 독립적으로 검증할 수 있도록 분리하여, 문제가 발생하면 어느 단계에서 비롯되었는지 빠르게 추적할 수 있도록 하였습니다.
2.0 입력 분할 — 컨텍스트 윈도우 한계를 고려한 BC 단위 호출
구현에 앞서 가장 먼저 결정해야 했던 것은 입력 전달 방식이었습니다. 입력 자료는 수십 개의 비즈니스 경계와 수백 개의 서비스를 담고 있었기 때문에, 이를 한 번의 프롬프트에 모두 밀어 넣으면 두 가지 문제가 우려되었습니다. 하나는 토큰 한계(Context Limit)이고, 다른 하나는 긴 입력의 가운데 정보가 소실되는 현상(Lost in the Middle)이었습니다.
그래서 입력 자료를 한 번에 던지지 않고, 비즈니스 경계(BC) 단위로 쪼개어 루프를 돌며 개별적으로 LLM을 호출하는 방식을 택하였습니다. 각 호출은 해당 BC의 서비스 목록과 공통 분류 체계(레이어 정의 등)만 컨텍스트로 받고, 그 BC에 해당하는 문서 조각만 생성하도록 하였습니다. 이렇게 생성된 조각들을 마지막에 하나의 문서로 병합하였습니다.
이 방식의 이점은 분명했습니다. 각 호출의 입력이 짧아 정보 소실이 거의 없었고, 특정 BC에서 문제가 생겨도 그 BC만 재생성하면 되므로 전체를 다시 만들 필요가 없었습니다. 대규모 입력일수록 "한 번에"가 아니라 "의미 단위로 쪼개어 순회"하는 편이 안정적이라는 점을 확인하였습니다.
2.1 구조 재설계 — '서비스 = 섹션'을 '서비스 = 표의 행'으로
분량 폭증의 직접적인 원인은 구조였습니다. 그래서 가장 먼저 문서의 골격을 명시적으로 다시 정의하였습니다. 핵심 원칙은 "개별 서비스를 별도 섹션으로 만들지 않는다"였습니다. 대신 같은 비즈니스 경계에 속한 서비스들을 하나의 표로 묶어, 각 서비스를 표의 행으로 압축하였습니다.
이를 LLM 프롬프트에 명시적인 제약을 못 박았습니다. 단순히 "간결하게 써 줘"가 아니라, "각 서비스를 H4 섹션으로 만들지 말 것. 도메인 표의 행으로 압축할 것. 비즈니스 경계당 평균 1.5쪽을 넘기지 말 것"처럼 구조를 수치로 통제하였습니다.
[구조 제약 — 프롬프트에 명시]
1. 개별 서비스를 H4 섹션으로 만들지 말 것 (그러면 180쪽 초과).
→ 서비스는 도메인 표의 '행'으로 압축.
2. 비즈니스 경계(BC)당 평균 1.5쪽, 총 40~45쪽 목표.
3. 문서 골격(헤딩 구조)은 아래 고정 템플릿을 그대로 따를 것.
- H1 = Part, H2 = 비즈니스 경계, H3 = 하위 섹션
이 한 가지 변경만으로 분량이 극적으로 줄었습니다. 187쪽이던 문서가 31쪽(v2)까지 떨어졌습니다. "LLM에게 분량을 줄이라고 부탁하는 것"과 "구조 자체를 분량이 늘어날 수 없는 형태로 고정하는 것"은 전혀 다른 결과를 만든다는 점을 확인하였습니다.
2.2 생성 도구 전환 — pandoc 기본 변환의 한계
분량 문제를 해결한 v2(31쪽)는 구조는 적절했지만, 또 다른 문제가 있었습니다. LLM이 생성한 마크다운을 pandoc으로 워드로 변환했는데, pandoc의 기본 스타일은 표지·목차·색상·표 디자인 같은 시각적 완성도를 보장하지 못했습니다. 내용은 맞지만, 고객에게 전달할 수준의 문서로 보기는 어려웠습니다.
그래서 렌더링 방식을 전환하였습니다. 마크다운을 그대로 변환하는 대신, 문서 구조를 코드로 직접 조립하는 docx 생성 라이브러리(docx-js)를 사용하기로 하였습니다. 이렇게 하면 표지, 목차, 헤딩 스타일, 표 디자인, 색상 같은 서식 요소를 코드 레벨에서 정밀하게 통제할 수 있었습니다.
이때 반드시 짚어야 할 부분이 LLM 출력과 코드 사이를 잇는 '데이터 다리'였습니다. LLM은 본질적으로 자연어를 출력하는 모델이라, 자유 형식 텍스트를 그대로 받으면 코드가 안정적으로 파싱할 수 없습니다. 그래서 LLM의 최종 출력 형식을 엄격한 JSON 스키마로 강제하였습니다. 자연어 문서를 직접 쓰게 하는 대신, 미리 정의한 스키마의 필드를 채우도록 한 것입니다.
// LLM 출력은 자연어가 아니라 이 스키마를 따르는 JSON 으로 강제
{
"bc": "환자·방문",
"overview": "환자/encounter/동의 등 ...",
"coreServices": [
{ "id": "S-001", "name": "환자", "layer": "Data", "owner": "원무" }
],
"events": ["PatientCreated", "EncounterStarted"],
"kpis": ["환자 등록 정합성", "encounter 완료율"]
}
코드는 이 JSON을 받아 docx-js 객체로 기계적으로 렌더링하기만 했습니다. 즉 LLM은 "무엇을 쓸지"를 JSON 필드로 채우고, 코드는 그 필드를 읽어 표의 행과 문단을 그리는 역할만 맡았습니다. 이렇게 출력 형식을 스키마로 묶자, LLM이 형식을 임의로 바꾸는 경우가 사라지고, 잘못된 형식의 응답은 파싱 단계에서 즉시 걸러낼 수 있었습니다.
// 서식의 단일 책임: 스타일 규칙을 코드 한 곳에서만 정의
const doc = new Document({
styles: {
default: { document: { run: { font: "Malgun Gothic", size: 20 } } },
paragraphStyles: [
{ id: "Heading1", run: { size: 32, bold: true }, ... },
{ id: "Heading2", run: { size: 28, bold: true }, ... },
],
},
sections: [{ children: [/* 표지 → 목차 → 본문 */] }],
});
도구 전환의 핵심은 "콘텐츠와 서식의 분리"였습니다. LLM이 만든 내용(표의 행 데이터)은 그대로 두고, 그것을 그리는 방식(폰트·색상·테두리)은 코드가 단독으로 책임지게 한 것입니다. 덕분에 LLM 출력이 조금 달라져도 최종 문서의 모양은 항상 동일하게 유지되었습니다.
2.3 서식 스타일링 — 색상 코딩과 표 디자인
마지막으로 가독성을 위한 시각적 규칙을 코드에 반영하였습니다. 문서에는 성격이 다른 여러 영역이 섞여 있었기 때문에, 영역별로 색상을 다르게 지정하여 한눈에 구분되도록 하였습니다. 또한 표 헤더에 배경색을 넣고 교차되는 행에 음영(zebra striping)을 적용해 표의 가독성을 높였습니다.
[영역별 색상 코딩]
Core 영역 : 딥블루 (#1F4E79)
AI 지원 영역 : 퍼플 (#7030A0)
Analytics : 그린 (#00875A)
공통(Shared) : 앰버 (#BF8F00)
이 모든 서식 규칙을 코드 한 곳에 모아두니, 문서 전체에 일관되게 적용되었고 이후 수정도 한 곳만 바꾸면 전체에 반영되었습니다. 이렇게 완성한 v3 최종본은 43쪽이었습니다. 분량은 목표 범위에 들었고, 표지·목차·색상·표 디자인까지 갖춘, 실제 전달 가능한 수준의 문서가 되었습니다.
2.4 정합성 검증 — 누락과 환각을 원천 차단하기
대규모 생성에서 가장 신경 쓴 부분은 데이터의 정합성이었습니다. 기술 문서는 수백 개의 서비스 중 단 하나의 누락이나 왜곡(할루시네이션)도 치명적입니다. LLM은 입력에 없던 항목을 그럴듯하게 지어내거나, 반대로 일부 항목을 조용히 빠뜨릴 수 있기 때문에, 사람이 43쪽을 일일이 대조하는 방식은 신뢰할 수도 지속할 수도 없었습니다.
그래서 파이프라인 마지막 단계에 정합성 검증 스크립트를 배치하였습니다. 핵심은 단순했습니다. 원본 엑셀의 서비스 ID 목록과, LLM이 생성한 JSON 데이터의 서비스 ID 목록을 기계적으로 상호 대조하는 것이었습니다. 두 집합의 차이를 계산해, 누락된 ID나 원본에 없는 ID(환각)가 하나라도 있으면 검증을 실패 처리하고 후속 단계로 넘어가지 못하도록 하였습니다.
source_ids = set(load_ids_from_excel()) # 원본 카탈로그
output_ids = set(s["id"] for bc in result for s in bc["coreServices"])
missing = source_ids - output_ids # 누락된 서비스
halluc = output_ids - source_ids # 원본에 없는 서비스(환각)
assert not missing, f"누락: {missing}"
assert not halluc, f"환각: {halluc}"
이 검증 덕분에, 특정 BC 호출에서 일부 서비스가 빠지거나 입력에 없던 서비스가 생성되는 사례를 사람이 보기 전에 자동으로 잡아낼 수 있었습니다. 문제가 검출되면 해당 BC만 다시 생성하면 되었기 때문에 비용도 적게 들었습니다. "생성했다"가 아니라 "입력과 정확히 일치하게 생성했다"를 보장하는 단계가, 대규모 문서 자동화에서는 반드시 필요하다는 점을 확인하였습니다.
3. 시행착오를 통해 얻은 교훈
이번 작업은 한 번에 완성된 것이 아니라, 세 번의 버전을 거치며 개선되었습니다. 각 버전에서 얻은 교훈을 정리하면 다음과 같습니다.
버전 분량 상태 핵심 문제 / 개선
---- ---- ------ -------------------------------
v1 187쪽 실패 서비스마다 별도 섹션 → 분량 폭증
v2 31쪽 분량 OK 구조는 잡힘, 그러나 서식 미흡(pandoc 기본)
v3 43쪽 최종 docx-js로 표지·목차·색상·표 완성
첫째, 분량 통제는 부탁이 아니라 구조로 해야 합니다. LLM에게 "짧게 써달라"고 요청하는 것만으로는 187쪽이 줄지 않았습니다. "서비스는 표의 행으로 압축한다"는 구조적 제약을 명시했을 때 비로소 분량이 잡혔습니다.
둘째, '내용이 맞다'와 '전달 가능하다'는 다릅니다. v2는 내용과 분량이 모두 적절했지만, 서식이 부족해 그대로 전달하기 어려웠습니다. 도구를 pandoc에서 docx-js로 바꿔 서식을 코드로 통제하고 나서야 완성도가 올라갔습니다.
셋째, 서식은 한 곳에서 통제해야 합니다. 색상·표·헤딩 같은 규칙을 콘텐츠 곳곳에 흩어두지 않고 렌더링 코드 한 곳에 모았더니, 일관성도 유지되고 수정도 쉬워졌습니다.
넷째, LLM 출력은 믿는 것이 아니라 검증하는 것입니다. 출력을 JSON 스키마로 강제하고, 입력 대비 ID 정합성을 자동으로 대조하지 않았다면, 누락과 환각은 사람이 발견하기 전까지 그대로 문서에 남았을 것입니다. 검증을 파이프라인 안에 코드로 내장한 것이 신뢰도를 좌우했습니다.
4. 적용 결과
결과적으로 187쪽의 사용 불가능한 초안을, 표지·목차·색상·표 디자인을 갖춘 43쪽의 전달 가능한 문서로 완성할 수 있었습니다. 단순 분량으로만 봐도 약 77% 축소였고, 무엇보다 매번 흔들리던 구조와 서식이 안정화되었다는 점이 가장 큰 성과였습니다.
작업 방식 측면에서도 변화가 있었습니다. 초기에는 LLM 출력물을 사람이 일일이 다듬어야 했지만, 구조를 고정하고 서식을 코드로 통제한 이후에는 같은 종류의 문서를 반복 생성할 때 사람의 개입이 크게 줄었습니다. 입력 자료가 갱신되면 동일한 파이프라인으로 다시 생성하면 되었기 때문에, 유지보수 비용도 함께 낮아졌습니다.
5. 한계 및 향후 계획
이번 작업을 통해 LLM 기반 대규모 문서 생성의 기본 골격은 충분히 검증할 수 있었습니다. 다만 개선이 필요한 부분도 명확합니다.
첫째, 현재는 구조 제약과 서식 규칙을 사람이 직접 정의하고 있습니다. 향후에는 입력 자료의 규모를 보고 적정 분량과 압축 수준을 자동으로 제안하는 단계가 있으면 더 효율적일 것으로 보고 있습니다.
둘째, 입력 자료가 표(엑셀) 형태일 때는 잘 동작했지만, 비정형 문서를 입력으로 받는 경우에는 먼저 구조를 추출하는 전처리가 필요합니다. 이 부분은 기존에 정리했던 문서 레이아웃 분석 경험과 결합하면, "비정형 문서를 읽어 구조를 이해하고, 새 정의서로 재구성하는" 전체 흐름까지 확장할 수 있을 것으로 기대하고 있습니다.
이번 경험을 통해, 단순히 "AI가 글을 잘 쓰는 것"만으로는 실무에서 쓸 수 있는 대규모 산출물이 나오지 않는다는 점을 직접 체감할 수 있었습니다. 결국 중요한 것은 "내용을 생성하는 것"이 아니라 "구조를 사람이 통제하고, 서식을 코드가 책임지며, 그 결과를 검증하는 것"이라는 점을 다시 한번 확인할 수 있었던 경험이었습니다.
참고 문헌
-
docx (docx-js), "docx Documentation", https://docx.js.org
-
Pandoc, "Pandoc User's Guide", https://pandoc.org/MANUAL.html
-
python-docx, "python-docx Documentation", https://python-docx.readthedocs.io
-
Liu et al., "Lost in the Middle: How Language Models Use Long Contexts", TACL 2024, https://arxiv.org/abs/2307.03172
Junny