gwimong's blog Software Engineer

artifact-transformer 작업 일지 #23

숨긴 기능, 두 번 틀린 분류, 그리고 순서가 논리인 라운드트립 — 같은 날 세 갈래로 흩어진 손질


이 글은 2026-07-08 하루의 기록이다. 같은 도구를 두 채널(터미널 세션, Agent Mode 세션)에서 동시에 손봤고, 그 사이 하위 프로젝트 SUOMS에서는 비즈니스 모델 재정의가 도구를 타고 흘러들어왔다. 하루치 로그를 다시 읽으며 남는 건 결과물 목록이 아니라, 어디서 사람이 잘못 짚었고 어디서 도구가 대신 걸러 줬는지다.

배경 — 같은 웹 에디터, 두 채널

artifact-transformer의 웹 에디터는 로컬 스크립트에서 Git 기반 협업 웹앱으로 넘어가는 중이다(server/ 파이썬 백엔드 + 정적 JS/HTML 프런트, pytest 스위트). 이날은 그 에디터의 UX를 두 채널에서 나란히 건드렸다 — 터미널 세션에서 세 가지 요청을 처리했고, 별개로 Agent Mode 세션에서 화면 구조를 놓고 대여섯 라운드를 왕복했다. 두 채널 다 같은 결론에 닿았다: 에디터가 사용자보다 먼저 판단을 내리고 있었다. 있는 기능을 없는 것처럼 숨기고, 데이터를 사용자가 원하지 않는 기준으로 묶었다.

그와 별개로, 도구가 적용되는 하위 프로젝트 SUOMS에서는 비즈니스 모델 자체가 다시 그어졌다 — 8개 묶음형 업무가 20개 단일 액션 업무로. 이쪽은 UX 이야기가 아니라, 라운드트립을 지키는 규칙이 “순서”라는 사실을 다시 확인시켜 준 하루였다.

문제

  1. 이미 구현된 기능이 조건부로 안 보이면, 사용자는 그걸 “없다”고 판단한다 — 어디서 이런 일이 났나.
  2. “분류”라는 한 단어를 사용자와 에이전트가 서로 다르게 이해하면 무슨 일이 벌어지나.
  3. API 키 없이 AI를 부르고 싶다는 요청과, 그걸 편법으로 풀지 않겠다는 원칙은 어떻게 공존하나.
  4. SUOMS 다이어그램 라운드트립에서 “내용”이 아니라 “순서”가 진실인 이유는 무엇인가.

결정 1 — 숨긴 기능부터 다시 꺼냈다

“파일 목록이라던가 현황, 수정 폼 등 UX가 고려되어 있지 않아”라는 한 줄에서 시작한 터미널 세션은, 코드를 새로 짜기 전에 먼저 이미 있는데 숨어 있던 기능부터 찾아냈다.

ANTHROPIC_API_KEY가 없으면 “AI 지시” 패널 자체가 렌더링에서 빠지도록 짜여 있었다. 기능은 구현돼 있는데, 환경변수 하나가 없다는 이유로 화면에서 통째로 사라진 것이다. 사용자 입장에서는 “이 기능이 없다”와 “이 기능은 있는데 켜는 법을 모른다”를 구분할 길이 없다. 고친 방향은 단순했다 — 버튼은 항상 보이게 두고, 비활성 상태에는 “활성화 방법”을 인라인으로 붙였다.

같은 라운드에서 초기 로드 시 “저장 안 된 변경사항 있음” dirty 플래그가 잘못 켜지는 버그도 잡았다. 둘 다 기능 자체의 결함이 아니라, 상태를 사용자에게 정직하게 보여주지 못한 결함이었다.

이후 한국어 제목·그룹으로 묶은 파일 목록(검색, 최종편집자·상대시간 배지), Ctrl+S와 dirty dot이 붙은 저장 툴바, 검색·페이지네이션(15개씩)이 있는 배열 에디터, 클릭 가능한 요약 카드와 stale/latest 배지를 가진 대시보드를 붙였다. 255개 테스트와 라이브 서버 스모크 테스트(/ui, /assist info, /status)로 확인했다.

감춰진 기능은 없는 기능과 사용자 눈에 똑같이 보인다. 조건부 렌더링은 상태를 숨기는 UI가 아니라 상태를 오독시키는 UI다.

결정 2 — claude.ai 대신 CLI를 백엔드로: ToS 경계 안에서 이중화

