MonoSpecs는 무엇인가: 왜 OpenSpec의 추가 업그레이드와 확장이라고 하는가
MonoSpecs는 무엇인가: 왜 OpenSpec의 추가 업그레이드와 확장이라고 하는가
한 제품 시스템이 40개 이상의 독립 Git 레포지토리로 확장되면, “규격”은 어디에 두어야 하는가? 이 글에서는 HagiCode가 다중 레포지토리 거버넌스에서 걸어온 두 단계를 이야기해보자: 먼저 OpenSpec을 메인 레포지토리로 올리고, 그 위에서 MonoSpecs라는 다중 레포지토리 관리 솔루션을 발전시킨 것이다. 사실 별거 아니다, 그냥 몇 개의 구덩이를 밟고 그걸 기록해두고 싶었을 뿐이다.
배경
조금 더 큰 규모의 제품을 만들어본 경험이 있는 동료들은 대개 이런 경험을 해봤을 것이다——처음엔 코드가 하나의 레포지토리만 있고, 규칙적이고 평화롭다; 나중엔 프론트엔드, 백엔드, 데스크톱, 문서 사이트, 공식 사이트, 빌드 도구가 각각 독립적인 레포지토리로 분리되고, 레포지토리 수가 미친 듯이 늘어나서 잡초처럼 막을 수가 없다. 그리고 나서 어떤 레포지토리 간 기능에 대한 “규격 문서”를 쓰고 싶다면 갑자기 어디에 써야 할지 모르게 된다. 뭐라고 해야 할까, 어릴 적 용돈이 갑자기 사라진 것과 비슷하다.
우리 자신의 HagiCode도 바로 40개 이상의 독립 Git 레포지토리로 구성된 제품 시스템이다. 초기에는 OpenSpec의 openspec/ 디렉토리를 백엔드 하위 레포지토리 hagicode-core에 그냥 넣어버렸다. 아무튼 백엔드가 핵심이니 여기에 두는 게 가장 안정적일 것이라고 생각했다. 결과적으로 레포지토리가 더 많이 분리되면서 이 솔루션은 일련의 골치 아픈 문제를 노출시켰다. 결국 코드 세계는 당신이 “안정적으로 생각했다”고 해서 정말 안정적이지 않다.
첫 번째 고통스러운 점: specs가 단일 하위 레포지토리에 갇혀 있다. 한 기능이 동시에 프론트엔드 web과 백엔드 hagicode-core에 영향을 미친다면, hagicode-core에 제안을 쓰고 다른 하위 레포지토리로 가서 코드 변경을 실행해야 한다. 제안이 어느 레포지토리에 속해야 하는지 자체가 논쟁거리가 된다.
두 번째 고통스러운 점: 하위 레포지토리가 순수하지 않다. 각 하위 레포지토리는 자신의 openspec/을 가지고 있고, 규격 문서와 제품 코드가 섞여 있다. 누군가 당신의 프론트엔드 레포지토리를 clone 하면 결과적으로 백엔드 제안 문서를 한 무더기 가져오게 되고, 얼굴이 당황스러워진다.
세 번째 고통스러운 점: AI Agent가 레포지토리 관계를 이해하기 어렵다. 각 하위 레포지토리는 서로 독립적이고, AI에게 기계가 읽을 수 있는 “목록”이 없다: 이 제품은 어떤 레포지토리로 구성되어 있는지, 각각 무엇을 담당하는지, 어느 것이 편집 가능한지, 어느 것이 읽기 전용 참조인지.
네 번째 고통스러운 점: 레포지토리 간 편집 비용이 높다. spec을 수정하려면 먼저 cd로 해당 하위 모듈로 들어가야 하고, 경로가 이리저리 튀어서 협업에 대한 심적 부담이 매우 크다.
바로 이러한 배경에서 우리는 먼저 “OpenSpec Monorepo Migration”을 수행하여 specs를 하위 레포지토리에서 monorepo 루트 디렉토리로 올렸다. 그리고 그 위에서 MonoSpecs라는 다중 레포지토리 관리 솔루션을 발전시켰다. 이 두 단계의 진보적 관계를 이해하는 것이 “왜 monospec이 openspec의 추가 업그레이드와 확장이라고 하는지”를 이해하는 핵심이다.
HagiCode에 대해
이 글에서 공유하는 솔루션은 HagiCode 프로젝트에서의 실무 경험에서 나온 것이다. HagiCode는 AI 코드 어시스턴트 프로젝트로, 레포지토리 수가 많고 언어 간 협업이 잦아, 이러한 구조적 복잡성은 우리가 “규격”과 “레포지토리 거버넌스” 이 두 가지를 모두 견고하게 만들도록 강요했다. MonoSpecs 솔루션은 바로 이러한 다중 레포지토리 실전에서 조금씩 연마된 것이다. 사실 별로 신기한 기술도 없다, 그저 몇 걸음을 더 걸었을 뿐이다.
OpenSpec은 “규격을 어떻게 작성하고 어떻게 진화시키는지”를 해결한다
두 관계를 명확히 설명하려면 먼저 각각 무엇을 담당하는지 분리해서 보아야 한다.
OpenSpec은 본질적으로 spec-driven 변경 관리 워크플로우이다. 그 핵심 산출물은 다음과 같다:
openspec/├── specs/ # 현재 유효한 기능 규격 (각 기능마다 하나의 spec.md)├── changes/ # 진행 중인 제안│ └── archive/ # 보관된 과거 제안└── project.md그것은 답하는 질문은: 하나의 변경이 제안(proposal), 설계(design), 작업(tasks), 보관(archive)와 같은 수명 주기를 거치고, 보관할 때 deltas를 specs에 병합한다. 이 메커니즘 자체는 “레포지토리가 몇 개이고, 어디에 있고, 누가 관리하는지”와 무관하며, spec 파일을 어떻게 조직하는지만 관심 있다.
우리는 한 번의 마이그레이션 제안을 통해, 원래 hagicode-core/openspec/에 분산되어 있던 82개 이상의 spec 파일을 monorepo 루트 디렉토리의 openspec/로 올려서, 모든 spec이 한 곳에서 통합적으로 보이고 통합된 버전 제어를 하도록 했다.
하지만 이번 마이그레이션은 말하자면 “spec 파일을 이사시킨 것”일 뿐이고, 더 근본적인 질문은 답하지 못했다: 이 monorepo는 정확히 어떤 하위 레포지토리로 구성되어 있는가? 이러한 하위 레포지토리 간의 관계는 무엇인가? 이것이 바로 MonoSpecs가 채워야 할 부분이다.
MonoSpecs는 “다중 레포지토리 자체를 어떻게 관리하는지”를 해결한다
MonoSpecs의 핵심은 기계가 읽을 수 있는 목록 파일이다: .hagicode/monospecs.yaml. 그것은 OpenSpec이 전혀 다루지 않는 네 가지 일을 한다.
첫 번째: 하위 레포지토리 목록 선언. 각 레포지토리의 path, url, displayName, icon, tags, “More”로 접을지 여부를 모두 하나의 YAML에 써서 한눈에 알 수 있다.
두 번째: clone 스크립트 구동. scripts/clone-repos.mjs는 이 YAML을 직접 읽고 일괄적으로 git clone을 수행하며, 레포지토리 목록을 하드코딩하지 않는다. 새 레포지토리를 추가할 때는 YAML에 한 줄만 추가하면 되고 스크립트는 변경 없다.
세 번째: AI/IDE에 프로젝트 구조 컨텍스트 제공. AGENTS.md와 함께 사용하면, AI Agent는 어느 레포지토리가 편집 가능하고 어느 것이 참조 전용인지, 기술 스택이 무엇인지 한눈에 알 수 있다.
네 번째: OpenSpec의 산출물을 메인 레포지토리에 고정. specs는 더 이상 각 하위 레포지토리에 흩어지지 않고 메인 레포지토리 루트 디렉토리의 openspec/에 통합 수록되며, 하위 레포지토리는 따라서 순수성을 유지한다.
두 가지 의미, 혼동하지 마세요
MonoSpecs의 공식 가이드에서 매우 쉽게 혼동되는 부분을 명확히 지적한다: MonoSpecs에는 사실 두 가지 의미가 있다.
하나는 구성 시스템 계층으로, .hagicode/monospecs.yaml라는 구성 파일 자체와 그것의 로딩, 검증, 캐싱 메커니즘을 의미한다.
다른 하나는 레포지토리 유형 계층으로, “메인 레포지토리 + 다중 하위 레포지토리 + 중앙화 specs”라는 레포지토리 조직 모드를 의미한다. 우리가 한 프로젝트가 “MonoSpecs 프로젝트다”라고 말할 때, 그것이 이러한 구조를 채택했다는 의미이다.
이 두 계층이 함께 겹쳐야만 완전한 MonoSpecs가 된다. 처음 접하는 많은 사람들은 YAML 파일 계층만 보고 MonoSpecs가 단순히 구성 목록이라고 생각하기 쉽다. 사실 그 가치는 두 번째 계층에 더 있다——명확한 다중 레포지토리 협업 패러다임. 사실, 아름다운 것은 종종 첫눈에 보이지 않는다, 몇 번 더 봐야 한다.
왜 “업그레이드와 확장”이라고 하는가
둘을 함께 비교하면 관계가 명확해진다:
| 차원 | OpenSpec | MonoSpecs |
|---|---|---|
| 관심사 | spec 파일의 내용과 수명 주기 | 레포지토리의 조직 구조와 목록 |
| 핵심 산출물 | openspec/specs/*/spec.md | .hagicode/monospecs.yaml |
| 상호 의존 여부 | MonoSpecs에 의존하지 않음 | OpenSpec에 의존하고, 그 openspec/을 재사용하여 변경 관리 |
| 해결하는 고통스러운 점 | 규격을 어떻게 작성하고 진화시키는지 | 다중 레포지토리를 어떻게 선언하고, 어떻게 clone하고, AI가 어떻게 이해하는지 |
| 작용 범위 | 모든 레포지토리에서 사용 가능 | “일주 다중 자식” 다중 레포지토리 구조를 위해 특별히 설계됨 |
말하자면, MonoSpecs는 OpenSpec을 대체하는 것이 아니라 그 위에 “레포지토리 거버넌스” 계층을 추가한 것이다. monospecs.yaml로 레포지토리 토폴로지를 설명하고, 중앙화된 openspec/으로 spec을 하위 레포지토리와 분리하고, commit_when_archive로 보관을 자동으로 메인 레포지토리에 저장한다.
하나의 비유를 사용하면: OpenSpec은 “변경 구문”을 제공하고, MonoSpecs는 “다중 레포지토리 의미”를 제공한다. 전자는 후자의 전제이고, 후자는 전자의 확장이다. 모든 길은 로마로 통한다, 단 이번에는 길이 상상보다 조금 더 길 뿐이다.
어떻게 실현하는가: 4단계
첫 번째 단계: 메인 레포지토리와 구성 파일 확정
monorepo 루트 디렉토리에 구성 파일을 잘 놓고 모든 하위 레포지토리를 선언한다. 우리 자신의 프로젝트를 예로 들면 구조는 대략 다음과 같다:
version: "1.0"commit_when_archive: true
repositories:- path: "repos/web" url: "https://github.com/HagiCode-org/web.git" displayName: "프론트엔드" tags: [frontend, react, pcode-client]
- path: "repos/hagicode-core" url: "https://github.com/newbe36524/pcode" displayName: "백엔드" tags: [backend, dotnet, orleans]
- path: "repos/docs" url: "https://github.com/HagiCode-org/docs.git" displayName: "문서" tags: [docs, astro, starlight] ui: collapseToMore: true # UI에서 "More" 뒤로 접기몇 개의 필드는 특별히 주의해야 한다:
path는 메인 레포지토리 루트에 대한 상대 경로이며, 각 레코드의 유일한 키이다.url은 Git 원격 주소이며, clone 스크립트가 이것으로 코드를 가져온다.displayName/icon/tags는 UI 표시와 AI 컨텍스트에만 영향을 미치고 clone 행동에는 영향을 미치지 않는다.commit_when_archive: true는 OpenSpec 제안 보관 시 자동으로 메인 레포지토리에 커밋하게 한다.
두 번째 단계: OpenSpec을 메인 레포지토리 루트 디렉토리로 올리기
마이그레이션 전후 대비는 다음과 같다:
마이그레이션 전(specs가 하위 레포지토리에 갇혀 있음) 마이그레이션 후(specs가 메인 레포지토리에 중앙화됨)hagicode-core/ . (메인 레포지토리 루트)└── openspec/ ├── .hagicode/monospecs.yaml └── specs/ (82+ specs) ├── openspec/ │ ├── specs/ (중앙화 관리) │ └── changes/ └── repos/ ├── hagicode-core/ (순수, openspec 없음) ├── web/ └── docs/하위 레포지토리는 더 이상 openspec/을 짊어지지 않고, 메인 레포지토리가 유일한 spec 진실 소스가 된다. 이 단계는 단순해 보이지만, 가져온 수익은 매우 실재적이다——어떤 엔지니어도 메인 레포지토리 루트 디렉토리에 서서 전체 제품 시스템의 모든 규격을 볼 수 있다.
세 번째 단계: clone 스크립트가 하드코딩이 아니라 구성을 읽게 하기
scripts/clone-repos.mjs의 핵심 논리는 YAML을 읽고 하나씩 clone 하는 것이다:
const CONFIG_PATH = path.join(__dirname, '..', '.hagicode', 'monospecs.yaml');// repositories 배열 분석// 각 항목에 대해 git clone <url> <path> 실행// 대상 디렉토리가 이미 존재하면 건너뛰거나 git pull새 레포지토리를 추가할 때는 YAML에 한 줄만 추가하면 되고, 스크립트는 건드리지 않아도 된다. 이 작은 변경은 무수한 “레포지토리 목록 동기를 잊은” 쟁투를 아낀다. 누가 반복 노동을 좋아하겠는가?
네 번째 단계: 백엔드가 통합된 MonoSpecs 서비스 계층 제공
한 계층의 추상화를 추출하지 않으면, 구성 분석 논리는 쉽게 GitAppService, ProjectAppService 각 구석에 흩어진다. HagiCode는 ClaudeHelper 모듈에서 IMonoSpecsService를 추출하여, 대외적으로 명확한 능력 그룹을 노출한다:
public interface IMonoSpecsService{ Task<MonoSpecsConfigDto> GetConfigAsync(string projectPath); Task<List<RepositoryInfoDto>> GetSubRepositoriesAsync(string projectPath); Task<MonoSpecsDataDto> GetMonoSpecsDataAsync(string projectPath); Task<MonoSpecsManagementDto> GetManagementDocumentAsync(string projectPath); Task<MonoSpecsManagementDto> InitializeManagementDocumentAsync(string projectPath); Task ValidateManagementDocumentAsync(string projectPath, UpdateMonoSpecsManagementRequestDto request); Task SaveManagementDocumentAsync(string projectPath, UpdateMonoSpecsManagementRequestDto request);}이 서비스는 구성 로딩, 검증, 캐싱을 담당하고, “최소 템플릿 초기화” 능력을 제공한다——빈 프로젝트에 원클릭으로 monospecs.yaml, repos/, openspec/changes/archive/, openspec/specs/ 골격을 생성하고 자동으로 .gitignore를 완성한다. 캐시는 기억과 같다, 기억해두면 다음에 다시 힘들게 생각할 필요가 없다.
실전에서의 몇 개의 구덩이
완전히 새로운 MonoSpecs 프로젝트 초기화
InitializeManagementDocumentAsync를 호출한 후, 디스크에 다음과 같은 구조가 나타난다:
my-project/├── .gitignore # 새로운 repos/ 무시 규칙 추가 (멱등, 중복 추가 안 함)├── .hagicode/│ └── monospecs.yaml # 최소 템플릿: version / commit_when_archive / repositories: []├── openspec/│ ├── changes/archive/│ └── specs/└── repos/ # 빈 디렉토리, clone 대기여기서 몇 가지 경계에 주의해야 하며, 모두 spec에서 뽑아낸 것이다:
- 멱등성: 이미 존재하는
repos/,openspec/디렉토리는 보존되고 오류가 발생하지 않는다. - 덮어쓰지 않음:
monospecs.yaml이 이미 존재하고 정상적으로 분석할 수 있으면 초기화는 그것을 건드리지 않고, 누락된.gitignore규칙과 openspec 디렉토리만 보충한다. - 더러운 구성 거부: 이미 존재하지만 분석할 수 없는
monospecs.yaml은 직접 거부되고, 진단 가능한 오류 정보를 반환하며 절대 덮어쓰지 않는다. - 자동 스캔 없음: 초기화는 디스크 디렉토리를 자의적으로 스캔하여 레포지토리 항목으로 만들지 않고,
repositories는 기본적으로 비어 있으며, 수동 또는 UI를 통해 채워야 한다.
구성 파일 위치의 마이그레이션 함정
역사적으로 monospecs.yaml은 프로젝트 루트 디렉토리에 있었고, 나중에 강제로 .hagicode/monospecs.yaml로 마이그레이션되었다. 이 점은 spec에서 매우 명확하게 쓰여 있다:
루트 디렉토리의
monospecs.yaml은 더 이상 감지되지 않으며, 호환성 롤백으로도 사용되지 않습니다. clone 스크립트는.hagicode/monospecs.yaml만 인식합니다.
따라서 오래된 프로젝트를 업그레이드할 때는 반드시 수동으로 mv monospecs.yaml .hagicode/monospecs.yaml을 실행해야 하며, 어떠한 무음 호환 경로도 없다.乍看 불친절해 보이지만, 자세히 생각해보면 이것은 “두 위치 모두 효력이 발생할 수 있다”는 모호성을 완전히 제거하기 위한 것이다——이러한 모호성이 한 번 존재하면 문제를 조사할 때 사람을 미치게 만들 수 있다. 누구도 두 파일 사이에서 답을 찾고 싶지 않다.
저장 검증: 유효하지 않은 구성을 쓰지 마세요
SaveManagementDocumentAsync를 통해 다시 쓰기 전에 서비스는 필드 레벨 검증을 수행한다. 몇 가지 전형적인 거부 시나리오:
- 두 레포지토리 항목의
path가 중복됨 → 거부, 충돌 필드 반환 - 어떤 항목에
path가 누락됨 → 거부, 필수 필드 오류 반환 url이 비어 있지 않지만 합법적인 절대 URL이 아님 → 거부
검증을 통과한 후에만 YAML로 직렬화하여 디스크에 쓰고, 동시에 해당 프로젝트 경로의 구성 캐시를 무효화하여 다음 읽기에서 최신 내용을 가져오도록 보장한다. 이 단계는 사소해 보이지만, 무수한 “왜 내가 구성을 변경해도 적용되지 않는가” 티켓을 피할 수 있다. 이런 티켓이 너무 많으면 누구도 버틸 수 없다.
workspace 모드 vs 수동 repositories 모드
구성 파일은 두 가지 파생 레포지토리 목록 방식을 지원한다.
하나는 수동 repositories 모드로, YAML에 직접 각 레포지토리를 나열하고, 관리 문서는 편집 가능으로 표시된다.
다른 하나는 workspace 모드로, .code-workspace 파일을 선언하고, 그것이 레포지토리 목록을 파생한다. 이 모드에서 관리 문서는 읽기 전용으로 표시되고, 직접 레포지토리 배열을 수정하는 것이 금지되며, 지원되는 최상위 필드만 수정할 수 있다.
우리 자신의 HagiCode Mono는 현재 workspace 모드를 주석 처리하고 수동 모드를 채택한다. 이유는 간단하다: 수동 모드는 각 레포지토리의 icon과 tags를 정밀하게 제어할 수 있고, UI 표시 효과가 더 통제 가능하다. 뭐라고 해야 할까, 통제할 수 있는 것은 마음이 더 편안하다.
AI Agent에 대한 실전 제안
이제 AI 프로그래밍이 점점 보편화되면서, MonoSpecs 솔루션에는 사실 하나의 암묵적 가치가 있다: 그것은 AI에게 구조화된 프로젝트 지도를 제공한다.
다중 레포지토리 협업에서 AGENTS.md와 monospecs.yaml은 AI를 위한 두 가지 핵심 컨텍스트이다. 권장 워크플로우는 다음과 같다:
- 먼저
monospecs.yaml을 읽어 레포지토리 토폴로지를 얻고, 누가 편집 가능하고 누가 참조 전용인지 명확히 한다. - 다음으로 루트
AGENTS.md의 “Active Edit Scope”를 읽어 현재 수정이 허용된 범위를 확인한다. - 레포지토리 간 변경은 메인 레포지토리 루트의
openspec/changes/에서 통합적으로 제안을 작성하고, 각 하위 레포지토리에서 별도의 openspec을 시작하지 않는다.
이러한 규약은 AI가 “메인 레포지토리가 specs를 관리하고, 하위 레포지토리가 코드를 관리한다”는 분업을 안정적으로 이해하게 하고, 실수로 spec을 하위 레포지토리에 쓰는 것을 방지한다——우리는 이러한 오작동을 여러 번 경험했다. 사실 AI를 탓할 수 없다, 하위 레포지토리와 메인 레포지토리가 너무 비슷해서 누가 한눈에 분명히 할 수 있겠는가?
요약
한 문장으로 요약하면: OpenSpec은 “변경을 어떻게 작성하는지”를 정의하고, MonoSpecs는 “레포지토리를 어떻게 배치하는지”를 정의한다.
전자는 후자의 구문적 기초이고, 후자는 전자를 단일 레포지토리 컨텍스트에서 다중 레포지토리 컨텍스트로 확장하며, 하나의 YAML 목록으로 레포지토리 토폴로지, clone 프로세스, AI 컨텍스트, specs 소속을 한 번에 수렴한다. 이것이 “monospec이 openspec의 추가 업그레이드와 확장”의 진정한 의미이다——대체가 아니라 그 위에 다중 레포지토리 의미 계층을 추가한 것이다.
만약 당신도 비슷한 규모의 다중 레포지토리 제품을 만들고 있다면, 이 두 계층이 모두 잘 준비되었는지 생각해 보세요. 규격이 아무리 아름답게 작성되어도 명확한 레포지토리 거버넌스가 지탱하지 않으면 결국 엉망이 될 것이다…
참고 자료
- HagiCode 공식 사이트
- HagiCode-org/site GitHub 레포지토리
- OpenSpec 워크플로우 문서
- MonoSpecs 관련 spec:
monospecs-guide,monospecs-repository-config,monospec-config-management
요약
“MonoSpecs는 무엇인가: 왜 OpenSpec의 추가 업그레이드와 확장이라고 하는가”에 대해, 더 안정적인 추진 방식은 먼저 핵심 구성, 의존 경계 및 실현 경로를 단계적으로 통과시키고, 그 후에 최적화 세부 사항을 보완하는 것이다.
목표, 단계 및 수용점이 명확해지면, 이러한 솔루션은 보통 더 원활하게 실제 전달에 들어갈 수 있다.
开始使用 HagiCode
一次安装,几分钟上手
HagiCode for Windows 在 Microsoft Store 免费提供。打开商店即可安装并保持更新;也可以先对比各版本与定价,再决定从哪个渠道开始。