gwimong's blog Software Engineer

artifact-transformer 작업 일지 #33

발행한 것을 다시 읽어 들이는 날 — Notion 역복원 파이프라인 완성, 그리고 앱에게 문서 정비를 시킨 첫 도그푸딩


이 글은 2026-08-10 하루의 기록이다. 오전부터 오후까지 Notion에 발행한 산출물을 로컬로 되돌리는 역복원 파이프라인을 .docx → .puml → 발행 측 정비 순으로 완성했고, 그 사이 복원이 드러낸 스키마 오류·개정이력 유실을 연쇄로 잡았다. 오후엔 앱의 AI 편집 기능으로 FDSP 문서 정비를 앱 자신에게 시켜 봤다 — 그리고 빈 프롬프트 호출 버그도 같이 얻었다.

배경 — 발행은 되는데 되돌아오는 길이 반쪽이었다

전날 만든 임포터는 Notion에 발행된 산출물을 로컬 파일로 되돌리는 기능인데, .xlsx만 복원했다. 명세서·계획서·절차서 14종은 .docx라서 통째로 빠졌고, 그 안에 있어야 할 데이터 속성·필드가 비어 추적성 ERROR가 16건 남아 있었다. 다이어그램(.puml)은 아예 건너뛰어 participant.json·sequence.json이 없었고, 관련 검사가 통째로 비활성이었다.

발행 측에도 구멍이 있었다. 발행 도구가 “단계별 하위 페이지” 구조 하나만 알아서, 산출물을 인라인 DB 행으로 관리하는 프로젝트에 발행하면 같은 산출물이 두 벌로 공존하고, 정리 로직은 사람이 직접 쓴 방향·원칙 블록까지 삭제 대상에 올렸다.

문제

  1. .docx 역변환 명세를 어떻게 정하나 — 추측하면 파서가 못 읽는 파일이 나온다.
  2. 복원이 드러낸 스키마 오류 38건은 어디서 오나.
  3. 다이어그램은 문서 복원 경로를 못 타는데 어떤 규약으로 되돌리나.
  4. 발행 도구가 사람이 쓴 본문을 지우지 않으려면 경계를 어디에 긋나.
  5. 앱의 AI 편집 기능은 실제 문서 정비 업무를 감당하는가.

결정 1 — 역변환 명세를 추측하지 않고 발행 사슬을 뒤집는다

.docx 복원의 핵심은 역변환 규칙을 새로 정의하지 않은 것이다. 발행 사슬이 이미 docx Heading N → md #×N → Notion heading_(N-1)로 결정적이니, 그걸 그대로 뒤집어 Notion heading_M → docx Heading (M+1)로 1:1 대응시켰다.

신규 common/docx_writer.py는 서식이 아니라 파서가 읽는 구조만 만드는 최소 docx 생성기다. 다만 document.xml만 넣으면 안 되고, 패키지 관계 파일과 Heading 1~6 스타일을 함께 넣어야 워드로 열리고 python-docx 재처리가 된다.

함정이 하나 있었다. docx에서 표 둘이 빈 줄 없이 이어지면 markdown 단계에서 한 덩어리가 되어 Notion이 표 하나로 읽는다. 그대로 복원하면 물리 데이터 명세서의 필드 표가 카드 표에 섞여 fields가 통째로 빈다 — 실제로 발생했다. GFM 구분선(|---|---|)이 데이터 행에 남아 있는 것을 경계 표식으로 삼아 되나누는 split_merged_tables를 넣었다.

결과: TDIH 프로젝트 복원 19→35종, 추적성 ERROR 16→0.

역변환을 설계할 때 가장 위험한 것은 “이 정도면 비슷하겠지”다. 정방향 변환이 결정적이면 역방향도 그 사슬을 뒤집는 것 외의 창작을 하지 않는다.

결정 2 — 빈 자리를 만들어 두는 것이 위반이다

복원이 끝나자 스키마 오류 38건이 드러났다. 전부 같은 패턴이었다 — 스키마는 control과 project를 선택 항목으로 두지만, 키를 두는 순간 하위 키를 필수로 요구한다. 값이 없는데 자리만 만들어 두면 오히려 위반이다.

  • control 32건: 항목 템플릿이 빈 {}를 깔아 두는데, ControlInfo는 authority·changeProcedure를 필수로 본다. control은 dataScope가 ‘외부참조’일 때만 의미 있으니 내용이 없으면 키째 제거했다. 템플릿에서 아예 지우지 못한 이유도 있다 — 정규화가 템플릿에 없는 키를 떨어뜨리는 구조라, 지우면 이미 채워진 control까지 잃는다.
  • project 6건: reset 경로만 env에서 project를 채우고 partial 병합 경로는 안 채웠다. 기본 경로가 partial이라 --reset 없이 처음 만든 파일은 project가 빈 {}로 남았다. partial 쪽도 비어 있을 때만 env로 채우게 했다 — 있으면 덮어쓰지 않는 부분 병합 규약은 유지한다.

