OpenSpec 을 설치하지 않고 원칙만 빌렸습니다 — 요구·현황·변경·근거를 가른 구현 명세

정기창·

스펙 주도 개발(Spec-Driven Development) 오픈소스인 OpenSpec 을 설치하지 않고, 그 원칙만 빌려 구현 명세를 만들었습니다. 기획은 다른 쪽이 쓰고 구현은 제가 맡는 협업 프로젝트에서, «이 기능은 지금 어디까지 왔고 무엇이 남았나»에 답할 때마다 기획 문서와 이슈, PR, 배포 기록을 처음부터 다시 훑어야 했기 때문입니다. 빌린 것은 명령어나 폴더 구조가 아니라 분리였습니다. 무엇을 만들면 끝인가(요구), 지금 어디까지 왔는가(현황), 무엇이 바뀌었는가(변경), 무엇으로 그렇게 말하는가(근거)를 서로 다른 자리에 두는 것입니다. 이 넷을 가르면 같은 질문에 매번 처음부터 답하지 않아도 될 것이라고 예상했습니다.

실제로 해 보니 예상과 어긋난 곳이 여럿이었습니다. 기획을 옮겨 적지 않는 데는 성공했지만, 시범 문서 안에서는 같은 현재 사실 하나가 열 군데 가까이 적혀 있었습니다. 범위를 넓혀 쓴 명세에서는 근거 칸 여러 곳이 언제든 닫힐 수 있는 열린 PR 을 근거처럼 적고 있었습니다. 갱신 절차는 병합 기록에 치우쳐 있었습니다. 문서에서 찾지 못한 것을 곧바로 결정권자에게 물을 질문으로 올리기도 했습니다. 그리고 범위를 넓힐 때는 «바꿀 부분만 명세하라»는 OpenSpec 문서의 권고와 다르게, 처음 한 번은 열린 이슈와 PR 전부를 요구와 대조했습니다. 이 글은 그 원칙을 빌리며 무엇을 예상했고, 실제 결과가 어디서 어긋났고, 그래서 규칙을 어떻게 고쳤는지에 대한 기록입니다.

먼저 밝혀 둘 것이 있습니다. 이 방식을 검증한 것은 기능 몇 개짜리 한 묶음의 시범, 작성자 한 사람, 인계 한 번이 전부입니다. 그 뒤 범위를 넓히며 바꾼 규칙들도 있지만, 여러 사람이 함께 쓰거나 오래 유지할 때 드는 비용은 아직 재지 못했습니다. 그래서 아래의 결과는 일반화할 만큼 쌓인 증거가 아니라, 한 번의 시범과 그 뒤의 확장에서 본 것입니다. 본문의 예시는 모두 가상의 «회의실 예약 서비스»로 바꿔 적었고, 요구 ID·결정 ID·커밋 SHA 도 자리표시자입니다.

흩어진 기획 위에서 «무엇이 남았나»에 답하기

이 프로젝트에서 저는 서버와 인프라 구현을 맡았고, 제품 기획과 결정은 기획 쪽이 소유했습니다. 기획 결정은 결정 로그, PRD, 인계 문서 여러 곳에 나뉘어 있었고, 뒤의 결정이 앞의 결정을 대체하기도 했습니다. 기획 문서는 저장소에 꾸준히 병합되었고, 열린 이슈와 PR 도 많았습니다.

그래서 기능 하나가 지금 어디에 있는지 물으면, 답은 늘 여러 문서와 이슈, PR, 배포 기록을 처음부터 다시 맞춰 봐야 나왔습니다. 질문은 같은데 조사는 매번 새로 시작되었습니다.

OpenSpec 이 제안하는 것 — 지금의 사실과 바뀌는 것을 가르기

OpenSpec 은 Fission-AI 가 MIT 라이선스로 공개한 스펙 주도 개발 도구입니다. 아래는 이 글을 쓰는 시점의 README 와 공식 문서에서, 뒤의 이야기를 읽는 데 필요한 만큼만 옮긴 것입니다. README 의 «Why OpenSpec?» 절은 이렇게 시작합니다.

AI coding assistants are powerful but unpredictable when requirements live only in chat history. OpenSpec adds a lightweight spec layer so you agree on what to build before any code is written.