“claude.ai 대신 API 키 없이 AI 호출 가능한가?”라는 요청에는 두 가지 길이 있었다. 하나는 claude.ai 웹세션을 쿠키 기반으로 흉내 내는 것 — 빠르지만 Anthropic ToS를 건드릴 소지가 있다. 다른 하나는 공식적으로 지원되는 headless Claude Code CLI(claude -p)를 API 키가 없을 때의 대체 백엔드로 쓰는 것이다.

전자는 명시적으로 거부했다. 대신 API 키가 있으면 API를, 없고 CLI에 로그인돼 있으면 CLI를 부르는 이중 백엔드로 설계했다. 편의를 위해 경계를 넘지 않고, 대신 공식 경로 안에서 이중화 폭을 넓힌 셈이다.

결정 3 — Markdown·PlantUML을 자체 렌더링한 이유

Markdown 내보내기와 PlantUML 뷰어를 붙이면서도 같은 원칙이 두 번 반복됐다. 기존 markdown.py(docx/xlsx → Confluence용 Markdown)와 generate.py --md를 웹앱에 연결하면서, Markdown 렌더러를 CDN이나 외부 라이브러리 없이 직접 짰다(제목·표·목록·링크·코드) — 프로젝트의 “no-CDN” 원칙을 지키기 위해서다.

PlantUML 쪽은 더 명확한 이유가 있었다. plantuml.com에 소스를 그대로 임베드하는 방식은 편하지만, 그러면 내부 산출물 소스가 외부 서버로 흘러나간다. 그래서 로컬 render.py + tools/plantuml.jar로 서버사이드 SVG 렌더링을 택했고, .puml 소스가 더 최신일 때만 재생성하는 캐싱을 붙였고, Java·jar가 없으면 503을 돌려주게 했다. 263개 테스트와 라이브 E2E(생성+md → 웹 뷰, puml → SVG 렌더)로 검증했다.

이 렌더러와 이중 백엔드 둘 다 새로 쓴 보안·ToS 민감 표면이다. PlantUML 로컬 렌더 경로는 호스트에 Java와 jar가 있다는 전제 위에서만 돈다 — 둘 다 사람이 한 번 더 눈으로 봐야 할 자리로 남겨 둔다.

결정 4 — “분류”를 두 번 틀리고서야 맞췄다

Agent Mode 세션은 같은 에디터의 화면 구조를 놓고 대여섯 라운드를 왕복했다. 1차에서 평면적이던 문서별 에디터를 “프로젝트 정보”(접기 가능, 스키마 allOf 병합), “문서 정보”(리비전을 표로), 핵심 필드 강조 세 구역으로 나눴다.

2차에서 사용자는 “프로젝트 정보가 문서마다 반복되면 안 된다”며 데이터 분류 기준 자체를 다시 설계하라고 했다. AskUserQuestion으로 방향을 확인했고, 사용자는 에이전트가 추천한 “통합 문서 뷰” 대신 **“도메인 중심 뷰”**를 명시적으로 골랐다. PROJ-TYPE-DOMAIN-NNN 식별자를 파싱해 도메인 레일을 짰다.

3차에서 사용자가 정정했다 — “분류”란 도메인이 아니라 업무·기능·데이터·시나리오·유스케이스·시험 같은 산출물 유형을 뜻한 것이었다. 도메인 보드는 폐기하고, document_codes.py를 역매핑하는 /api/doc-codes 엔드포인트를 새로 만들어 유형 기준 통합 워크스페이스로 다시 짰다.

4차에서 그 새 엔드포인트가 버그를 하나 물었다 — “이 데이터에 정의된 산출물이 없습니다” 오류. 프런트가 /api/doc-codes 응답에만 의존했는데, 서버가 재시작되기 전이면 빈 응답이 돌아왔다. document_codes.py에서 뽑아낸 15개 유형 전체의 클라이언트 사이드 폴백 맵을 서버 응답 우선으로 병행시켜 메웠다.

5차에서 따로 있던 “뷰어” 탭을 산출물 파일 목록에 합쳐 클릭 시 PlantUML·Markdown이 인라인으로 펼쳐지게 했고, 6차에서 diagram_links.py 기반 다이어그램 산출물(아키텍처/C4, 플로차트, ERD 등 10종)을 /api/diagram-kinds로 추가했다 — 처음엔 독립 사이드바 그룹으로.