스키마 오류 38→0.

결정 3 — 이름을 지어내느니 건너뛴다

다이어그램 산출물은 문서 31종 코드 밖이라 문서 복원 경로를 못 탄다. 대신 발행물의 규약을 역이용했다 — 발행 시 #### {파일명}.puml 제목 바로 뒤에 코드 블록으로 소스를 싣으니, (제목, 소스) 쌍으로 추출하면 된다. 제목에서 파일명을 못 찾으면 @startuml {이름} 폴백, 둘 다 없으면 건너뛴다.

건너뜀을 택한 이유가 이 날의 판단 중 하나다.

이름을 지어내면 파일명 규약 검사에 안 걸려 조용히 무시된다. 조용한 무시는 실패보다 나쁘다. 모르면 건너뛰고 건너뛴 사실을 남긴다.

ERD는 손대지 않았다. 발행 시 !includesub로 조합되던 partial이 하나로 인라인되어 OVERVIEW 형태로만 남았는데, 되나누려면 사라진 !startsub 구조를 추측해야 한다. physical-data.json은 정의서·명세서 쪽에서 이미 완전해 데이터 영향이 없으니, 추측 복원 대신 보류가 맞다.

결과: 복원 35→42건, 입력 JSON 14→16종, 다이어그램 검사 전부 통과.

결정 4 — 발행 도구는 사람의 본문을 넘보지 않는다

발행 측 정비에서 제일 무서웠던 건 정리 로직이었다. clear_children(keep_pages=True)가 사람이 직접 쓴 방향·원칙 블록 16개를 삭제 대상에 올렸고, 발행 직전 중단으로 사고를 면했다.

인라인 「산출물」 DB 구조 지원을 넣으면서 경계를 명시했다.

대상도구의 권한
산출물 DB 행 본문도구 표식(DOC_MARK) 이후만 교체 — 선두의 사람 머리말은 보존
행의 상태 속성채우지 않음 — 초안/검토중/작성완료는 사람 판단 (신규 행만 ‘초안’)
프로젝트 행 본문아예 건드리지 않음

행 탐색은 제목이 아니라 문서ID로 한다 — 제목은 사람이 고칠 수 있다. 그리고 인라인 DB 탐색 규약을 find_artifact_db로 승격해 복원과 발행이 같은 곳을 보게 통일했다.

저녁에 발행 쪽 버그가 하나 더 잡혔다. Notion append가 최상위 블록 100개 단위로만 청크를 나눴는데, Notion의 1000개 한도는 중첩 포함 전체 블록 수다. 표는 행이 중첩 children이라 표 많은 문서에서 “exceeds limit of 1000”으로 4건이 실패했다. 블록의 무게(자신+중첩 전체)를 계산해 최상위 ≤100 AND 전체 무게 ≤900으로 자르게 고쳤고, 7개 프로젝트 248/248 발행이 완료됐다.

결정 5 — 주입하는 8곳 대신 읽는 1곳을 관대하게

노션 발행물에서 인수·통합 시험 8개 산출물의 버전 마커가 전부 v?로 떠 있었다. 추적하니 개정이력이 denormalize 단계에서 버려지고 있었다.

원인은 파서 관례의 분화였다. 운영 계열 파서는 data["revisions"](평탄)에, 인수·통합 계열 파서 8개는 data["document"]["revisions"](중첩)에 개정이력을 넣는데, denormalize 함수들은 평탄한 쪽만 읽었다. 중첩 값은 조용히 소실되고 스켈레톤이 빈 배열을 만들었다.

파서 8개를 고쳐 관례를 통일하는 대신, 읽는 쪽 한 지점(_revisions_of)이 두 형태를 모두 수용하게 했다. 관례가 이미 갈라져 있는 상태에서 주입 측을 고치면 8개 파일을 건드리고 다른 계열을 깨뜨릴 위험이 더 크다. 둘 다 있으면 평탄 우선 — 이 계약을 테스트 20건으로 고정했다.