요구가 대화 기록에만 있으면 AI 코딩 도구는 강력한 만큼 예측하기 어렵고, 그래서 코드를 쓰기 전에 무엇을 만들지 합의하는 가벼운 명세 층을 두자는 말입니다. 여러 AI 코딩 세션이 함께 일하던 이 프로젝트에서도, 요구가 대화 기록에만 있으면 세션이 바뀔 때마다 맥락이 끊겼습니다. README 가 내세우는 철학 가운데에는 «built for brownfield not just greenfield», 즉 새로 시작하는 프로젝트만이 아니라 이미 돌아가는 프로젝트를 위해 만들었다는 항목도 있습니다.

OpenSpec 의 specs 와 changes — 지금의 동작과 제안된 변경

가장 중요한 구분은 specs 와 changes 입니다. 개념 문서는 «Specs are the source of truth — they describe how your system currently behaves.»라고 적습니다. openspec/specs/ 는 시스템이 지금 어떻게 동작하는지를 적는 정본이고, openspec/changes/<이름>/ 은 아직 합치지 않은 변경 제안입니다. 변경 폴더에는 왜 무엇을 바꾸는지 적는 proposal.md, 바뀌는 요구만 담은 델타 명세(specs/), 기술 접근을 적는 design.md, 구현 체크리스트인 tasks.md 가 들어 있습니다.

델타 명세는 전체 명세를 다시 쓰지 않고 바뀌는 것만 적습니다. ## ADDED Requirements, ## MODIFIED Requirements, ## REMOVED Requirements 세 절로 나누고, 변경을 마쳐 archive 하면 ADDED 는 본 명세에 덧붙고, MODIFIED 는 기존 요구를 대체하고, REMOVED 는 본 명세에서 지워집니다. 변경 폴더는 날짜 접두어를 달고 changes/archive/ 로 옮겨져, 무엇을 왜 바꿨는지의 맥락이 그대로 남습니다.

요구는 평범한 마크다운입니다. ### Requirement: 아래에 SHALL 문장을 두고, #### Scenario: 아래에 GIVEN·WHEN·THEN 으로 사례를 적습니다. 이 글의 가상 도메인으로 옮기면 이런 모양이 됩니다.

## ADDED Requirements

### Requirement: Booking cancellation
The system SHALL allow a booking to be cancelled until 30 minutes before it starts.

#### Scenario: Cancel 31 minutes before the start
- GIVEN a confirmed booking
- WHEN the user cancels it 31 minutes before the start
- THEN the booking is cancelled

도구를 설치하면 openspec init 으로 프로젝트를 초기화하고, /opsx:explore → /opsx:propose → /opsx:apply → /opsx:archive 순서의 명령으로 변경을 탐색하고, 제안하고, 적용하고, 보관합니다. 이번에는 이 흐름을 쓰지 않았습니다.

OpenSpec Stores — 요구를 한 팀이 소유하고 다른 팀이 가져다 쓸 때

이번 상황에 더 가까운 기능으로는 아직 베타인 Stores 가 있습니다. README 는 팀에서 생기는 어려움 가운데 하나를 «requirements are owned by one team and consumed by others», 즉 요구는 한 팀이 소유하고 다른 팀이 가져다 쓴다고 표현하고, Stores 를 그 답으로 내놓습니다. 기획을 같은 openspec/ 모양의 별도 저장소에 두고 여러 코드 저장소가 그것을 참조하는 방식인데, 문서는 이 참조가 읽기 전용이라고 적습니다(«References are read-only.»).

기존 프로젝트에는 바꿀 부분만 명세하라는 권고

마지막으로 기존 프로젝트에 OpenSpec 을 들이는 방법을 다룬 문서가 있습니다. 첫 문장은 «You do not document your whole codebase to start. You write specs only for what you're about to change.»입니다. 처음부터 전체를 문서화하지 말고, 바꾸려는 부분만 명세하라는 것입니다.

기존 PRD 같은 문서는 «source material for exploration, not as specs to convert», 즉 변환할 명세가 아니라 탐색의 재료로 다루라고 합니다. OpenSpec 의 명세는 동작을 먼저 적고 변경 범위로 한정되어 있어서, 긴 PRD 와는 «a different artifact with a different job», 하는 일이 다른 문서라는 설명입니다. PRD 가 «무엇을, 왜 만드는가»에 답하는 문서라는 이야기는 «기획 문서의 종류와 체계 (1): 발견·정의 단계와 PRD» 에 적은 적이 있습니다.

