모두를 지배하는 하나의 스키마: Config v2 재작성
요약
AI 코딩 에이전트 akm의 설정 관리 시스템을 Config v2로 재작성하여 기술 부채를 해결한 과정을 다룹니다. 기존의 파편화된 파서들을 단일 Zod 스키마로 통합하여 설정 오류를 방지하고 시스템 안정성을 높였습니다.
핵심 포인트
- 1,400줄의 레거시 파서를 단일 Zod 스키마로 통합
- 오타나 잘못된 JSON 입력 시 조용히 기본값으로 넘어가는 문제 해결
- 설정 파일 누락 및 손상에 대한 명확한 에러 처리 구현
- 설정 구조의 일관성 확보를 통한 유지보수 효율성 증대
이 글은 AI 코딩 에이전트(AI coding agents)가 의존하는 점점 늘어나는 기술, 스크립트 및 컨텍스트(context)를 관리하는 시리즈의 16번째 파트입니다. 0.8.0 릴리스 노트는 이번 재작성과 함께 배포된 저장소 및 파이프라인(pipeline) 변경 사항을 다루며, 13번째 파트는 새로운 profiles.improve 설정이 어떻게 개선 파이프라인(improve pipeline)을 구동하는지 다룹니다.
설정 파일(Config files)은 프로젝트가 조용히 기술 부채(technical debt)를 쌓아가는 곳입니다. 새로운 기능이 추가될 때마다 새로운 키(key)가 생깁니다. 새로운 키가 생길 때마다 새로운 파서(parser)가 생깁니다. 각 파서는 약간씩 다른 에러 처리(error handling), 약간씩 다른 기본값(defaults), 그리고 무엇이 "유효하지 않은지(invalid)"에 대한 약간씩 다른 개념을 가집니다. 사용자가 "설정 파일에 오타를 냈는데 akm이 3주 동안 그냥 조용히 기본값을 사용했다"라는 이슈(issue)를 제기하기 전까지는 아무도 눈치채지 못합니다.
그것이 0.8.0 버전으로 넘어가기 전 akm 설정 레이어(config layer)의 상태였습니다.
기존 구조의 모습
v1 설정은 2년 동안 독립적으로 성장한 세 가지 최상위 블록을 가지고 있었습니다: LLM 연결 설정을 위한 llm.*, 에이전트 프로세스(agent process) 설정을 위한 agent.*, 그리고 기능별 LLM 호출을 제어하는 불리언(boolean) 플래그인 llm.features.*입니다. 많은 기능이 LLM이 아닌 에이전트를 사용했음에도 불구하고, features 블록은 역사적인 이유로 llm 아래에 중첩되어 있었습니다. 에이전트의 프로세스별 맵(map)은 agent.processes 아래에 위치했으며, LLM이 제어하는 기능들은 llm.features.index.metadata_enhance와 같은 점 표기법(dotted paths) 스타일을 사용했습니다.
각 블록은 자체적인 파서 함수(parser function)를 가졌습니다. parseLlmConfig, parseEmbeddingConfig, parseIndexConfig 등 수십 개가 있었습니다. 새로운 config-schema.ts 상단의 주석은 이를 직설적으로 표현합니다: Zod 스키마(schema)가 "약 1,400줄(LOC)에 달하는 레거시 형태별 파서들을 대체한다"라고 말이죠.
그 약 1,400줄의 코드에 쌓인 문제점들은 다음과 같습니다:
알 수 없는 키(Unknown keys)가 조용히 수용되었습니다. 만약 llm.temperaure(오타)라고 작성했다면, 파서는 이를 무시하고 기본 온도(default temperature)로 돌아갔습니다. 경고도 없었습니다. 당신은 아무런 효과도 없는 키를 튜닝하고 있었던 것입니다.
잘못된 JSON이 마스킹되었습니다. 설정 로더(config loader)가 JSON 파싱 에러를 포착하면 컴파일 시 포함된 기본값인 DEFAULT_CONFIG로 되돌아갔습니다. 설정 파일 전체가 손상되었더라도 akm은 아무런 불만 없이 모든 부분에 기본값을 사용하여 시작될 수 있었습니다.
파일이 누락되어도 기본값으로 되돌아갔습니다. 동작 방식은 동일했습니다. 설정 파일이 누락된 경우와 파일은 존재하지만 손상된 경우 모두 런타임(runtime) 시점에는 동일하게 보였습니다.
필드를 추가하는 것은 파서를 추가하는 것을 의미했습니다. 기능(feature) 아래에 새로운 불리언(boolean) 플래그를 추가하고 싶다면 어떻게 해야 할까요? 적절한 파서(parser) 함수를 찾고, 추출 로직을 추가하고, 타입 선언을 추가하고, 힌트 문자열을 추가하고, 테스트를 추가해야 했습니다. 새로운 필드를 추가하는 비용은 단 한 줄이 아니었습니다. 네다섯 군데를 수정해야 하는 작은 PR(Pull Request)이었습니다.
Zod가 제공하는 것
0.8.0 재작성 버전은 이 모든 것을 src/core/config-schema.ts로 통합했습니다. 즉, 디스크에 저장되는 형태의 진실의 원천(source of truth)이 되는 단일 Zod 스키마(schema)입니다.
Zod는 이전에 약 1,400줄(LOC)에 달하는 수동 작성 코드에 흩어져 있던 파싱(parse), 변환(transform), 검증(validate) 단계를 처리합니다. 새로운 설정 필드를 추가하는 것은 스키마에 한 줄을 추가하는 것뿐입니다. 타입 추론(Type inference) 덕분에 AkmConfig에 대한 TypeScript 타입은 스키마로부터 자동으로 유도됩니다. 즉, 스키마와 타입 선언 사이의 병렬 유지보수가 필요 없습니다.
스키마 설계는 엄격함(strictness)과 회복 탄력성(resilience) 사이에서 의도적인 트레이드오프(tradeoff)를 수행합니다:
최상위 객체는 .passthrough()를 사용하여 향후 추가될 알 수 없는 키들이 손상되지 않고 그대로 전달되도록 합니다. 사용자가 버전을 업그레이드했다가 다시 다운그레이드하더라도, 새 버전에서 추가된 키들이 이전 버전에서 에러를 발생시키지 않고 유지됩니다. sanitizeConfigForWrite가 쓰기 작업 시 무엇을 제거할지 결정합니다.
중첩된 하위 객체들은 필드 수준의 형태 에러에 대해 .catch(undefined)를 사용하여, 한 필드의 오타가 다른 유효한 설정들을 파괴하지 않도록 합니다. 이는 구조적인 문제는 포착하면서도, 개별 필드에 대해서는 기존 파서의 '경고 후 무시(warn-and-ignore)' 의미론을 유지합니다.
.strict() 벽은 오타가 발생하기 가장 쉬운 레코드들인 registries[], sources[], 그리고 profiles.* 하위 형태(sub-shapes)를 보호합니다. 이제 프로필 이름이나 소스 타입(source type)에 오타가 있으면 로드 시점에 유효성 검사 오류(validation error)가 발생합니다.
superRefine에 의해 조용히 무시되는 대신 엄격하게 거부되는 두 가지 사례가 있습니다. 바로 기존의 stashes[] 키(sources[]로 대체됨)와 삭제된 레거시 소스 타입(legacy source type)입니다. 두 사례 모두 명시적인 마이그레이션 경로(migration paths)가 존재하므로, 이를 조용히 무시하는 것은 사용자의 데이터 손실을 은폐하는 결과가 됩니다.
침묵하는 실패(Silent Failure)의 해결
새로운 로더(loader)는 현장에서 침묵하는 실패를 유발하던 세 가지 동작을 변경했습니다.
이제 알 수 없는 키(Unknown keys)는 프로필 레벨에서 오류를 발생시킵니다. profiles.llm.my-profile에 오타가 있으면 무시되는 대신 로드 시점에 포착됩니다. 오류 메시지는 예상치 못한 키의 이름을 명시하고 해당 프로필 블록을 가리킵니다.
잘못된 JSON은 이제 예외를 던집니다. 만약 config.json이 유효한 JSON이 아니라면, akm은 파일 경로와 파싱 오류(parse error)를 포함한 ConfigError를 던집니다. 기본값으로의 폴백(fallback)은 없습니다. 사용자는 즉시 이를 알 수 있습니다.
누락된 파일은 누락된 상태로 유지됩니다. 설정 파일이 누락된 것은 파일이 손상된 것과는 다른 상황이며, akm은 이제 이를 다르게 취급합니다. 설정 파일이 없는 상태에서의 첫 실행 시 akm setup 또는 명시적인 akm config set을 통해 파일을 생성합니다. 이후 실행 중 파일이 누락된 경우에는 조용히 폴백되는 것이 아니라 오류로 처리됩니다.
자동 생성된 JSON 스키마 (JSON Schema)
Zod 스키마를 신뢰할 수 있는 단일 출처(source of truth)로 사용함에 따라, 에디터 자동 완성(autocompletion)을 위한 JSON 스키마를 생성하는 것은 자연스러운 결과물입니다. schemas/akm-config.json 파일은 Zod 스키마로부터 생성되어 체크인(checked in)됩니다. 체크인된 파일이 스키마 소스와 동기화되지 않으면 CI 드리프트 테스트(drift test)가 실패합니다. 따라서 필드를 추가할 때 별도로 기억해야 할 수동 단계가 없습니다.
에디터가 이 스키마를 참조하도록 설정하면 config.json에서 필드 완성(field completion)과 인라인 문서화(inline documentation)를 사용할 수 있습니다:
{
"$schema": "https://itlackey.github.io/akm/schemas/akm-config.0.8.0.json",
"configVersion": "0.8.0"
...
$schema 키는 선택 사항입니다. VSCode 및 기타 JSON Schema 인식 에디터는 필드 완성(field completion) 및 인라인 문서(inline docs)를 위해 이를 자동으로 인식합니다.
새로운 설정 구조 (The New Config Shape)
0.8.0 구조는 흩어져 있던 llm.*, agent.*, llm.features.* 블록을 통합된 profiles 트리와 일급 객체(first-class) 기능 섹션으로 대체합니다.
| 기존 위치 | 새로운 위치 |
|---|---|
llm.endpoint, llm.model, llm.apiKey | profiles.llm.<name>.endpoint, .model, .apiKey |
| ... | ... |
이름이 지정된 LLM 연결은 profiles.llm.<name> 아래에 위치하며, 한 번 선언되면 프로세스 엔트리에서 이름으로 참조됩니다. 이름이 지정된 에이전트(agent) 연결은 profiles.agent.<name> 아래에 위치합니다. 개선 프로필(profiles.improve.<name>.processes.*)은 프로세스를 특정 LLM 또는 에이전트 프로필에 바인딩하고 프로세스별 게이팅(gating)을 제어합니다. 비-개선(Non-improve) 기능(index.metadataEnhance, index.stalenessDetection, search.curateRerank)은 일급 최상위 엔트리(top-level entries)입니다.
configVersion 필드는 버전 게이트(version gate) 역할을 합니다. 이 필드가 없거나 0.8.0 미만의 값이 설정된 구성은 첫 실행 시 자동으로 마이그레이션됩니다.
최소 작동 설정 (The Minimal Working Config)
개선(improve) 작업을 위한 클라우드 LLM을 포함하여, 완전히 기능하는 0.8.0 설치를 위한 가장 작은 설정은 다음과 같습니다:
{
"configVersion": "0.8.0",
"$schema": "https://itlackey.github.io/akm/schemas/akm-config.0.8.0.json",
...
이것은 핵심 프로필과 기본값에 집중한 축약된 예시입니다. docs/configuration.md에 있는 전체 최소 설정에는 기본값이 포함된 feedbackDistillation, index, search 최상위 블록도 포함되어 있습니다. 해당 블록들을 생략하면, akm은 컴파일 시 포함된(compiled-in) 기본값을 사용합니다.
로컬 모델(local models)의 경우, openai-mini를 Ollama 또는 LM Studio 프로필로 교체하고 apiKey 필드를 제거하면 됩니다. supportsJsonSchema 플래그는 이를 지원하는 프로바이더(provider)에 대해 akm이 구조화된 JSON 출력(structured JSON output)을 사용하도록 지시합니다. response_format: {type: "json_schema"}를 준수하는 OpenAI 호환 엔드포인트(endpoints)의 경우 이 값을 true로 설정하고, 이를 지원하지 않는 로컬 모델의 경우 설정을 해제하십시오.
v1에서 마이그레이션하기
0.7.x 버전을 사용 중이라면 설정을 직접 수정할 필요가 없습니다. 마이그레이션 명령어가 키 재매핑(key remapping)을 처리합니다:
# 쓰기 작업 없이 변환 내용 미리보기
akm config migrate --dry-run
...
--dry-run은 아무것도 작성하지 않고 어떤 키가 이동하며 새로운 형태(shape)가 어떻게 구성되는지 보여줍니다. --dry-run 없이 실행하면, akm은 실제 파일에 접근하기 전에 ~/.cache/akm/config-backups/에 타임스탬프가 찍힌 백업을 작성합니다. 다음 명령어로 확인할 수 있습니다:
ls -1t ~/.cache/akm/config-backups/ | head
자동 마이그레이션(Auto-migration)은 업그레이드 후 첫 번째 명령 실행 시에도 실행됩니다. 이때 백업 경로와 함께, 향후 자동 마이그레이션을 억제하려면 AKM_NO_AUTO_MIGRATE=1을 설정하라는 안내 메시지가 stderr로 한 번 출력됩니다. 이 환경 변수(env flag)는 배포 단계에서 akm config migrate를 명시적으로 실행하고자 하는 읽기 전용 CI 마운트(CI mounts) 환경에서 유용합니다.
마이그레이션 후에는 결과를 확인하십시오:
akm config get configVersion
# "0.8.0"
...
이전 키에서 새 키로의 전체 매핑 정보는 docs/migration/v0.7-to-v0.8.md에 있습니다.
필드 추가 방식의 변화
재작성 전에는 프로세스당 새로운 옵션을 추가하려면 파서(parser) 함수, 타입 선언(type declaration), 힌트 문자열(hint string), 그리고 테스트를 모두 수정해야 했습니다. Zod 스키마(schema)에서는 동일한 변경 사항이 관련 하위 스키마(sub-schema) 객체 내의 단 한 줄로 해결됩니다. TypeScript는 추론(inference)을 통해 새로운 필드를 자동으로 인식합니다. JSON 스키마는 다음 빌드 시 재생성됩니다. 만약 재생성 단계가 누락될 경우, CI 드리프트 테스트(CI drift test)가 이를 잡아냅니다.
그러한 개선을 위해 지불하는 비용은 구체화할 가치가 있습니다. 스키마 파일은 641 LOC(Lines of Code, 코드 라인 수)입니다. 마이그레이션 로직은 또 다른 643 LOC입니다. 설정 로더(config loader) 자체는 590 LOC입니다. 이는 총 약 1,874 라인으로, 기존의 약 1.4k LOC에 달하던 파서(parsers)를 대체하는 동시에, 이전에는 존재하지 않았던 마이그레이션 파이프라인(migration pipeline), 엄격한 검증(strict validation), 그리고 구조화된 오류 보고(structured error reporting) 기능을 추가한 것입니다. 기능당 유지보수 범위(maintenance surface)는 더 높아진 것이 아니라 오히려 낮아졌습니다.
Config v2는 akm 0.8.0에 포함되어 있습니다. 전체 설정 참조 문서는 docs/configuration.md에서 확인할 수 있습니다. 0.8.0 release notes에는 설정 재작성과 함께 적용된 더 광범위한 스토리지 및 파이프라인 변경 사항이 포함되어 있습니다. 만약 improve 파이프라인을 실행 중이며 profiles.improve 설정이 실제로 어떻게 동작하는지 확인하고 싶다면, Your Agent Has a Memory That Runs While You Sleep에서 전체 프로세스 설정이 적용된 상태로 24시간 동안 자율 운영되는 모습을 다루고 있습니다.
업그레이드를 진행하는 경우, akm config migrate --dry-run으로 시작하여 설정을 적용하기 전에 출력 결과가 예상과 일치하는지 확인하십시오.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기