관례가 갈라진 지 오래된 코드에서는 쓰는 쪽을 통일하는 것보다 읽는 쪽을 관대하게 만드는 편이 침습이 작다. 대신 우선순위를 계약으로 못 박아야 한다.

결정 6 — 문서 정비를 앱 자신에게 시켜 보다

오후에는 FDSP 문서 정비 두 건을 앱의 AI 편집 기능으로 처리했다. 앱이 Claude Code를 헤드리스로 호출해 산출물 JSON을 편집하는 그 기능이다 — 즉 도그푸딩.

첫 건은 검토 의견(FDSP-244) 반영이었다. 요구사항 유형이 전 건 ‘SW’로 통일되어 있던 것을, 타 프로젝트 분류 체계를 선례로 대조해 기능(FR) 18건·성능(PR) 1건으로 재분류하고 개정 이력 1.3.0을 기록했다. 소요 2분 30초.

둘째 건은 구현 역도출로 신설된 요구사항 REQ-MON-102에 Jira 백킹 스토리를 연결하는 작업이었다. 여기서 파이프라인 구조가 드러난다 — Jira 이슈 생성(FDSP-278)은 바깥 오케스트레이션이 먼저 수행하고, 그 결과 키를 프롬프트에 주입해 문서 편집 호출은 issueKey 기입과 remarks 정리만 한다.

이슈 생성(REST, 결정적)과 문서 반영(AI 편집)을 한 호출에 섞지 않는다. AI 호출에는 이미 확정된 사실만 주입하고, 편집 범위를 “지시받은 변경만”으로 계약한다.

프롬프트 계약도 이 관점으로 짜여 있다. 출력은 {"explanation", "content"} 단일 JSON, 새 항목 식별자는 서버가 발급하도록 NEW: placeholder 강제, 요구사항 문장 형식과 모호어 금지 규칙까지 프롬프트에 정책으로 내장했다.

의도치 않은 함정

빈 프롬프트로 헤드리스 호출이 두 번 나갔다. 16시 25분과 28분, 앱이 Claude Code를 호출했는데 프롬프트가 system\n\n---\n\nuser — 템플릿 구분자만 남고 내용이 전부 빠진 채였다. 프롬프트 조립 단계의 버그다. AI는 “요청이 비어 있다”고 답하고 끝났으니 피해는 없지만, 헤드리스 호출은 사람이 화면을 안 보고 있으니 이런 실패가 조용히 쌓인다. 조립 결과가 비었으면 호출 전에 막는 가드가 필요하다.

한도의 단위를 확인하지 않았다. Notion 블록 1000개 한도를 최상위 블록 수로 착각한 것이 청킹 버그의 뿌리다. 한도 초과 오류를 만나면 무엇을 세는 한도인지부터 API 문서에서 확인해야 한다.

마무리

이날 파이프라인이 왕복으로 닫혔다. 로컬 산출물 → Notion 발행 → 로컬 역복원이 문서(.xlsx/.docx)와 다이어그램(.puml)까지 돌고, 각 단계 복원이 드러낸 오류(추적성 16건 → 스키마 38건 → 개정이력 유실)를 그날 안에 소거했다. 그리고 그 도구로 실제 문서 정비 업무 두 건을 처리해 봤다.

  • 정방향이 결정적이면 역방향은 창작하지 않는다 — 발행 사슬을 뒤집는 것이 역변환 명세다.
  • 선택 항목은 빈 자리를 만들어 두는 순간 위반이 된다 — 내용 없으면 키째 없애라.
  • 조용한 무시는 실패보다 나쁘다 — 이름을 지어내 규약 검사를 통과시키느니 건너뛰고 기록한다.
  • 자동화 도구와 사람의 편집 영역은 표식으로 경계를 긋는다 — 도구는 자기 표식 이후만 교체한다.

다음에 남는 것:

  • 단계 복귀 흐름 보강 작업이 진행 중이다 — 서버가 복귀 사유를 강제하고(direction 기록), 도달했던 최고 단계를 계산해 “재작업 대기”를 구분하고, 전환 이력을 화면에 표출하는 3축. 커밋 전이라 다음 일지의 몫.
  • ERD 역복원 보류 건 — !startsub 구조 추측 없이 되돌릴 방법이 있는지, 아니면 발행 시 partial 경계를 마커로 남길지.
  • 빈 프롬프트 호출을 막는 조립 결과 검증 가드.

Comments

Content