그 권고의 이유도 적혀 있습니다. «Forcing a one-time bulk conversion tends to produce a large, stale spec nobody trusts.» 한 번에 몰아서 변환하면 아무도 믿지 않는 크고 낡은 명세가 되기 쉽다는 것입니다. 주의 사항의 첫 줄은 «Resist the urge to back-fill everything.»입니다. 미리 채운 명세가 낡는 까닭은 «nothing forces them to track reality», 즉 명세가 현실을 따라가도록 강제하는 것이 아무것도 없기 때문이라고 설명합니다. 이 권고는 뒤에서 제가 다르게 간 곳을 적을 때 다시 꺼내겠습니다.

OpenSpec 을 설치하지 않고 분리만 빌렸습니다

도구를 그대로 들이지 않은 이유는 셋이었습니다.

첫째, 기획의 정본은 기획 쪽의 것이었습니다. OpenSpec 의 specs/ 는 자기 폴더를 정본으로 삼습니다. 그 폴더로 기획을 옮겨 적으면 기획 쪽 문서와 제 폴더, 정본이 둘이 되고, 둘은 반드시 어긋납니다. 그래서 원칙을 하나 세웠습니다. «기획 본문을 베낀 또 하나의 정본을 만들지 않는다.» 정본은 하나만 둔다는 단일 진실 원천(SSOT, Single Source of Truth)의 원칙을, 다른 쪽이 소유한 기획에 대해서도 지키기로 한 것입니다. Stores 가 바로 이런 상황을 다루지만, 기획을 별도 저장소로 옮기는 일이 전제여서 이번 범위 밖이었습니다.

둘째, 다른 기여자에게 새 의무를 지우지 않으려 했습니다. 새 폴더도, 새 명령도, PR 본문의 새 칸도 요구하지 않았습니다. 문서를 쓰고, 갱신하고, 대조하는 일은 구현 쪽인 제가 맡았습니다. 문서를 쓰다 찾은 일을 넘길 때도 이슈와 PR 댓글처럼 이미 있는 길만 썼습니다.

셋째, 새 자동화나 전역 훅, CI 를 만들지 않고 기능 몇 개짜리 한 묶음으로 시범부터 했습니다. 시범을 하고, 평가하고, 넓힐지는 그다음에 판단했습니다. 규칙을 제안한 것과 승인받은 것, 실제로 적용한 것도 따로 기록했습니다. 규칙 문서를 병합했다는 것이 규칙이 작동한다는 뜻은 아니기 때문입니다.

그래서 빌린 것은 도구가 아니라 분리였습니다.

  • 요구 — 무엇을 만들면 끝인가. 목표입니다.
  • 현황 — 지금 어디까지 왔는가. 지금의 사실입니다.
  • 변경 수신 — 무엇이 바뀌었는가. 다른 쪽이 일으킨 변화를 받는 자리입니다.
  • 근거 — 무엇으로 그렇게 말하는가. 고정된 판을 가리키는 링크입니다.

예측은 단순했습니다. 기획은 기획 쪽 문서에 그대로 두고, 그 위에 기획을 가리키는 얇은 층 하나만 얹으면 같은 조사를 되풀이하지 않아도 될 것이라고 생각했습니다. 아래는 그 예측이 실제로 어떻게 되었는지에 대한 이야기입니다.

기획은 옮겨 적지 않고 가리킵니다 — 고정 커밋 링크

요구 하나에는 한 줄 요약과 정본의 위치만 둡니다. 정본의 위치는 결정 ID 로 적고, 결정 ID 가 없으면 절 제목으로 적습니다. 그리고 그 줄을 가리키는, 커밋 SHA 를 박은 고정 링크(permalink)를 겁니다.

R-07 예약 취소 — 회의 시작 30분 전까지만 취소 가능
  정본: D-12 · …/blob/<SHA>/<경로>#L<줄>

줄 번호는 그 SHA 아래에서만 뜻이 있습니다. 브랜치 이름 아래의 줄 번호는 기획 문서에 새 내용이 병합되면 다른 줄을 가리킬 수 있으므로 쓰지 않았습니다. 갱신할 때는 바뀐 줄만 새 판으로 다시 걸고, 바뀌지 않은 줄의 옛 판 링크는 그대로 둡니다. 그 판에서는 여전히 맞는 링크이기 때문입니다.

코드를 가리킬 때는 파일·함수·상수의 이름을 먼저 적고, 줄 번호는 링크의 앵커로만 씁니다. 주석을 한 번 정리하기만 해도 줄은 밀립니다. 표의 폭을 줄이려고 약어를 쓸 때는 링크 글자로만 쓰고, 약어와 경로를 잇는 표를 문서에 한 번 둡니다. 링크 없는 약어를 근거로 남기지 않기 위해서입니다.

