릴리스 4회에서 반복된 실패 5종: 확정 결정 40개 중 모순 7건
작업 로그 하나로 시작하겠다.
신규 소설 프로젝트를 툴킷으로 진행해 확정 결정이 40개쯤 쌓였을 때, 전수 감사를 돌렸다. 모순 7건이 나왔다. 그중 5건이 한 뿌리였다 — 같은 변수를 회차 도구·주인공 능력·세계 메커니즘 세 곳에 쓰면서 규칙을 통일하지 않은 것.
그런데 더 중요한 건 따로 있었다. 그 7건과, 함께 나온 주요 개선 6건이 전부 사용자가 명시적으로 요구했을 때만 나왔다는 것이다.
| 나온 것 | 촉발한 발화 | 안 물었으면 |
|---|---|---|
| 적대자 3층 분리 | "매 화 적대자는 애매한데" | 매 화 새 적대자 설계로 진행 |
| 세 안을 관통하는 네 번째 안 | "관통하는 안이 있는지 봐" | 두 요소 조합에서 멈춤 |
| 원안 폐기 | "한번 더 검토해줘" | 결함을 안은 채 확정 |
| 모순 7건 | "검증해봐" | 전부 집필 단계까지 생존 |
표에 적은 것 전부 사용자가 밀어서 나왔다. 밀지 않았으면 하나도 안 나왔다.
이건 모델이 게을러서가 아니다. 툴킷이 그 절차를 규정하지 않았기 때문이다. 그리고 이 형태의 실패 — 아무도 틀렸다고 말해주지 않는 실패 — 는 지난 네 번의 릴리스에서 계속 다른 얼굴로 나타났다.
무엇을 만들고 있나
webnovel-toolkit 은 한국 웹소설 집필용 Claude Code 플러그인이다. 스킬 13종, 커맨드 5종.
기획(주제·로그라인·세계관)부터 전개 설계, 집필, 원고 검토, 회차 색인, 설정 연속성 감사까지를
다룬다.
LLM에게 소설 상담을 하면 답은 늘 그럴듯하게 나오는데, 일관성이 없다. 같은 질문을 다른 세션에서 하면 다른 기준으로 답하고, 어제 확정한 설정을 오늘은 모른다. 게다가 웹소설에는 순문학과 다른 규칙이 있다 — 회차 단위로 결제되고, 매 화 독자가 이탈을 결정하고, 300화를 버텨야 한다. 범용 LLM은 이 제약을 모른 채 "문학적으로 좋은" 조언을 한다.
그래서 설계 축을 셋으로 잡았다.
- 판단 기준을 파일에 고정한다. 스킬마다 루브릭이 저장소에 있다. 세션이 바뀌어도 같은 잣대가 나온다.
- 작가의 것으로 남긴다. 설정을 대신 완성하지 않는다. 선택지와 트레이드오프까지가 역할이고 결정은 작가가 한다. 작가가 자기 세계를 장악하지 못하면 30화쯤에서 스스로 어긴다.
- 기억을 대체한다. 50화를 넘기면 사람은 자기가 뭘 썼는지 기억하지 못한다. 독자는 기억한다. 그 비대칭이 설정 붕괴의 실제 원인이다.
여기까지는 계획대로 갔다. 계획에 없던 건 그다음이다.
첫 번째 얼굴 — 침묵하는 실패
v1.1에서 고친 버그 하나를 소개한다. 이 툴킷 역사상 최악이었다.
프로젝트 안내 문서가 이렇게 말하고 있었다. "회차 색인을 커밋하려면 .gitignore 에서
index/ 줄을 지우세요."
그런데 색인의 실제 경로는 .webnovel/index 였고, 파일 첫 줄의 .webnovel/* 가 먼저 잡았다.
줄을 지워도 아무 효과가 없었다.
이 버그의 성질이 이렇다.
- 에러가 안 난다
- 경고도 없다
- 사용자는 시킨 대로 했으므로 커밋됐다고 믿는다
- 회차 색인은 원문을 읽어야 채워지는 값이 들어 있어 재생성이 공짜가 아니다
- 그리고 이 사실은 300화를 채우고 기기를 옮길 때 드러난다
고정 문자열로 적힌 경로가, 설정에서 나오는 실제 경로와 달랐다. 원인은 그게 전부다.
고친 방법이 이 글의 요지다. 문서를 고치지 않았다. 아니, 문서도 고쳤지만 그게 해결이
아니었다. .gitignore 를 설정된 실제 경로에서 생성하도록 바꾸고, 통합 점검에
git check-ignore 에 직접 묻는 실효 검사를 넣었다.
문서로 고친 버그는 다시 난다. 검사로 고쳐야 한다.
같은 작업에서 곁가지로 하나 더 배웠다. .gitignore 에서 # 은 줄 첫 글자일 때만 주석이다.
!.webnovel/index/ # 설명 은 되살리기가 아니라 엉뚱한 패턴이 된다. 친절하게 붙인 주석이
파일을 무력화한 것이다. 이것도 침묵한다.
그리고 이번 v1.3 작업 중에 같은 유형을 또 만났다. 검증 스크립트가 스킬 디렉토리를 훑는데,
에이전트 런타임이 작업 디렉토리에 만든 .omc/ 를 스킬로 세고 있었다. 검사가 통째로
실패했다. 이 디렉토리는 gitignore 되어 있어서 커밋에는 안 남고 로컬에서만 깨진다. 원인을
찾기 전까지는 "내 환경만 이상한" 상태다. 점(.)으로 시작하는 디렉토리를 제외하는 세 줄로
끝났지만, 찾는 데는 그보다 오래 걸렸다.
두 번째 얼굴 — 사본
v1.1에서 사본 두 벌을 지웠다.
하나는 스킬 라우팅을 검사하는 스크립트와 그 테스트 데이터였다. 두 곳에 있었고, 스킬 쪽 사본은 아무도 참조하지 않는 낡은 부분집합이었다 — 48개 케이스 중 25개. 검사는 통과하는데 검사하는 대상이 절반이었다.
다른 하나는 README였다. 루트와 플러그인 디렉토리에 통짜 사본이 있었고, 한쪽만 갱신되는 일이 실제로 일어났다.
이때 얻은 결론은 이거였다.
사본은 "같이 갱신하자"는 규칙으로 관리되지 않는다. 한쪽만 갱신되는 것이 기본값이다.
그래서 규칙 대신 검사를 넣었다. 검증 스크립트가 지정된 파일명이 두 곳에 존재하면 실패시킨다. README는 겹치는 본문 자체를 없앴다 — 루트가 본문, 플러그인 쪽은 짧은 카드. 겹치는 게 없으면 갈라질 것도 없다.
그런데 이번 v1.3에서 갈라진 사본을 또 찾았다.
같은 「확정 사항」 표가 기획 스킬의 루브릭 부록과 설정 바이블 스킬의 템플릿 두 곳에 있었다.
컬럼 이름이 한쪽은 결정일, 다른 쪽은 확정일 이었다. 이미 갈라진 뒤였다.
v1.1에서 만든 불변식 검사는 .py 와 .json 파일명 세 개만 봤다. 마크다운 문서 안에
인라인으로 들어 있는 템플릿은 사각지대였다.
불변식을 만들 때 나는 "사본"을 파일 단위로 상상했다. 실제 사본은 파일 안에 있었다. 검사가 개념이 아니라 내가 그때 떠올린 구현을 인코딩하고 있었던 셈이다. 이건 아직 완전히 해결하지 못했다 — 일단은 사본 쪽을 정본을 가리키는 포인터로 바꿔놨고, 문서 내 템플릿까지 기계적으로 잡는 방법은 다음 과제다.
세 번째 얼굴 — 만들었는데 도달할 수 없음
v1.2 이야기다.
웹소설 클리셰는 세 종류로 나눈다. 있으면 오히려 좋은 것(계약), 닳은 것(소모), 그리고 덮게 만드는 것(지뢰).
앞의 둘은 상위작을 해체하면 나온다. 잘 팔리는 작품에 반복되는 패턴이니까. 그런데 지뢰는 안 나온다. 덮은 독자는 순위에 흔적을 남기지 않기 때문이다. 아무리 정교하게 랭킹을 분석해도 "무엇 때문에 덮었는가"는 그 데이터에 없다.
이건 생존 편향의 교과서적 사례고, 툴킷도 알고 있었다. 지뢰 목록 옆에 "커뮤니티 자료 기반이라 시간이 지나면 틀려진다"고 경고까지 적혀 있었다.
그런데 갱신할 수단은 제공하지 않았다. 스스로 유통기한을 표시해놓고 재입고 경로가 없는 상태였다.
그래서 경쟁작 분석 스킬에 여론 축을 신설했다. 독자 커뮤니티에서 하차 사유를 수집하는 절차, 수집 카드 형식, 채택 임계, 랭킹과의 교차 판정. 여기서 제일 신경 쓴 건 층을 섞지 않는 것이었다. 여론을 랭킹과 같은 층에 두면 망가진다. 커뮤니티마다 성향이 편중돼 있고, 불만은 원래 과대표집되며, 무엇보다 까이면서 잘 팔리는 작품이 많다. 그래서 별도 축이고, 트렌드 판정에는 쓰지 않는다.
여기까지는 잘 갔다. 문제는 다 만들고 나서였다.
라우팅을 점검하니 "커뮤니티에서 까이던데"라는 발화가 세계관 구축 스킬로 가고 있었다. 기능은 존재하는데, 그 기능을 원하는 사용자가 도달할 수 없었다. 만들지 않은 것과 동일한 상태다.
원인은 명확하다. 스킬의 description 을 안 고쳤다. 그리고 이게 왜 놓치기 쉬운지도
명확하다 — 스킬 시스템에서 description 은 사람이 읽는 설명문처럼 생겼지만 실제로는
라우팅 테이블이다.
기능 추가와 트리거 추가는 별개 작업이다. 후자를 잊으면 만든 기능이 존재하지 않는 것과 같다.
지금은 라우팅 프로브 52개가 검증 스크립트에서 돌고, 오라우팅이 하나라도 있으면 push를 막는다.
네 번째 얼굴 — 앞만 보는 체크리스트
그리고 이번 v1.3, 맨 앞의 그 표다.
툴킷의 기획 스킬에는 「추천안 자기검증」이라는 절이 있다. 모델이 안을 내놓기 전에 스스로 통과시켜야 하는 다섯 항목이다.
- 이 안이 회차를 생산하는가
- 목표 분량까지 버티는가
- 지뢰 클리셰에 걸리지 않는가
- 작가가 설명 없이 굴릴 수 있는 복잡도인가
- 1화에서 한 문장으로 전달 가능한가
다시 읽어보면 공통점이 보인다. 전부 앞을 본다. 이 안이 앞으로 잘 굴러갈 것인가만 묻는다.
뒤를 보는 항목이 하나도 없다. 이미 확정한 것과 부딪치는지, 기존 결정 중 무엇을 강화하는지, 아무것도 강화하지 않아 고립된 설정은 아닌지 — 묻지 않는다.
그래서 결정이 40개 쌓였을 때 모순이 7건 나왔다. 툴킷은 좋은 안을 만드는 법은 규정했는데, 만든 안을 되돌아보는 법이 통째로 없었다. 그 공백을 사용자가 매번 손으로 메우고 있었고, 안 메우면 모순이 집필 단계까지 살아남는다.
실사용에서 확인된 맹점 중 셋은 특히 구조적이었다.
수치·자원 흐름을 안 본다. 「적립액은 등급에 비례」와 「등급을 낮게 신고해 은신」을 같이 확정하면, 고등급 고객을 잡아도 수입이 없다. 설계 전체가 무너진다. 산수 수준의 모순인데 서사 검토만으로는 안 걸린다. 경제를 보는 항목이 어디에도 없었기 때문이다.
시간 스케일을 안 묻는다. 유지비가 하루 단위로 나가는데 한 회차가 작중 며칠인지 정해져 있지 않으면, 그 게이지는 긴장을 만들지 못한다. 이것도 서사 감각에는 안 걸린다.
"왜 아무도 안 하는가"를 주인공 쪽으로만 묻는다. 툴킷에는 "강력한 수단이 있는데 아무도 안 쓴다면 이유가 필요하다"는 검사가 있었다. 짝이 되는 질문이 없었다 — "왜 적과 체제는 주인공을 아직 잡지 않았는가?" 정체를 숨기면서 눈에 띄는 성과를 내는 설계는 웹소설에서 흔한데, 방어 장치 없이 통과할 수 있었다. 그러면 작가는 1부에서 적을 멍청하게 만들어 넘기게 되고, 그 순간 사이다가 싸구려가 된다.
이 셋은 프롬프트 톤으로는 안 고쳐진다. "꼼꼼히 검토해줘"를 아무리 강하게 써도 안 나온다. 항목으로 강제해야 나온다.
그래서 v1.3에서 넣은 것.
- 자기검증에 뒤를 보는 항목 — 확정된 것과의 대조(충돌하는가 / 아무것도 강화하지 않아 고립됐는가)
- 수치·자원 검사는 조건절이 아니라 판정으로 넣었다. 「이 작품에 수치·자원·타이머가 있는가」를 먼저 답하게 하고, 「있다」가 나온 뒤에 흐름과 회차당 경과일을 묻는다. 조건이 앞에 붙으면 판정을 안 하고 넘어가고, 넘어갔다는 사실이 출력에 남지 않는다
- 확정 직전 질문 하나 — "이 안이 만드는 선택이 실제로 갈등인가, 답이 뻔한가"
- 2~3안을 낸 뒤 관통·합성 가능한지 한 번 더 보기. 배타적이라고 가정하지 않는다
- 결정 기록을 의무화. 이전에는 "작가에게 파일 생성을 제안하라"였다. 제안은 안 하게 된다
- 덧붙이기 금지 한 줄. 새 결정이 무효화하는 기존 문장을 지목하고 그 문장을 고친 뒤 기록을 마친다
- 세계 쪽 주도성 질문.
"아직 눈치 못 챘다"는 답이 아니다 - 파급 점검에 역방향 — "이미 정한 것 중, 이 설정 때문에 거짓이 되는 것은 무엇인가"
마지막 것을 조금 더 설명하고 싶다. 원래는 정방향만 있었다. "이 설정이 경제·계층·전쟁·일상에 무엇을 바꾸는가." 이건 새로 생기는 것을 묻는다. 역방향은 무너지는 것을 묻는다. 그리고 "충돌하는가"보다 "거짓이 되는가"가 잡는 범위가 넓다 — 두 설정이 서로 부딪치지 않아도, 새 메커니즘 때문에 이미 깔아둔 전제가 논리적으로 반대 결론을 내게 되는 경우가 있다. 실제로 그렇게 됐다.
덧붙이기 금지는 모델의 성질에 대한 것이다. 새 결정이 기존 문서의 특정 문장을 무효화하는데 그 문장이 그대로 남는 일이 반복됐다. 모델은 덧붙이기는 잘하고 고쳐쓰기는 잘 안 한다. 덧붙이는 건 쉽고, 무엇을 고쳐야 하는지 찾는 건 어렵기 때문이다. 그래서 이건 습관이 아니라 절차로 뒀다.
다섯 번째 얼굴 — 자기 검토는 자기가 판 함정을 못 본다
여기까지 쓰고 나서, 이 변경 자체를 독립 검토에 넘겼다. 판정 기준을 바꾸는 릴리스이기 때문이다.
검토는 두 가지를 잡았고, 둘 다 내가 직전에 확인하고 "이상 없음"이라고 보고한 것이었다.
작업 파일 13개가 CRLF 로 뒤집혀 있었다. 저장소는 순수 LF 인데 편집 과정에서 전부 CRLF 가
됐다. .gitattributes 에 * -text(줄바꿈 변환 금지)가 걸려 있어서 그대로 커밋되면 13개
파일 전 줄이 재작성된다. diff 가 2,887줄이었는데 공백을 무시하면 356줄이었다.
나는 이걸 앞서 검사했다. 그리고 "혼입 없음"이라고 적었다. 그때 쓴 스캔이 거짓 음성을 냈고, 검사가 초록이라는 사실이 검사가 작동한다는 뜻은 아니라는 걸 확인하지 않았다.
그리고 그 CRLF 가 새로 만든 검사의 실패를 가리고 있었다. LF 로 되돌리자 「도달 경로 동기화」가 즉시 실패했다. 원인은 같은 파일 안에 이미 적혀 있었다 — 프로브 개수를 세는 함수의 주석이 이렇게 경고한다.
파일을 재포맷하면 전 줄이 추가로 보이고, 그 순간 모든 스킬이 면제된다 (이 검사를 만들면서 실제로 그랬다).
그 방어를 자기 함수에만 넣고 바로 옆 함수에는 안 넣었다. 같은 함정에, 같은 파일 안에서, 두 번 빠졌다. 두 번째는 첫 번째를 주석으로 적어둔 상태에서 빠졌다.
이게 이 글 전체의 요약이다. 아는 것과 적용하는 것은 다른 일이고, 그 간극은 주의력으로 메워지지 않는다.
검토를 다섯 번 돌린 방식
남은 미결 두 건을 판정하려고 독립 검토를 다섯 번 돌렸다. 같은 질문을 다섯 번 묻지 않았다. 중복 검토는 같은 것을 다섯 번 놓친다.
| 렌즈 | 무엇을 보는가 |
|---|---|
| 작가 실사용 | 사용자가 실제로 그 경로를 밟는가 |
| 지시문 효력 | 이 문구가 모델 행동을 바꾸는가, 표현만 바뀌는가 |
| 유지보수 비용 | 4곳에 같은 문장 = 사본인가 |
| 반박 | 기본 입장을 「불필요」로 두고 공격 |
| 약속 이행 | 안 넣으면 릴리스 노트의 어느 문장이 거짓이 되는가 |
넷째는 편향을 의도적으로 반대로 걸었다. Claude Code 공식 문서가 경고한다 — "gap 을 찾으라고 지시받은 리뷰어는 작업이 멀쩡해도 대개 뭔가 보고한다." 다섯이 모두 같은 방향으로 기울면 결과는 과설계다.
다섯째가 판정 기준을 가장 날카롭게 만든다. 거짓이 되는 문장이 없으면 필수가 아니다. 「필수」를 취향에서 검증 가능한 것으로 옮긴다.
그리고 정직하게 적어야 하는 것
독립 검토는 6회 시도해 2회 회수됐다. 다섯 렌즈는 전부 회수 실패했다. 에이전트가 결과 없이 유휴 상태로 빠지는 일이 반복됐고, 외부 CLI 로 우회하려던 시도는 계정이 모델을 거부해 세 번 다 실패했다.
그래서 릴리스 노트에 이렇게 적었다 — "6회 시도해 2회 회수했다." "충분히 검토했다"가 아니다.
이건 사소한 표현 차이가 아니다. 이 저장소의 릴리스 노트는 "무엇을 고쳤다"가 아니라 "무엇이 잘못되어 있었다"로 쓴다는 규칙이 있다. 같은 이유로 검증 기록도 시도가 아니라 결과로 쓴다. 회수 실패를 성공으로 적으면, 다음 릴리스에서 그 기록을 근거로 삼는 순간 거짓말이 된다.
넣지 않기로 한 것
개선 제안이 열 개 들어왔다. 버린 것들을 여기 남겨둔다 — 기각 근거가 채택 근거보다 재사용성이 높은 경우가 있어서.
새 정본 문서를 만들자는 제안은 안 받았다. 여러 스킬이 같은 문장을 들어야 할 때만 정본이 값을 한다. 살아남은 항목 중 스킬을 넘나드는 건 한 줄뿐이었다. 한 줄 때문에 파일을 만들면 읽히지 않는 문서가 하나 늘어날 뿐이다.
설정 강화도 지표는 새 설정이 기존 몇 개와 맞물리는지 세어서 0~1개면 삭제 후보로 보자는 제안이었다. 관찰 자체는 그럴듯했지만 두 가지가 걸렸다. "맞물린다"의 정의가 없어 셀 수 없고, 무엇보다 방향이 반대다. 새 설정은 정의상 연결이 적다. 그걸 삭제 기준으로 쓰면 신설 결정만 골라 지우고 기존 결정만 살아남는다. 같은 것을 카운터 없이 잡는 항목(「고립」)이 이미 자기검증에 들어갔으므로 지표는 버렸다.
의존 관계를 양방향으로 적자는 제안 — 의존하는 결정 과 이 결정에 의존하는 것 두 컬럼을
두자는 것이었다. 이건 v1.1에서 배운 것이 그대로 적용된다. 둘 다 손으로 유지하면 반드시
한쪽만 갱신된다. 한 방향만 적고 역방향은 조회로 얻는다.
결정 기록 의무를 다른 세 스킬로 넓히는 건 미뤘다. 이건 기각이 아니라 이월이고, 그 차이를 적어두는 게 중요하다.
근거는 있었다. 실사용에서 모순 5건의 뿌리였던 한 변수가 세 곳에 쓰였는데, 그 셋 중 둘이 기록 의무가 있는 스킬 밖이었다. 감사 대상이 처음부터 반쪽이다.
그런데도 안 넣은 이유는 범위다. 같은 취지의 문장이 네 곳에 흩어지면, 이 저장소 기준으로 그건 한 줄 추가가 아니라 사본 관리 대상이 된다 — 검사로 동일성을 강제해야 하는 것인지부터 판단해야 한다. 그 판단을 받으려고 돌린 렌즈가 회수에 실패했다.
필요성은 인정하되 범위 설계 없이 넣지 않는다. 이 저장소가 사본으로 이미 두 번 대가를 치렀기 때문이다.
적대 검토 세 질문 중 둘도 뺐다. "가장 약한 고리는 무엇인가", "이 안이 틀렸다면 그 이유는" — 둘 다 범용 자기비판 문구고, 모델이 이미 늘 하는 말이라 실효 증거가 없었다. 실제로 결함을 잡은 건 세 번째 질문("이 선택이 실제로 갈등인가")뿐이라 그것만 남겼다.
정리 — 네 번의 릴리스에서 반복된 것
| 원리 | 어디서 대가를 치렀나 |
|---|---|
| 문서로 고친 버그는 다시 난다. 검사로 고쳐야 한다 | .gitignore 침묵 실패, 런타임 디렉토리 오검출 |
| 사본은 "같이 갱신하자"로 관리되지 않는다. 한쪽만 갱신되는 게 기본값이다 | 테스트 데이터, README, 문서 내 템플릿 |
| 못 믿는 게이트는 없는 게이트보다 나쁘다 | CI를 껐다 |
| 기능 추가와 트리거 추가는 별개 작업이다 | 도달할 수 없던 여론 축 |
| 모델은 절차가 없으면 앞만 본다 | 자기검증 다섯 항목 |
| 한계를 말하지 않는 도구는 사용자가 자기 탓을 하게 만든다 | 첫 릴리스의 「알려진 한계」 |
| 검사가 초록인 것과 검사가 작동하는 것은 다르다 | CRLF 스캔이 거짓 음성을 냈다 |
| 아는 것과 적용하는 것은 다른 일이다. 간극은 주의력으로 안 메워진다 | 함정을 주석에 적어두고 옆 함수에서 그 함정에 빠졌다 |
마지막으로 한 가지. 이 툴킷의 패치노트는 "무엇을 고쳤다"가 아니라 "무엇이 잘못되어 있었다"로 쓴다는 규칙이 있다. "gitignore 수정"이 아니라 "색인이 커밋되지 않는데 커밋된다고 안내하던 버그"라고 적는다.
6개월 뒤에 그 항목을 찾는 사람은 증상으로 검색하기 때문이다. 그리고 이 글도 그 규칙을 따라 썼다.
webnovel-toolkit v1.3.0 작업 기록. 배포하지 않는 개인 도구다.