7차, 세션이 끝나는 지점에서 사용자가 다시 한 번 정정했다. “다이어그램은 원본 데이터 세션에 속해야 한다” — 업무 구성도·흐름도는 업무 정의서·명세서와 같이 있어야지, 별도 목록이면 안 된다는 것이다. DOC_DIAGRAMS 소유 매핑을 도입했다(업무→BCDG/BPDG, 유스케이스→UCDG, 시퀀스→SQDG, 물리데이터→ERDG). 느슨한 키워드 매칭을 정확한 파일명-약어 매칭으로 바꿨고, 소유 데이터 세션이 없는 다이어그램(아키텍처/C4, DFD, 상태흐름도)만 독립 세션으로 남겼다. ERD를 물리/논리 데이터 중 어디에 붙일지, DFD·상태흐름도도 특정 세션에 배정할지는 세션이 끝날 때까지 미해결로 남았다.

같은 “분류”라는 말이 라운드마다 다른 걸 가리켰다 — 도메인, 유형, 그다음엔 소유 관계. 세 번 다 사람이 먼저 자기 머릿속 기준을 언어로 옮기다 어긋났고, 에이전트는 그때마다 이전 구조를 버리고 다시 짰다. 분류 기준을 코드로 옮기기 전에 그 기준이 도메인인지 유형인지 소유 관계인지부터 합의해야 한다 — 이번엔 그 합의가 코드를 세 번 갈아엎고 나서야 됐다.

결정 5 — SUOMS: 순서가 곧 논리인 자리

하위 프로젝트 SUOMS는 이날 비즈니스 모델을 8개 묶음형 업무에서 20개 단일 액션 업무(COM/AST/PLN/OPS, DEF는 SAF로 개명)로 다시 그었다. 이 변화가 다이어그램과 JSON을 타고 내려가는 과정에서, 라운드트립을 지키는 규칙이 내용이 아니라 위치라는 사실이 다시 드러났다.

C4 라운드트립 다이어그램(level1~3 CDG)을 손대기 전에 initializer/init_participant.py와 common/json_diff.py를 먼저 읽었다. 비교기는 내용이 아니라 배열 순서 기반으로 동작했다 — participant.json의 배열 순서와 PlantUML에서 파싱되는 순서가 원소 단위로 정확히 일치해야 라운드트립이 통과된다. 이 사실 하나가 이후 모든 배치를 결정했다. Person ep 삭제, mechanic을 admin으로 재매핑하되 라벨은 다르게 유지해 별개 엣지로 인식되게 하고, sysadmin을 privacy로 개명하고, operator를 추가하고, System_Ext(sulas, ...)를 externalSystems 4번 인덱스에 놓았다. sulas 관련 관계 2건은 CMPDG-API 파일 맨 끝에 배치했다 — json에서 46·47번 인덱스였기 때문이다. “논리적으로” 더 맞아 보이는 CTXDG에 넣었다면 순서가 깨져 라운드트립이 실패했을 것이다. 검증 결과는 cdg participant.json 일치 [+0 -0 ~0], 레벨 커버리지도 통과.

같은 날, 표시 전용(비-라운드트립) BPDG/DFDG/SFDG 다이어그램은 OPS 업무를 5개에서 6개로 더 쪼개고(사전 브리핑·현장 브리핑·운용 기록·디브리핑·비행 실적 등록), “통합 보고서 산출” 업무와 외부 SULAS 연동을 완전히 제거하는 쪽으로 정리됐다. “운용 기록” 업무 범례에는 실시간 이벤트 로깅·Failsafe·비상 대응·비행 기록 관리가 SUOMS 범위 밖이라고 명시했다 — 시스템 책임 경계를 그림에 직접 박아 둔 것이다. 렌더 체크 35개 대상이 전부 통과했고, SULAS·통합보고서 잔존 참조는 grep으로 0건임을 확인했다.

그런데 같은 날 다른 서브에이전트의 작업 로그를 보면, 20개 업무 전체의 BPDG/DFDG/SFDG를 다시 구성하면서는 오히려 외부 시스템 SULAS를 OPS-005(통합 보고서)로 유입되는 흐름으로 추가한 흔적이 있다. 정확한 선후관계까지는 로그만으로 확정하기 어렵지만, 하루 사이에 “SULAS를 다이어그램에 어떻게 반영할지”에 대한 결정이 최소 한 번은 뒤집혔다는 뜻이다. 요구사항이 하루 안에서도 실시간으로 흔들렸다는 정황으로 남겨 둔다.