어긋난 곳은 근거 칸에 있었습니다. 범위를 넓혀 쓴 명세에서 근거 칸 여러 곳이 열린 PR 의 번호를 근거처럼 적고 있었습니다. 열린 PR 이나 draft, 닫힌 채 병합되지 않은 PR 은 닫히거나 바뀔 수 있으니 근거가 될 수 없습니다. 그래서 열린 PR 은 근거가 아니라 진행 위치라는 원칙을 문서에 적고, 그런 자리마다 «근거 아님» 또는 «진행 위치»라고 표시했습니다. 진행 위치는 지금 어디서 그 일이 움직이고 있는지를 알려 줄 뿐, 무엇이 정해졌는지를 말해 주지는 않습니다.

링크 자체에는 문제가 없었습니다. 시범 문서에 건 고정 링크 수백 개를 경로와 줄 범위로 검사했고, 오류는 없었습니다. 다만 링크가 실재한다는 것과, 그 줄이 요구의 뜻을 맞게 담고 있다는 것은 다른 일입니다. 그 검사가 확인해 준 것은 «가리키는 곳이 있다»까지였고, 뜻은 표본을 골라 따로 대조했습니다.

OpenSpec 과 견주면, Stores 의 읽기 전용 참조와 «기존 문서는 변환 대상이 아니라 재료»라는 권고를 함께 빌린 셈입니다. 별도의 저장소를 두는 대신, 고정 커밋 링크로 읽기 전용 참조를 흉내 냈습니다.

현재 상태는 한 자리에서만 고칩니다

기획을 베끼지 않는 데는 성공했지만, 시범을 평가해 보니 같은 일이 다른 곳에서 일어나고 있었습니다. 이번에는 문서가 스스로 만든 사실을 베끼고 있었습니다. «이 기능은 어느 배포 회차에 운영에 반영되었다» 같은 현재 사실 하나가 문서 안 열 군데 가까이에 적혀 있었습니다. 사실이 바뀌면 그 모든 곳을 고쳐야 하고, 하나만 놓쳐도 요약표와 상세표가 서로 다른 말을 합니다.

『The Pragmatic Programmer』 는 DRY 원칙을 «Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.»이라고 정의합니다. 모든 지식은 시스템 안에서 하나의, 모호하지 않은, 권위 있는 표현을 가져야 한다는 뜻입니다. 기획에 대해서는 이 원칙을 지키려 했는데, 정작 문서가 만든 지식에는 적용하지 못하고 있었던 셈입니다.

그래서 범위를 넓힐 때는 파일을 역할별로 갈랐습니다.

파일(역할)담는 것
안내범위, 근거의 우선순위, 읽는 순서
요구바꾸지 않는 ID, 동작·입출력·예외, 필요한 짝, 수락 조건
현황요구 ID 별 코드·시험·병합·배포·관찰·남은 일·담당·차단과 재개 조건 — 현황을 고치는 단 하나의 자리
근거기획·결정·코드의 고정 판과 인용 위치
변경 수신들어온 변경과 미결 질문의 원문. 현재 상태는 적지 않고 해당 요구 행을 가리킴
보조원본 이슈·PR 대응표, 역방향 추적, 의존과 실행 순서

요약표와 기능표에는 현황 행으로 가는 ID 링크만 둡니다. 상태를 나타내는 낱말이나 다음 행동을 그 표에 옮겨 적지 않습니다. 옮겨 적는 순간 그 표는 또 하나의 사본이 됩니다.

다만 검증과 배포의 이력은 고치지 않습니다. 이력은 시각과 SHA 가 고정된 과거의 관측이어서, 최신 상태로 덮어쓰지 않습니다. 새로 실행했으면 새 이력으로 덧붙입니다. 현황은 한 자리에서 고치고, 이력은 덧붙이기만 합니다.

OpenSpec 과 견주면 이 부분은 한 번 더 가른 것입니다. OpenSpec 은 지금의 동작(specs)과 제안된 변경(changes)을 가르고, archive 로 이력을 남깁니다. 여기서는 목표 동작(요구)과 현재 구현 상태(현황)를 다른 파일로 한 번 더 갈랐습니다. 기획이 앞서 가고 구현이 따라가는 프로젝트라 둘 사이의 시차가 컸기 때문입니다. OpenSpec 의 specs 는 «지금 이렇게 동작한다»를 적는데, 우리에게는 «이렇게 정해졌지만 아직 그렇지 않은» 동작이 많았습니다.

링크만 있는 표는 구현 명세가 아니었습니다 — 행동과 수락 조건

