gwimong's blog Software Engineer

artifact-transformer 작업 일지 #16

설정을 실행 코드에서 떼어내고, 라운드트립 점검을 일급 명령으로 — 도구가 스스로를 검증하는 방식을 정리하다


이 글은 2026-06-17 하루 동안 artifact-transformer 자신의 구조를 정리한 작업 기록이다. 설정 파일을 실행 코드에서 떼어내 env/로 분리하고 경로 해석을 한곳으로 모았으며, 손으로 두 단계 밟던 라운드트립 전체 점검을 roundtrip.py 일급 명령으로 끌어올렸다.

배경

artifact-transformer는 Jira 이슈와 PlantUML 다이어그램을 입력으로 받아 요구사항·유스케이스·기능·시퀀스 명세서와 데이터 정의서/명세서 등을 docx/xlsx로 자동 생성·역파싱하는 도구다 (#4 참조).

#15까지는 시선이 바깥을 향해 있었다 — 한 시스템에서 내린 결정을 도구·스킬·규약으로 끌어올려 모든 시스템이 상속하게 만드는 일. 여러 프로젝트를 가로지르며 규약을 일반화했다.

오늘은 시선을 안으로 돌렸다. 규약을 바깥으로 퍼뜨리는 동안, 정작 도구 자신의 살림에는 두 가지 빚이 쌓여 있었다 — 설정과 실행 코드가 한 평면에 뒤섞여 있었고, 도구의 핵심 보증인 생성↔파싱 가역성 점검이 여전히 손으로 밟는 두 단계였다. 둘 다 “돌아가긴 하는데 제자리에 있지 않은” 것들이었다.

문제

손대기 전, 루트 디렉터리를 열면 보이던 두 가지.

  1. 설정 파일이 실행 코드와 같은 자리에 섞여 있었다 — project.*.env 8종과 *.env.example이 CLI 진입점(generate.py, parse.py 등)과 같은 평면에 흩어져, 루트가 무엇을 실행하는 곳인지 무엇을 설정하는 곳인지 한눈에 갈리지 않았다.
  2. 라운드트립 점검이 일급 명령이 아니었다 — “전체 생성(generate all --samples) → 전체 역파싱(parse all)“으로 가역성을 확인하는 절차가, 도구의 정합성을 보증하는 핵심인데도 매번 두 명령을 손으로 이어 밟고 결과를 눈으로 맞춰 보는 일이었다.

결정 1 — 설정을 실행 코드에서 떼어내 env/로

루트에 평면으로 깔려 있던 설정을 한 디렉터리로 모았다. 무엇을 실행하는 자리와 무엇을 설정하는 자리를 갈라, 루트를 열었을 때의 인지 부하를 줄이는 게 목표였다.

  • project.*.env 8종과 *.env.example 2종을 env/로 옮겼다. 단순 이동이 아니라 git mv로 옮겨 파일 이력을 보존했다 — 설정 한 줄이 언제 왜 바뀌었는지를 잃지 않으려고.
  • common/env.py에 default_project_env() / default_system_env()를 신설했다. env/를 먼저 보고, 없으면 레거시 루트로 fallback한다. 7개 진입점에 흩어져 있던 하드코딩 경로를 이 두 함수로 교체해, 경로 해석을 한곳으로 모았다.
  • 경로가 바뀌면 그 경로를 아는 모든 곳이 따라와야 한다 — .gitignore·ci.yml·README·스킬 문서의 env 경로를 함께 갱신했다.

대신 CLI 진입점 자체는 루트에 남겼다. 진입점까지 하위 디렉터리로 내리면 import 경로가 흔들리기 때문이다. 정리의 명분보다 import 안정성이 먼저였다 — 설정은 옮기되, 실행 표면은 건드리지 않는다.

검증은 세 겹으로 걸었다 — pytest 127건 통과, check.py samples --strict 무오류, 그리고 라운드트립 정상. 경로를 건드리는 변경일수록 “돌아가는 것처럼 보이는데 한 군데가 어긋난” 사고가 나기 쉬워서, 옮긴 직후 끝까지 한 번 통과시켜 두는 게 안전하다.

루트에 실행과 설정이 섞여 있으면, 새로 들어온 사람은 무엇을 건드려도 되는지부터 헷갈린다. 설정을 한 디렉터리로 모으고 경로 해석을 한 함수로 모으면, “여기는 실행, 저기는 설정”이 구조로 드러난다.

결정 2 — 라운드트립 전체 점검을 일급 명령으로

도구의 핵심 보증은 생성한 산출물을 다시 역파싱했을 때 처음으로 돌아온다는 가역성이다. 그런데 그 보증을 확인하는 일이 여태 두 명령을 손으로 잇는 절차였다. 핵심일수록 일급 명령이어야 한다는 생각에서, 양방향 점검을 하나의 명령으로 끌어올렸다.

  • roundtrip.py를 신설했다. 두 오케스트레이터(생성·파싱)를 그대로 재사용하고, 종료 코드와 산출물/JSON 개수로 단일 판정을 내린다. 사람이 두 출력을 눈으로 맞출 필요가 없다.
  • 점검 자체가 부작용을 남기면 안 되므로 번들 샘플 모드·비파괴로 설계했다. project.env가 없어도 돌고(--work-dir로 임시 작업 공간을 쓴다), --diff로 어긋난 지점을, --keep으로 산출물 보존을, -q로 조용한 출력을 고를 수 있다.
  • tests/test_roundtrip_orchestrator.py에 오케스트레이터 레벨 end-to-end 2건을 추가했다. 명령이 일급이 되었으니 테스트도 명령 단위로 붙인다.
  • .claude/commands/roundtrip.md로 /roundtrip 안내 계층을 두어, 결과 해석과 실패 단계 구분(생성에서 깨졌나, 파싱에서 깨졌나)을 사람이 바로 읽게 했다.
  • CI도 일원화했다 — ci.yml의 round-trip 단계를 roundtrip.py -q 한 줄로 모았다. 로컬에서 치는 명령과 CI가 치는 명령이 같아야, “로컬은 되는데 CI만 깨진다”가 줄어든다.

README의 커맨드 표·라운드트립 단축 명령·디렉터리 트리·테스트 목록도 함께 갱신했다. 검증은 pytest 129건 통과(신규 2건 포함), roundtrip.py 자체 통과(생성 39·파싱 31, 실패 0).

매번 손으로 두 단계를 밟아 눈으로 맞추는 점검은, 바쁠 때 가장 먼저 생략된다. 한 명령에 단일 판정으로 묶어 두면, 점검은 “돌렸나 안 돌렸나”의 문제로 단순해진다.

의도치 않은 함정

  • 경로를 옮기는 변경은 따라오는 곳이 많다 — env를 env/로 옮기는 순간 진입점 7곳뿐 아니라 .gitignore·CI·README·스킬 문서까지 같은 경로를 알고 있었다. 한 군데라도 빠지면 “로컬은 되는데 CI만 못 찾는” 식으로 뒤늦게 터진다. env/ 우선·루트 fallback의 2단 해석을 둔 덕에, 옮기는 중에도 옛 경로가 죽지 않아 한 번에 갈아엎지 않을 수 있었다.
  • 이 폴더 구조 규약은 그 자리에서 메모리에 박아 두었다 — 다음 시스템이 들어올 때 같은 자리에서 다시 고민하지 않도록, “실행은 루트, 설정은 env/”를 규약으로 남겼다. #15의 결을 그대로 잇는 처리다.

마무리

오늘 관통한 결은 하나였다 — 도구가 스스로를 어떻게 두고 어떻게 검증하는지를 제자리에 놓은 것.

  • 설정을 실행에서 떼어냈다 — env/로 분리하고 경로 해석을 두 함수로 모았다. 루트를 열면 실행과 설정이 구조로 갈린다.
  • 핵심 보증을 일급 명령으로 — 손으로 두 단계 밟던 라운드트립을 roundtrip.py 단일 판정으로 끌어올리고, 테스트·슬래시 커맨드·CI까지 같은 명령으로 묶었다.

어제까지가 규약을 바깥으로 퍼뜨린 날이라면, 오늘은 그 규약을 만든 도구 자신의 살림을 정리한 날이다. 설정과 실행이 갈리고, 가역성 점검이 한 명령이 되면, 다음 변경은 더 적은 마찰 위에서 시작한다.

다음에 할 것:

  • 신규 4종 프로젝트 부트스트랩 실행 — env만 세워 둔 프로젝트들을 init.py 그린필드 절차에 실제로 태운다(#15 결정 1).
  • 문서 재생성(docx/xlsx) — 버전·규약·구조 변경분이 쌓였으니 일괄 재생성으로 산출물을 맞춘다.
  • 위험도 평가 AI 역량 상세화 — #15에서 기획으로 얹은 RAG·군집 위험 역량을 요구사항·유스케이스·시퀀스까지 내린다.

Comments

Content