이와 별개로 읽기 전용 orphan·remap 감사도 돌았다. requirement.json 등 77개 요구사항 중 70개는 새 업무 ID로 재매핑, 7개는 삭제 대상(데이터수집계획 2건, 비상대응·로깅 관련 5건)으로 식별됐고, 연쇄적으로 함수 7개·유스케이스 2개·시퀀스 2개·통합 시나리오 1건·단위테스트 3건이 삭제 후보로 딸려 나왔다. 액터 ep(외부 조종사)는 완전히 unused가 돼 participant.json에서 제거 대상으로 잡혔다. 애매한 매핑(예: REQ-COM-003 장애대응이 COM-001·COM-003 중 어디로 가야 하는지)은 자동 결정하지 않고 ⚠ 플래그로 남겼다 — 이 스펙은 “스크립트로 나중에 적용”하기 위한 것이고, 모호한 매핑은 사람이 확인하기 전까지 보류다.

“논리적으로 맞는 자리”와 “라운드트립이 통과하는 자리”가 다를 수 있다는 게 이날의 핵심이다. 비교기가 위치 기반이라는 사실을 먼저 읽지 않았다면, sulas 관계를 CTXDG에 “정리해서” 넣었을 것이고 그 순간 라운드트립은 깨졌을 것이다. 도구의 계약을 읽는 것과 도메인을 이해하는 것은 서로 다른 일이고, 후자만으로는 전자를 통과할 수 없다.

의도치 않은 함정 — stale 캐시가 검증을 무력화시켰다

Agent Mode 세션 내내 반복된 문제가 하나 있었다. 샌드박스의 mcp__workspace__bash Linux 마운트가, Windows 쪽 Edit 도구로 수정한 뒤의 index.html을 계속 stale 캐시로 서빙했다 — 수정 후에도 옛 줄 수 그대로였다. 그 상태에서 node --check를 실제 파일에 돌리면 결과를 신뢰할 수 없다.

매 라운드 반복된 우회책은 이랬다: 수정한 JS 블록을 세션 outputs 폴더 아래, bash가 한 번도 본 적 없는 새 파일명으로 따로 써서 그 파일만 격리 검증하고, 실제 파일과의 일치는 Grep·Read로 눈으로 대조했다. 거의 매 라운드 이 패턴이 반복됐다 — 검증 도구 자체가 낡은 스냅샷을 붙들고 있으면, 검증을 믿는 대신 검증 경로를 매번 새로 파야 한다는 걸 몸으로 확인한 셈이다.

같은 세션에서 잡은 자잘한 수정 두 건도 남겨 둔다. onclick에 인라인 JSON을 박아 넣던 openItem은 특수문자가 섞이면 깨지는 취약한 구조였는데, 인덱스 기반 배열 조회로 바꿔 해결했다. generateThisDoc은 호출 시점이 아니라 읽기 시점의 문서명을 캡처하고 있던 tautology 버그였다 — 캡처 시점을 호출 시점으로 옮겨 고쳤다.

마무리

이날은 결이 다른 세 종류의 손질이 겹쳤다.

  • 숨긴 기능을 정직하게 드러내기 — 조건부로 사라지던 “AI 지시” 패널과 잘못 켜지던 dirty 플래그를 고쳤다. 기능이 있는데 없어 보이는 상태가 가장 나쁜 상태였다.
  • 경계를 지키는 이중화 — claude.ai 웹세션 흉내는 거부하고 headless CLI를 공식 대체 백엔드로 세웠다. Markdown·PlantUML도 CDN·외부 서버 대신 자체 렌더링으로 프라이버시와 재현성을 지켰다.
  • “분류”를 세 번 다시 정의하기 — 도메인 → 유형 → 소유 관계로, 매번 사용자가 정정하고 에이전트가 구조를 갈아엎었다. 말 하나가 코드 구조를 세 번 바꿨다.
  • 위치가 논리인 자리 — SUOMS 라운드트립은 내용이 아니라 배열 순서로 판정한다는 사실이 모든 배치 결정을 지배했고, 같은 날 SULAS를 넣을지 뺄지에 대한 결정 자체가 한 번 뒤집힌 흔적도 남았다.
  • 검증 도구가 낡으면 검증을 새로 파야 한다 — stale bash 마운트 아래에서는 격리된 새 파일로 우회 검증하는 패턴이 거의 매 라운드 반복됐다.

다음에 남는 것:

  • Agent Mode 세션에서 미해결로 끝난 다이어그램-데이터 세션 소유 배정(ERD, DFD, 상태흐름도)을 마저 결정한다.
  • SUOMS의 SULAS 취급이 하루 안에 뒤집힌 정황을 다음 세션에서 확정 짓고, 최종 결정을 명시적으로 문서화한다.
  • 커스텀 Markdown 렌더러·API/CLI 이중 백엔드·로컬 PlantUML 렌더 경로, 이 세 가지 신규 보안 표면은 사람이 한 번 더 검토한다.

Comments

Content