시범 문서는 기획을 가리키는 층, 말하자면 색인이었습니다. 범위를 넓히려 하자 앞의 예측이 어긋났습니다. 링크만 모은 표에서는 구현 단위가 나오지 않았습니다. 무엇을 만들면 끝인지가 표에 없었기 때문입니다.

그래서 구현 명세에서는 요구마다 몇 가지를 짧게 적기로 했습니다. 바꾸지 않는 ID, 사용자의 동작과 입력·출력·판정·예외·비범위, 그 동작에 필요한 짝(화면·API·서버·DB·파일 출력), 그리고 완료 조건입니다. 완료 조건에는 합격 사례와 실패 사례를 함께 적습니다.

R-07 예약 취소
  동작   예약한 사람이 자기 예약을 취소
  판정   회의 시작 30분 전까지만 허용
  짝     취소 버튼(화면) · 취소 API · 예약 상태 칸(DB) · 취소 알림 메일
  합격   시작 31분 전 취소 → 성공
  실패   시작 29분 전 취소 → 거절 메시지
  정본   D-12 · …/blob/<SHA>/<경로>#L<줄>

원문을 전부 복사하지도, 명세에서 새로운 제품의 뜻을 정하지도 않습니다. 기획의 뜻이 분명하지 않으면 명세에서 메우지 않고 미결로 남긴 뒤 질문을 연결합니다. 짧은 행동 명세는 기획을 대신하는 문서가 아니라, 기획을 구현 단위로 읽어 내는 문서입니다.

합격과 실패 사례를 함께 적는 모양은 OpenSpec 의 Scenario 자리와 같고, 인수 기준(Acceptance Criteria)을 Given-When-Then 으로 적는 방식과도 같습니다. 인수 기준이 «어디까지 되면 완성인가»를 못 박는 문서라는 이야기는 «기획 문서의 종류와 체계 (3): TRD·ADR·API Spec과 검증» 에 적은 적이 있습니다. OpenSpec 문서가 명세를 «behavior-first»라고 부르는 것도 같은 뜻으로 읽었습니다.

세는 규칙도 정했습니다. 새 동작 하나가 수락 단위 하나입니다. 어떤 동작을 하고 되돌리는 것, 저장, 권한, 화면은 그 동작의 짝이지 별도의 요구가 아닙니다. 예약 취소에 딸린 버튼·API·상태 칸·알림 메일을 요구 넷으로 세지 않는 것과 같습니다. ID 는 지우지도, 다시 쓰지도 않습니다.

숫자에 대해서도 두 가지를 정했습니다. 하나는 분류한 건수가 검토한 건수가 아니라는 것입니다. 열린 항목을 전부 어딘가에 분류했다고 해서 전부 검토한 것은 아닙니다. 분류는 검토의 대리 지표일 뿐이어서, 둘을 따로 셉니다. 다른 하나는 이 문서에서 진행률(%)을 계산하지 않는다는 것입니다. 상태를 모르는 항목이 섞인 분모로 낸 진행률은 숫자의 모양을 한 거짓말이 됩니다.

상태는 여러 사실로 쪼갰습니다 — 미상은 0 이 아닙니다

OpenSpec 의 tasks.md 는 체크박스 목록입니다. 값은 완료와 미완, 둘입니다. 한 변경 안의 할 일을 지워 나가기에는 충분하지만, 배포 단계가 여럿인 프로젝트에서 «이 요구는 지금 어디에 있나»에 답하기에는 값이 모자랐습니다.

그래서 서로 대신 쓰지 않는 사실들로 쪼갰습니다. 기획 확정, 코드 존재, 시험, 공유 브랜치 병합, 개발 서버 배포, main 반영, 운영 배포, 운영 관찰입니다. 공유 브랜치에 병합되었다는 것은 운영에 나갔다는 뜻이 아니고, 운영에 배포되었다는 것은 운영에서 관찰했다는 뜻이 아닙니다. 각 사실에는 근거 표지를 하나씩 붙였습니다.

표지뜻
[직접]문서를 쓴 쪽이 그 판에서 명령이나 조회로 확인함
[보고]PR 본문이나 댓글에 적힌 작성자의 주장
[추정]간접 증거뿐

상태를 나타내는 낱말도 정해 두었습니다. 사실마다 있음·부분·없음·해당 없음(사유)·미확인(사유) 가운데 하나를, 요구 전체에는 구현·부분·남음·미결·대체 가운데 하나를 씁니다. 그리고 미상, 조회 실패, 미검증을 0 이나 정상, 완료로 바꾸지 않습니다.

