OpenSpec에 검증 체계를 얹은 이야기
전편에서 OpenSpec을 도입해 스펙 기반 개발을 시작한 이야기를 썼다. 글 마지막에 "앞으로 해볼 것" 두 가지를 적었는데, 그 중 하나가 E2E 검증 단계를 넣는 것이었다.
명세와 사용자 워크플로우를 기준으로 E2E(또는 통합) 테스트를 작성하고, opsx:apply 이후 이 테스트가 통과해야 완료로 치는 흐름을 만들고 싶다. 문서 정합성 검증(validate)과 코드 반영(apply)만으로는 "완료"를 선언하기 어렵고, 결국 동작 검증이 기준이 되어야 한다.
기본 OpenSpec으로도 spec.md에 WHEN/THEN 시나리오를 쓸 수 있고, tasks.md에 검증 항목을 넣을 수 있다. 아예 틀이 없는 것은 아니다. 다만 그것을 체계적으로 추적하고 실행하는 구조까지 제공하지는 않는다. 결국 그 부분은 직접 만들어야 한다.
무엇이 빠져 있었나
빠져 있는 건 두 가지였다.
첫째, 시나리오에 고유 번호가 없었다. spec.md에 시나리오를 여러 개 정의하고 tasks.md에 검증 항목을 적어도, 어떤 검증이 어떤 시나리오를 다루는지 추적하기 어려웠다.
둘째, 검증 방식이 분명하지 않았다. "이 시나리오를 확인한다"라고만 적혀 있으면, unit 테스트인지 E2E인지, 아니면 사람이 직접 확인하는 건지 알 수 없다. 에이전트가 "검증 완료"를 선언해도, 실제로 어느 수준까지 확인한 것인지 판단하기 어려웠다.
시나리오 ID와 VERIFICATION 필드
OpenSpec CLI를 수정한 게 아니라, 저장소 안에 에이전트가 읽는 지시 레이어를 추가한 것이다. built-in spec-driven 스키마 위에 저장소 로컬 AGENTS.md와 에이전트 스킬(.claude/skills/, .codex/skills/)을 덧댄 하이브리드 상태다.
이 커스텀의 핵심은 시나리오를 공통 식별자로 사용해 spec, 검증 항목, apply, archive를 하나의 연결된 흐름으로 관리하는 것이다.
시나리오 ID
spec.md의 모든 시나리오에 <capability>-SC-N 형식의 고유 ID를 부여했다.
#### Scenario: survey-template-create-SC-1 템플릿 생성 성공
- **WHEN** 사용자가 제목, 유형을 입력하고 "설문 문항 만들기" 버튼을 클릭한다
- **THEN** POST /api/v1/survey-templates가 호출된다
- **THEN** 성공 시 에디터 페이지로 이동한다
이 ID가 단순 라벨이 아니라 spec과 tasks를 묶는 키로 쓰인다. tasks.md에서 survey-template-create-SC-1 검증: ... 형태로 참조하면, 어떤 검증이 어떤 시나리오에 대한 건지 추적할 수 있다. 시나리오 제목이 바뀌어도 ID가 유지되면 연결이 깨지지 않는다.
VERIFICATION 필드
모든 시나리오에 검증 방식을 명시하는 필드를 추가했다.
- **VERIFICATION**: `[e2e] 템플릿 생성 플로우 | pass: 모달 입력 → 에디터 이동 → 목록 갱신이 성공한다`
형식은 [type] target | pass: criteria다. 허용하는 타입은 변경 성격에 따라 정했다.
| 변경 성격 | 기본 검증 방식 |
|---|---|
| 인프라/도구 | static 또는 command |
| 비즈니스 로직 | unit |
| UI 컴포넌트 | component |
| 사용자 워크플로우 | e2e |
| API 연동 | integration |
검증 방식은 change 단위가 아니라 시나리오별로 선택한다. 하나의 change가 API 연동과 UI 컴포넌트를 동시에 포함하면, 각 시나리오에 맞는 타입을 적용한다.
여기서 spec.md와 tasks.md의 경계를 명확히 했다. spec.md의 VERIFICATION은 "무엇을 확인하면 통과인가"를 정의한다. 도구나 파일 경로 같은 실행 디테일은 적지 않는다. tasks.md의 검증 태스크가 "어떻게 실행할 것인가"를 담당한다.
검증 준비/실행 분리와 apply 루프
검증 준비와 실행을 나눈다
테스트를 만드는 일과 테스트를 돌려서 통과를 확인하는 일은 성격이 다르다. 검증 준비는 테스트 코드를 작성하거나 환경을 세팅하는 작업이고, 검증 실행은 그 수단으로 실제 통과 여부를 확인하는 작업이다.
tasks.md에서는 이렇게 분리한다.
## 4. 검증
- governance-SC-1 검증 준비: spec 구조 검증 스크립트를 작성한다
- governance-SC-1 검증 실행: 스크립트를 실행하여 PASS를 확인한다
다만 항상 둘로 쪼개지는 않는다. 문서 수준 변경처럼 분리할 실익이 없으면 단일 검증 태스크로 합친다.
- governance-SC-2 검증: VERIFICATION 라인의 type이 허용 목록에 속하는지 확인한다
apply가 검증 루프를 돌린다
기존 apply는 tasks.md의 pending 체크박스를 수행하고 체크하는 단순 흐름이었다. 여기에 검증 루프를 얹었다.
┌─────────────────────────────────────────────────┐
│ 1. 구현 태스크 수행 │
│ 2. scenario별 검증 준비 태스크 수행 │
│ 3. scenario별 검증 실행 태스크 수행 │
│ 4. 검증 실패 시 → 관련 구현/검증 준비로 되돌아감 │
│ 5. 모든 기능 검증 통과까지 반복 │
└─────────────────────────────────────────────────┘
에이전트는 tasks.md의 ## N. 검증 섹션에 진입한 시점을 기능 검증 단계의 시작으로 간주한다. 검증이 실패하면 관련 구현 태스크로 되돌아가서 수정하고 다시 검증을 돌린다. 모든 검증이 통과할 때까지 반복한다.
## N. 검증 섹션이 없는 레거시 change는 기존 체크박스 동작으로 폴백한다. 새로운 체계를 강제하지 않고, 점진적으로 적용할 수 있게 했다.
archive 게이트와 개발자 최종 검수
archive에 두 가지 게이트를 추가했다
기본 archive는 아티팩트와 태스크가 완료됐는지 확인한다. 여기에 두 가지 검증 게이트를 추가했다.
첫째, 시나리오별 최소 커버리지 확인이다. spec.md의 모든 시나리오 ID가 tasks.md의 검증 태스크에서 최소 1번 이상 참조되는지 확인한다. 빠진 시나리오가 있으면 경고를 표시한다.
둘째, 검증 태스크 완료 확인이다. ## N. 검증 섹션의 모든 검증 태스크가 [x]인지 확인한다. 미완료 항목이 있으면 경고를 표시한다.
개발자 최종 검수
tasks.md의 검증 섹션 마지막에는 항상 "개발자 최종 검수" 태스크가 들어간다.
- 개발자 최종 검수: 시나리오별 검증 항목과 결과를 확인하고 이 체크리스트를 완료 기준으로 확정한다
에이전트가 검증을 자동으로 돌리고 통과를 확인하더라도, 최종적으로 사람이 검증 목록과 결과를 보고 "이걸 완료 기준으로 삼겠다"고 확정하는 단계다. 에이전트가 놓친 맥락이 있을 수 있고, 검증 항목 자체가 잘못 설정됐을 수도 있다. 이 태스크가 체크되어야 archive로 넘어갈 수 있다.
써보면서 든 생각
스펙 주도 개발 덕분에 할 일을 명확히 이해하고 계획을 세운 뒤 작업하는 흐름은 생겼다. 하지만 무엇을 검증할지도 스펙의 일부인데, 그 부분은 체계적으로 다루고 있지 않았다. 이번에 보강한 건 그 부분이다.
tasks.md는 원래 구현 체크리스트다. 여기에 검증을 넣은 건, spec의 시나리오를 apply가 실행 가능한 단위로 투영하기 위해서다. 이제 apply는 구현만 하는 단계가 아니라 시나리오를 닫는 단계가 된다. 결과적으로 구현 완료 시점에 검증까지 끝나 있는 상태가 된다.
검증 항목의 초안은 에이전트가 만들 수 있지만, 완료 기준으로 확정되는 건 개발자가 최종 확인했을 때다. 이 프로젝트에서 스펙의 완성은 요구사항과 시나리오를 문서화하는 데서 끝나지 않는다. 각 시나리오를 어떤 방식으로 검증해 PASS로 판단할지까지, 사람 책임 하에 합의된 상태를 의미한다.
다만 아직 개선할 점도 있다. 검증 타입의 선택 기준이 모호한 경우가 있다. UI 컴포넌트의 상태 변화를 component 테스트로 확인할지 e2e로 확인할지 애매한 경우가 그렇다. 케이스가 쌓이면 더 구체적인 기준을 만들 수 있을 것 같다.
그리고 이번 커스텀은 전부 에이전트 지시 레이어로 구현했는데, OpenSpec에 custom mode가 있다는 걸 나중에 알았다. 스키마 템플릿이나 apply instruction 같은 건 에이전트 스킬보다 custom schema 쪽이 더 자연스러운 위치다. 이 부분을 정리하는 걸 다음 과제로 생각하고 있다.