Spec-Driven Development 시작하기: 먼저 명세(Spec)를, 나중에 프롬프트(Prompt)를
요약
AI 에이전트를 활용한 개발 시 발생하는 예기치 않은 결정과 환각 문제를 해결하기 위한 '명세 기반 개발(Spec-Driven Development)'의 중요성을 설명합니다. 프롬프트만으로는 제어하기 어려운 세부 구현 로직을 명세(Spec)를 통해 사전에 정의해야 함을 강조합니다.
핵심 포인트
- AI 에이전트는 명시되지 않은 세부 구현을 학습 데이터의 관습에 따라 임의로 결정함
- 명세(Spec) 없이 프롬프트만 사용하면 디바운스 누락이나 잘못된 로직 등 문제가 발생함
- Spec-Driven Development(SDD)는 에이전트의 숨겨진 결정을 방지하는 기준점이 됨
- 효율적인 AI 코딩을 위해서는 기능 구현과 함께 정확한 결정 사항을 명세로 제공해야 함
지난 몇 달 동안 생각해 온 아이디어를 AI 에이전트 덕분에 현실로 만드는 것이 이렇게 쉬울 줄은 몰랐습니다. 기본적인 직관은 이렇습니다. 프롬프트를 주면, 전체 기능을 구축하고, 결과물도 멋지게 나옵니다. 끝입니다. 원래는 몇 시간이 걸렸을 만한 것을 단 몇 분 만에 만들 수 있습니다.
네, 알아요. 모두가 그렇게 하고 있다는 거요. 그렇죠?
제가 이렇게 시작하는 이유는 그 이후에 무슨 일이 일어났는지 지적하기 위해서입니다.
저는 검색창을 사용해 보았는데, 키를 누를 때마다 요청이 발송되었습니다.
잠깐만요, 뭐라고요? 저는 그렇게 하지 않았어요. 물론 여기에 디바운스(debounce)를 추가해야 합니다. 하지만 에이전트는 그렇지 않았습니다. 왜일까요? 제가 그것을 요구하지 않았기 때문입니다. 저는 '검색창을 만들어줘'라고 말했고, 작동하는 검색창을 만들었지만; 제가 정확히 무엇을 원하는지 말하지 않았습니다.
또한, 검색 버튼에 마우스를 올리면 색상이 변한다는 것을 알아차렸는데, 저는 이미 그렇게 하지 말라고 말했었습니다. 에이전트는 잊어버렸고, 환각(hallucinated)을 일으켰습니다. 그럼 무엇이 빠졌을까요?
빠진 것은 제가 에이전트에게 기능과 함께 사용할 정확한 결정 사항들; 또는 환각을 수정하기 위해 되돌아갈 적절한 기준점(reference point)을 제공하지 않았다는 것입니다. 다시 말해, 저는 적절한 명세(spec)를 제공하지 않은 것입니다. 그래서 그것은 스스로 숨겨진 결정들을 내렸습니다. 비록 기능을 구현해냈지만요. 이것이 Spec-Driven Development (SDD)가 해결하는 핵심 문제입니다.
AI 에이전트가 당신을 위해 내리는 숨겨진 제품 결정들
AI 에이전트에게 무언가를 설명하고 에이전트가 코드를 생성할 때 다음과 같은 일이 발생합니다. 즉, 수많은 결정이 내려집니다. 검색창 구현을 예로 들어보겠습니다. 필터링(filtering)은 클라이언트(client)에서 이루어지나요, 아니면 서버(server)에서 이루어지나요? 결과를 공유할 수 있도록 URL이 업데이트되나요? 빈 쿼리(empty query)에는 무엇을 보여줘야 할까요? 모든 것을 보여줄까요, 아니면 아무것도 보여주지 않을까요?
짧은 시간 안에 엄청난 양의 AI 생성 코드를 검토하다 보면 저는 세세한 디테일(nitty-gritty details)을 놓치는 경향이 있습니다. 코드는 작동하고, UI는 제대로 보이고, 그러면 저는 다음 단계로 넘어갑니다... 하지만 그 결정 하나하나가 모두 제 제품(product)에 속하는 결정들입니다. 제가 의식적으로 결정을 내리지 않으면, 에이전트는 자신의 학습 데이터(training data)에서 가장 자주 나타나는 패턴에 따라 결정을 내립니다.
그 검색 필터를 예로 들어보겠습니다. 에이전트에게 그냥 맡겨두면 다음과 같은 결과물을 내놓습니다:
onChange={(e) => fetchResults(e.target.value)}
const filtered = results.filter(item =>
item.name.includes(query) && activeCategory === item.category
...
괜찮아 보입니다. 하지만 제 제품에는 AND 논리가 틀릴 수도 있습니다. 사용자는 아마 OR 논리를 기대할지도 모릅니다. 쿼리에 대해 아무것도(nothing) 반환하는 것은 제 사용자가 필요로 하는 것과 반대될 수도 있습니다. 저 onChange는 키를 누를 때마다 조용히 백엔드(backend)를 호출하며, 요청을 날린 뒤 중간에 중단해 버립니다.
에이전트는 자신의 학습 데이터에 있는 관습(conventions)을 따랐을 뿐 그 외에는 아무것도 하지 않았습니다. 관습이 제 특수한 상황에 대한 정답은 아니었으며, 저는 관여할 기회조차 얻지 못했습니다.
저는 예전에 해결책이 AI 출력물을 주의 깊게 검토하는 것이라고 생각했습니다. 하지만 그것이 항상 통하는 것은 아닙니다. 문제는 제가 무언가를 검토할 때쯤이면 이미 결정 사항들이 코드 속에 파묻혀 버린다는 점이었습니다.
명세(Spec)란 무엇이며, 무엇이 아닌가
"먼저 명세(Spec)를 작성하라(Write a spec first)"라는 말은 30페이지짜리 제품 요구 사항 문서(PRD)에 붙이는 포스트잇처럼 들릴 수도 있습니다. 좀 더 구체적으로 말씀드리겠습니다.
그것은 마크다운(Markdown) 파일입니다. 에디터를 열기 전에 작성하며, 제가 생각할 수 있는 모든 동작 관련 질문에 답하는 단 하나의 파일입니다. 각 상태에서 컴포넌트는 무엇을 렌더링하는가? 어떤 상호작용(Interaction)이 어떤 동작을 트리거하는가? 경계 조건(Edges)에서는 어떤 일이 발생하는가? 이는 희망 사항이나 "나중에 해결하자" 식의 내용이 아닙니다. 이미 결정된 사항이어야 합니다.
만약 여러분이 구현을 시작하기 전에 평이한 언어로 기능의 동작을 정의하는 관행인 행동 주도 개발 (BDD, Behavior-Driven Development)을 해본 적이 있다면, 이는 도구만 제거된 동일한 직관입니다. BDD는 철학은 옳았지만, 대부분의 팀이 6개월 후 조용히 포기하게 만드는 방대한 병렬 인프라를 유지 관리해야 한다는 문제가 있었습니다. AI가 읽을 수 있는 마크다운 파일은 그러한 오버헤드 없이도 동일한 효과를 얻게 해줍니다.
이것은 완전히 새로운 아이디어도 아닙니다. GitHub은 2025년에 이 워크플로우를 위해 특별히 제작된 spec-kit이라는 도구를 출시했습니다. 더 무거운 버전도 있습니다. OpenAPI나 JSON Schema를 사용하는 기계 판독 가능 명세(Machine-readable specs)로, 명세 자체가 테스트와 모크(Mock)를 자동으로 생성하는 방식이지만, 이는 상당한 도구 투자(Tooling investment)가 필요합니다. 마크다운 표(Table)를 사용하는 것만으로도 20분 만에 대부분의 이점을 얻을 수 있으며, 저는 그 방식부터 시작할 것을 권장합니다.
하지만 도구는 흥미로운 부분이 아닙니다. 진짜 흥미로운 부분은 여러분이 자리에 앉아 명세를 작성하려고 시도할 때 일어나는 일입니다.
동작 표(Behavior table)의 두 번째 줄 정도를 작성하다 보면, 스스로에게 의식적으로 질문해 본 적 없는 문제에 부딪히게 될 것입니다. "결과 없음" 상태는 어떤 모습인가? 메시지인가, 일러스트인가, 아니면 아무것도 없는 상태인가? 요청이 진행 중(In flight)일 때 사용자가 타이핑을 하면, 이전 요청을 취소해야 하는가 아니면 기다려야 하는가? 사용자가 필터링된 URL을 동료와 공유할 수 있는가, 아니면 필터가 컴포넌트 상태(Component state)에만 존재하는가? 요구 사항이 변경될 때 이 파일의 소유권은 누구에게 있는가?
그렇긴 하지만, 명세는 최신 상태를 유지할 때만 가치가 있다는 점을 경고해야 합니다. 만약 명세가 실제 코드가 수행하는 동작과 어긋나기 시작하면, 명세가 없는 것보다 더 나쁜 상황, 즉 "자신 있게 거짓말을 하는 문서"가 되어버립니다.
SDD가 TDD와 다른 점
테스트를 먼저 작성한다면, 이 방식은 자연스럽게 적용됩니다. 테스트 주도 개발(TDD)은 코드를 작성하기 전에 실패하는 테스트를 작성하는 관행으로, 일관성을 유지하게 해줍니다. 하지만 그것이 올바르다는 것을 보장해주지는 않습니다. 잘못된 동작을 인코딩한 테스트를 작성할 수도 있고, 그 테스트는 영원히 그 잘못된 동작을 옹호할 것입니다. 테스트는 구현(implementation)을 검증하지만, 무엇이 테스트 자체를 검증할까요? 바로 명세(spec)입니다. 그것은 테스트 위에 있는 결정의 근거지입니다. 테스트가 실패하면 코드를 수정합니다. 명세가 변경되면, 먼저 명세를 업데이트하고, 그다음 테스트를, 그리고 마지막으로 코드를 업데이트해야 합니다. 항상 이 순서로 말이죠. 저에게는 TDD 위에 레이어를 추가하는 느낌이 아니라, 제 테스트를 검증하는 진실의 원천(source of truth)처럼 느껴집니다.
실제 적용에서의 SDD 워크플로우: 명세, 테스트, 구현
동일한 컴포넌트—제품 목록을 위한 검색 및 필터 UI—로 진행해 보겠습니다. 제가 생각할 필요 없이 프롬프트로 요청할 수 있는 종류의 것입니다.
1단계: 명세 정의하기
저는 빈 SPEC.md 파일을 열고 시나리오를 채우기 시작했습니다. 이것만으로도 저는 그렇지 않았다면 완전히 건너뛰었을 결정들을 내리도록 강요받았습니다. '디바운스(debounce)' 질문이 즉시 떠올랐습니다—제가 한 행으로 '사용자가 쿼리를 입력한다'고 작성하자, 동작(behavior) 열을 채워야 했습니다. 그리고 그것은 다음 질문을 이끌어냈습니다: 디바운스 시간보다 더 빠르게 타이핑하면 어떻게 될까요? 디바운스가 작동할 때 이미 요청이 진행 중이라면요? URL 업데이트 요구사항은 제가 스스로에게 '사용자가 필터링된 검색 결과를 친구와 공유할 수 있을까?'라고 물었기 때문에 나타났습니다. 기술적인 질문이 아니라 제품에 대한 질문이었죠. 프롬프트만으로는 그런 생각은 하지 못했을 것입니다.
명세를 완성했을 때의 모습은 다음과 같았습니다:
검색 필터 컴포넌트
| 상태 / 상호작용 | 동작 |
|---|---|
| 페이지 로드, 쿼리나 필터 없음 | 모든 결과 표시 |
| ... |
- 대소문자 구분 없는 매칭(Case-insensitive matching)
- 검색 시마다 URL 업데이트 (q= 및 category= 파라미터)—결과가 공유 가능함
- 필터 변경에 따른 정렬 순서 유지
"단순한" 컴포넌트라고 하기에는 예상보다 행(row)이 더 많습니다. 그것이 바로 핵심입니다.
2단계: 테스트 작성하기.
그다음 저는 에이전트(Agent)에게 명세(Spec)를 전달했습니다: "SPEC.md를 읽고 거기서 파생된 계약 테스트(Contract tests)를 작성하세요. 아직 구현(Implementation)은 작성하지 마세요."
첫 번째 시도에서는 제가 명시하지 않은 것들을 테스트하는 결과가 돌아왔습니다. 저는 이를 반려했습니다. 두 번째 시도는 더 나았지만 여전히 임의로 만들어낸 동작(Behavior)이 포함되어 있었습니다. 세 번째 시도에 이르러서야 비로소 다른 것 없이 오직 명세(Spec)만을 따르는 결과가 나왔습니다.
여기서 언급할 가치가 있는 점이 있습니다: "명세를 읽고 테스트를 작성하라"는 마법의 프롬프트(Prompt)가 아닙니다. 에이전트는 자신만의 아이디어를 추가할 것입니다. 생성된 테스트를 풀 리퀘스트(Pull request)처럼 취급하세요. 모든 줄을 읽어야 합니다.
이 워크플로우를 가장 자주 망가뜨리는 요소: 에이전트에게 명세(Spec)와 테스트를 모두 처음부터 작성하라고 요구하지 마세요. 그렇게 하면 둘은 서로 동의하게 될 것이고, 둘 다 틀릴 수도 있습니다. AI는 자신의 가정을 명세(Spec)에 녹여내고, 그 가정을 강제하는 테스트를 작성하며, 결국 아무것도 증명하지 못하는 '통과(Green)'된 테스트 스위트(Test suite)를 얻게 됩니다. 이것이 바로 명세(Spec)가 반드시 당신으로부터 나와야 하는 이유입니다. AI는 당신이 엣지 케이스(Edge cases)를 생각하도록 도울 수 있지만, 테스트가 작성되기 전에 결정은 반드시 당신의 것이어야 합니다.
3단계: 구현.
테스트가 진정으로 명세(Spec)로부터 파생되었고 실패(Failing) 상태가 되었을 때, 저는 구현(Implementation)을 위한 프롬프트(Prompt)를 입력했습니다: "명세를 읽고, 테스트를 통과시키되, 명세에 없는 것은 아무것도 추가하지 마세요." 통과(Green). 완료. 디바운스(Debounce) 타이밍에 대해 추측할 필요도 없고, 빈 상태(Empty state)에 대한 창의적인 해석도 필요 없습니다. AI는 작업할 구체적인 기준이 있었기에 빠르게 움직였습니다.
제가 내내 지킨 한 가지 규칙이 있습니다: 구현 후 테스트가 실패하면, 코드를 수정합니다. 테스트를 수정하는 것이 아닙니다. 코드를 통과시키기 위해 테스트를 변경하는 순간, 당신은 조용히 명세(Spec)를 변경한 것입니다. 그리고 이 모든 과정의 핵심은 명세(Spec)가 AI의 것이 아니라 당신의 것이어야 한다는 점입니다.
SDD가 해결하지 못하는 것
SDD는 명세(Spec)가 잘못되었다면 도움이 되지 않습니다. 만약 제가 잘못된 동작, 잘못된 상태 코드(Status Code), 잘못된 비즈니스 로직, 잘못된 보안 모델(Security Model)을 명시한다면—구현 단계에서는 정확히 그 잘못된 내용을 충실하게 만들어낼 것입니다. 훌륭한 명세 프로세스는 많은 실수를 잡아내지만, 그것이 마법은 아닙니다. 저는 여전히 신중하게 생각해야 하며, 신중하게 생각하는 것은 연습이 필요한 기술입니다.
좋은 명세를 작성하는 것 또한 처음 몇 번은 보기보다 어렵습니다. 흔히 발생하는 경향은 '해피 패스(Happy Path, 정상적인 흐름)'만 작성하고 멈추는 것입니다. 엣지 케이스(Edge Case), 에러 시나리오, 보안상의 영향 등을 드러내기 위해서는 노력이 필요합니다.
마지막으로, SDD는 도메인 지식(Domain Knowledge)을 대체하지 않습니다. 문제를 충분히 이해하지 못한다면, 명세에도 그 결함이 반영될 것입니다. 사용자들과 대화하고, 시스템의 맥락을 이해하며, 실제로 무엇이 중요한지 파악하는 과정은 명세를 먼저 작성한다고 해서 생략되지 않습니다.
SDD가 하는 역할은 당신의 생각이 코드가 되기 전에 가시화되도록 만드는 것입니다. 만약 생각이 잘못되었다면, 더 빠르고 저렴하게 발견할 수 있습니다. 만약 생각이 옳다면, 나머지 프로세스는 극적으로 매끄러워집니다.
AI 에이전트가 SDD를 그 어느 때보다 중요하게 만드는 이유
SDD는 항상 좋은 관행이었습니다. 과거에는 사고 과정을 건너뛰더라도 복구가 가능했습니다. 코드 리뷰나 QA, 혹은 프로덕션(Production) 단계에서 누락된 케이스를 발견하고, 빠르게 패치를 적용하며 무언가를 배울 수 있었습니다.
AI는 그 타임라인을 붕괴시켰습니다. "모호한 아이디어"와 "실행 가능한 코드" 사이의 간극은 과거에는 몇 시간의 수동 작업이 필요한 영역이었고, 그 시간 동안 답변하지 못한 질문들을 발견할 수 있었습니다. 하지만 이제는 단 몇 초 만에 이루어집니다. 질문들이 수면 위로 떠오르지 않습니다. 대신 이미 테스트를 통과한 코드 아래에 파묻혀 버립니다.
SDD는 프롬프트(Prompt)를 작성하기 전, 코드를 작성하기 전, 무엇인가가 실행되기 전에 사고 과정을 다시 최전방에 배치합니다. 이는 AI가 코드를 생성하는 능력이 부족해서가 아닙니다. 무엇을 만들지 결정하는 것은 여전히 당신의 업무이며, 앞으로도 영원히 그럴 것이기 때문입니다.
명세(Spec)를 먼저. 프롬프트(Prompt)는 나중에.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기