R-07 예약 취소 — 현황
  코드         있음 [직접]
  시험         통과 [직접]
  공유 브랜치  병합 [직접]
  운영 배포    미확인 — 배포 기록 조회 실패 (조회 시각을 함께 적음)

모르는 것을 0 으로 적으면, 그 0 은 «없다»는 주장이 됩니다. 배포 기록을 읽지 못한 것과 배포되지 않은 것은 다른 사실입니다.

이렇게 쪼갠 덕분에, 이미 운영에 반영된 기능과 같은 기능에서 기획 쪽에 남은 후속 작업을 동시에 적어 둘 수 있었습니다. 상태 하나로 뭉갰다면 «완료»와 «진행 중» 가운데 하나만 남았을 것입니다.

갱신은 병합 밖에서도 일어났습니다 — 기준 두 칸과 델타

문서는 한 번 쓰고 끝나지 않습니다. 처음 정한 갱신 절차는 «지난 기준 이후의 병합 기록 확인»에 치우쳐 있었습니다. 그런데 병합 기록만 보면 빠지는 것이 많았습니다. 질문에 대한 답변과 승인, 결정의 해소, 열린 PR 의 head·범위·상태 변화, 새 검증 결과와 실패, 배포와 운영 확인입니다. 협업 프로젝트에서 상태를 바꾸는 일은 병합 밖에서도 일어났습니다.

그래서 기준을 두 칸으로 갈랐습니다.

  • 코드 기준 — 공유 브랜치의 커밋 SHA. git 으로 잽니다.
  • 협업 확인 시각 — PR·이슈의 상태와 댓글, 리뷰, 배포 기록을 마지막으로 다시 본 시각.

둘 중 하나만 새로 봤다면, 다른 하나는 옛 값을 그대로 둡니다. 코드는 새로 봤는데 PR 댓글을 다시 보지 않았다면, 협업 확인 시각을 새로 쓰지 않습니다.

확인하는 범위는 관련 항목의 델타로 좁혔습니다. 문서가 이미 가리키는 PR·이슈의 변화, 옛 기준과 새 기준 사이의 병합 가운데 문서가 가리키는 파일·결정 ID·기능 낱말을 건드린 것, 그리고 릴리스 기록입니다. 저장소 전체를 다시 조사하지는 않습니다. 문서가 모르던 새 PR 을 찾는 일은 따로 있는 «기획 변경 영향 조사» 절차의 몫이고, 이 문서는 그 결과를 받습니다.

어떤 계기가 생기면 어떤 기록을 보고 어느 자리를 고치는지도 표로 정해 두었습니다. 예를 들어 열린 PR 의 head·범위·draft 상태가 바뀐 것이 계기라면, 그 PR 의 head SHA 와 파일 목록을 보고, 현황 행을 고친 뒤 새 검증 이력을 덧붙입니다.

조회에 실패하면 «미확인(무엇을 못 봤나 · 시각)»으로 남기고 앞의 값을 지우지 않습니다. 하나라도 못 본 판은 «다 봤다»고 적지 않습니다. 그리고 갱신할 때마다 기록을 한 줄 남깁니다.

옛 기준(코드 SHA · 협업 확인 시각) → 새 기준(코드 SHA · 협업 확인 시각)
계기 · 본 자료 · 고친 자리 · 놓친 변화 · 걸린 시간(실측과 추정을 가름)

갱신이 중간에 끊기면 이 기록의 마지막 줄부터 다시 합니다. 이 절차를 위한 새 자동화는 만들지 않았습니다. 절차를 문서로 적었다고 해서 자동화가 생긴 것처럼 말하지 않으려 했습니다.

이 절차로 실제 갱신을 한 번 해 보니 이런 것들을 찾았습니다. 오래된 배포 대기, 이미 해결된 문서 사본, 추적이 빠진 문장, 그리고 같은 이름의 숫자가 화면과 계산에서 서로 다른 뜻으로 쓰이던 혼동입니다.

시간 측정에서는 어긋남이 있었습니다. 핵심 편집은 20분이 채 안 걸렸다고 기록했지만, 그 구간에는 다른 작업이 섞여 있어서 순수한 작업 시간은 모릅니다. 검토를 요청하고 자동 리뷰 결과를 받기까지는 벽시계로 40분 가까이 걸렸습니다. 그래서 이 한 번을 «평소 갱신은 30분이면 된다» 같은 결론으로 일반화하지 않았습니다.

OpenSpec 의 changes 는 변경을 제안하는 쪽이 직접 쓰는 문서입니다. 여기서는 제가 쓰지 않은, 다른 쪽이 일으키는 변화를 따라가야 했고, 그래서 이 갱신 절차를 따로 덧붙였습니다.

문서에서 못 찾은 것은 결정 요청이 아니었습니다 — 미결의 갈래

시범 초기에는 문서에서 찾지 못한 것을 곧바로 «결정권자에게 다시 물을 것»으로 올리고 있었습니다. 돌이켜 보면 «제가 못 찾았다»와 «아직 정해지지 않았다»를 같은 것으로 다룬 셈입니다. 그래서 규칙을 바꿨습니다. 최신 결정 로그와 기존 회신(PR·이슈 댓글, 리뷰), 인계 문서를 먼저 봅니다. 그래도 남는 선택만 근거·선택지·추천·영향을 붙여 올립니다.

미결도 한 칸에 몰지 않고 갈래를 나눴습니다.

  • 구현 대기 — 유효한 결정이 있고 구현만 남음
  • 근거 확인 — 결정이라고 적힌 문장의 기록을 찾지 못했거나, 정할 권한이 있는 쪽이 아직 정하지 않음
  • 대체 확인 — 둘 다 유효해 보이는데, 뒤 결정이 앞 결정을 대체한다는 표지가 없음
  • 정책 충돌 — 둘 다 유효하다고 확인된 결정이 같은 범위에서 부딪침
  • 결정권자 판단 — 결정권자의 판단 말고는 닫을 길이 없음
  • 해결됨 — 근거가 이미 답함. 근거 링크와 함께 닫되 줄은 남김

대체 확인에는 규칙을 하나 붙였습니다. 날짜가 뒤라는 것만으로 대체로 보지 않습니다. 앞의 결정이 대체되었다는 것은 표지로 밝혀져 있어야 합니다. ADR 이 지난 결정을 지우지 않고 «대체됨»으로 표시한 뒤 새 결정을 잇는 것처럼, 대체는 날짜가 아니라 기록이 말해야 합니다.

막는 정도도 셋으로 나눴습니다. 지금 할 작업이 나아가지 못하는 «현재 차단»에는 무엇이 막히는지를, 아직 오지 않은 조건에 걸린 «미래 보류»에는 그 조건을 적습니다. 나머지는 «막지 않음»입니다.

미결은 문서 안에만 쌓아 두지 않습니다. 이슈나 PR 링크를 갖게 하고, 링크가 없으면 «연결 없음 — 다음 행동: …»으로 적습니다.

OpenSpec 의 변경 폴더에서 결정은 제안과 설계 문서(proposal·design) 안에 함께 적힙니다. 결정권자가 여럿인 이 프로젝트에서는 미결의 갈래를 더 잘게 나눠야 했습니다.

OpenSpec 문서와 다르게 간 곳 — 처음 한 번은 전부 대조했습니다

앞에서 옮긴 것처럼 OpenSpec 문서는 기존 프로젝트에 바꿀 부분만 명세하라고 권합니다. 전부를 미리 채우려 하지 말고, 기존 문서를 한꺼번에 변환하면 아무도 믿지 않는 낡은 명세가 된다고 경고합니다.

범위를 현재 버전 전체로 넓힐 때 저는 이 권고와 다르게 갔습니다. 처음 한 번은 양방향으로 대조했습니다. 한쪽으로는 열린 이슈와 PR 을 전부 적어도 하나의 요구에, 또는 문서·인프라·미래·참고용 같은 분류에 연결했습니다. 반대쪽으로는 이슈나 PR 이 없는 확정 요구와, 코드에만 남아 있던 후속 작업을 찾아 기존 추적에 연결했습니다. 요구사항 공학에서는 요구를 출처 쪽으로도, 구현 쪽으로도 따라갈 수 있는 성질을 양방향 추적성(bidirectional traceability)이라고 부릅니다. 이번 대조는 그 요구사항 추적을 처음 한 번 전체에 대해 해 둔 셈입니다.

질문이 달랐기 때문입니다. 제가 답해야 하는 질문은 «다음에 바꿀 한 조각»이 아니라 «현재 버전에서 무엇이 남았나»였습니다. 릴리스를 판단하려면 전체 그림이 필요했습니다.

대신 낡은 명세가 되지 않도록 몇 가지 장치를 붙였습니다.

  • 분류한 건수를 검토를 마친 건수로 세지 않습니다.
  • 처음 목록에서 미검토·해소·잔여로 남은 것과, 그 뒤에 새로 생긴 항목을 따로 셉니다.
  • 기준선을 잡은 뒤에는 관련된 델타만 받습니다. 같은 이유로 전체 조사를 되풀이하지 않습니다.
  • 현황은 한 자리에서만 고칩니다.
  • 진행률을 계산하지 않습니다.

이것이 OpenSpec 의 경고가 틀렸다는 근거는 아닙니다. OpenSpec 문서는 미리 채운 명세가 낡는 까닭을 «nothing forces them to track reality»라고 적었습니다. 위의 장치들은 그 강제를 자동화 없이 사람의 절차로 대신해 보려는 것인데, 그 절차가 오래 지켜질지는 아직 모릅니다. 장기적인 유지 비용은 재지 못했고, 경고가 맞는지는 시간이 지나야 알 수 있습니다.

아직 모르는 것

  • 시범은 기능 몇 개짜리 한 묶음, 작성자 한 사람, 인계 한 번에서만 검증했습니다. 여러 사람이 동시에 쓰거나 오래 유지할 때 드는 비용은 모릅니다.
  • 링크가 실재한다는 것과 뜻이 맞다는 것은 다릅니다. 고정 링크는 경로와 줄 범위로 검사했지만, 뜻은 표본으로만 대조했습니다.
  • 갱신 비용은 한 번만 쟀고, 그 한 번도 순수한 작업 시간은 모릅니다.
  • 800줄 가까이 자란 시범 문서를 기능마다 통째로 복제하는 방식은 권하지 않습니다. 작은 묶음씩 넓히고, 묶음마다 다시 평가하는 편이 낫다고 봅니다.

정리 — 스펙 주도 개발에서 빌린 것은 도구가 아니라 분리였습니다

OpenSpec 에서 빌린 것과 바꾼 것을 한 표로 모으면 다음과 같습니다.

OpenSpec이번에 한 것달라진 이유
specs — 지금 동작의 정본요구와 현황을 다른 파일로정해졌지만 아직 아닌 동작이 많음
changes · 델타변경 수신과 기준 두 칸의 델타 갱신내가 쓰는 제안이 아니라 다른 쪽이 일으키는 변화
Requirement · Scenario짧은 행동 명세와 합격·실패 사례가리키는 표만으로는 구현 단위가 나오지 않음
tasks.md 체크박스사실별 상태와 근거 표지배포 단계가 여럿
Stores 의 읽기 전용 참조고정 커밋 링크기획을 별도 저장소로 옮기는 일은 범위 밖
바꿀 부분만 명세처음 한 번 양방향 대조, 그 뒤는 델타질문이 «현재 버전에서 무엇이 남았나»

돌이켜 보면 OpenSpec 에서 가져온 것은 명령어가 아니라 경계였습니다. 목표와 지금의 사실, 바뀌는 것, 근거를 서로 다른 자리에 두는 것입니다. 다른 쪽이 소유한 기획 위에서는 옮겨 적지 않고 가리키는 것이 기본이었고, 짧은 행동 명세는 구현 단위가 필요해졌을 때 덧붙였습니다. OpenSpec 이나 Spec Kit 같은 스펙 주도 개발 도구에 관심은 있지만, 기획 문서가 이미 있고 그 주인이 따로 있어서 통째로 들이기 어렵다면, 도구보다 이 경계부터 빌려 보는 것도 방법이라는 생각이 듭니다.

사례들을 다시 읽어 보니, 어긋난 지점 여럿이 같은 뿌리에서 나왔다는 생각이 들었습니다. 열린 PR 을 근거로 적은 칸은 아직 정해지지 않은 것을 정해진 것처럼 적었고, 병합 기록에 치우친 갱신은 보지 않은 것을 바뀌지 않은 것처럼 다뤘습니다. 문서에서 못 찾은 것을 곧바로 결정 요청으로 올린 것은 못 찾은 것을 정해지지 않은 것으로 읽은 것이었고, 다른 작업이 섞인 구간을 편집 시간으로 기록한 것도 같은 모양입니다. 모두 모르는 것을 아는 값으로 바꿔 적는 일이었습니다. 미상은 0 이 아닙니다. 세지 못한 것을 세지 못했다고 적어 두면, 적어도 다음 갱신이 어디서부터 다시 봐야 하는지는 문서에 남습니다.

OpenSpec 의 경고가 이 문서에도 맞을지는 아직 모릅니다. 그것도 세지 못한 것으로 적어 둡니다.

OpenSpec스펙 주도 개발Spec-Driven Development구현 명세요구사항 추적기획문서문서화