스펙 주도 개발: 바이브 코딩을 넘어 AI 에이전트와 일하는 법
“장바구니 기능 만들어줘.”
코딩 에이전트에게 이렇게 요청하면 뭔가 그럴듯한 코드가 나옵니다. 그런데 결과를 보면 내가 원했던 것과 미묘하게 다릅니다. 상품 수량 변경이 빠져 있거나 할인 적용 로직이 내 의도와 다릅니다. 합계를 계산하는 방식이 요구사항과 안 맞기도 하죠. 에이전트가 멍청한 걸까요? 아닙니다. 내가 원하는 걸 충분히 명확하게 전달하지 않은 겁니다.
이런 식으로 분위기에 맡겨 코드를 생성하는 걸 바이브 코딩(Vibe Coding)이라고 부릅니다. 대략적인 의도만 던지고 에이전트가 알아서 해주길 바라는 거죠. 간단한 프로토타입에는 나쁘지 않지만 프로덕션 코드에는 위험합니다. 이 글에서는 바이브 코딩의 대안으로 떠오른 스펙 주도 개발(Spec-driven Development)을 이야기해 보겠습니다.
바이브 코딩의 함정
AI 코딩 에이전트를 쓰는 대부분의 개발자는 자연어로 요청합니다. “이런 함수 만들어줘”, “이 버그 고쳐줘”, “이 API 추가해줘”. 간단한 작업에는 이게 잘 먹힙니다. 하지만 비즈니스 로직이 복잡해지면 이야기가 달라지죠.
예를 들어 할인 계산 로직을 만들어달라고 요청한다고 해봅시다.
쿠폰 할인과 등급 할인이 동시에 적용될 때, 쿠폰 할인을 먼저 적용한 금액에
등급 할인을 적용해줘. 단, 최종 할인율이 50%를 넘으면 50%로 제한해야 해.
언뜻 명확해 보이지만 빈틈이 많습니다. 원래 가격이 0원이면? 할인 금액이 음수면? 소수점 이하 절사인지 반올림인지? 이런 세부 사항을 전부 자연어로 기술하려면 프롬프트 자체가 하나의 문서가 됩니다. 그리고 그 문서는 자연어인 이상 여전히 해석의 여지가 있어요.
더 큰 문제는 검증이 안 된다는 점입니다. 에이전트가 내 프롬프트를 “올바르게” 이해했는지 확인하는 방법이 뭘까요? 생성된 코드를 한 줄씩 읽어보는 것뿐입니다. GitHub의 Spec Kit 팀도 같은 문제를 지적합니다. “언어 모델은 패턴 완성에는 뛰어나지만 마음 읽기에는 소질이 없다”고요.
스펙 주도 개발이란
스펙 주도 개발의 핵심 아이디어는 단순합니다. 에이전트에게 코드를 시키기 전에, 먼저 기대하는 결과를 정의한다. 이 “기대하는 결과”가 바로 스펙(Specification)이에요.
스펙은 다양한 형태를 가질 수 있습니다. 요구사항 문서일 수도 있고 사용자 스토리와 수용 기준(Acceptance Criteria)일 수도 있죠. 기술 설계 문서가 되기도 합니다. 하지만 개발자에게는 더 정밀한 스펙 도구가 있습니다. 바로 테스트 코드와 타입 정의입니다.
테스트 코드를 먼저 작성하면 “이 함수는 이런 입력을 받으면 이런 출력을 내야 한다”는 걸 코드로 정의할 수 있습니다. 타입을 먼저 정의하면 “이 모듈은 이런 구조의 데이터를 다루고, 이런 인터페이스를 노출해야 한다”는 걸 표현할 수 있죠.
앞의 할인 계산 예제를 테스트로 바꿔보면 이렇게 됩니다.
import { describe, expect, test } from "vitest";
import { calculateDiscount } from "./discount";
describe("calculateDiscount", () => {
test("쿠폰 할인을 먼저 적용한 뒤 등급 할인을 적용한다", () => {
const result = calculateDiscount({
price: 10000,
couponRate: 0.1, // 10%
gradeRate: 0.05, // 5%
});
// 10000 * 0.9 = 9000 → 9000 * 0.95 = 8550
expect(result).toBe(8550);
});
test("최종 할인율이 50%를 넘으면 50%로 제한한다", () => {
const result = calculateDiscount({
price: 10000,
couponRate: 0.4,
gradeRate: 0.3,
});
// 제한 없이 적용하면 4200원이지만 50% 제한으로 5000원
expect(result).toBe(5000);
});
test("가격이 0원이면 할인 없이 0원을 반환한다", () => {
const result = calculateDiscount({
price: 0,
couponRate: 0.1,
gradeRate: 0.05,
});
expect(result).toBe(0);
});
test("할인율이 음수면 에러를 던진다", () => {
expect(() =>
calculateDiscount({
price: 10000,
couponRate: -0.1,
gradeRate: 0.05,
}),
).toThrow("할인율은 0 이상이어야 합니다");
});
});
이 테스트 코드가 사실상 명세입니다. 에이전트에게 “이 테스트를 통과하는 calculateDiscount 함수를 구현해줘”라고 말하면, 자연어 프롬프트와는 비교할 수 없을 정도로 명확한 결과를 얻게 됩니다.
테스트만으로는 부족한 경우도 있습니다. 함수의 입출력은 테스트로 정의할 수 있지만, 모듈 간의 관계나 데이터 흐름은 타입 정의가 더 적합하거든요.
interface CartItem {
productId: string;
name: string;
price: number;
quantity: number;
}
interface Cart {
items: CartItem[];
appliedCoupon: Coupon | null;
}
interface CartService {
addItem(cart: Cart, productId: string, quantity: number): Cart;
removeItem(cart: Cart, productId: string): Cart;
updateQuantity(cart: Cart, productId: string, quantity: number): Cart;
applyCoupon(cart: Cart, coupon: Coupon): Cart;
getTotal(cart: Cart): number;
}
이 타입 정의만 봐도 장바구니 모듈이 어떤 데이터를 다루고 어떤 연산을 지원해야 하는지 한눈에 파악됩니다. 에이전트에게 이 타입과 테스트를 함께 건네주면 “장바구니 기능 만들어줘”라는 프롬프트와는 차원이 다른 수준의 정밀한 구현을 얻을 수 있어요.
업계는 이미 움직이고 있다
스펙 주도 개발은 개인의 작업 습관을 넘어 업계 전체의 흐름이 되고 있습니다.
AWS가 만든 IDE Kiro는 바이브 코딩의 대안을 표방하며 스펙 주도 개발을 전면에 내세웠습니다. Kiro에서는 코드를 짜기 전에 요구사항(Requirements), 기술 설계(Design), 작업 목록(Tasks)을 순서대로 정의합니다. 에이전트는 이 스펙을 기반으로 작은 단위의 작업을 하나씩 처리하고, 개발자는 거대한 코드 덩어리 대신 집중된 변경 사항을 검토하죠.
GitHub도 비슷한 방향으로 Spec Kit이라는 오픈소스 툴킷을 내놨습니다. Spec Kit은 네 단계로 구성됩니다. 먼저 사용자 여정과 성공 기준을 정의하는 Specify, 기술 스택과 아키텍처 제약을 명시하는 Plan, 작업을 작고 검증 가능한 단위로 쪼개는 Tasks, 그리고 에이전트가 순차적으로 구현하는 Implement까지. 스펙이 “사람과 AI가 공유하는 단일 진실 공급원(Single Source of Truth)“이 되는 거예요.
두 도구 모두 같은 문제의식에서 출발합니다. 자연어 프롬프트만으로는 에이전트의 결과물을 통제할 수 없으니, 명확한 스펙을 먼저 정의하자는 것이죠.
냉정한 현실 점검
그런데 스펙 주도 개발이 만능은 아닙니다. Martin Fowler 블로그에서 Birgitta Böckeler가 Kiro, Spec Kit, Tessl 세 가지 도구를 직접 실험한 분석 글은 몇 가지 냉정한 현실을 보여줍니다.
우선 문제 크기와 도구가 안 맞는 경우가 많았습니다. 작은 버그 하나 고치는 데 Kiro가 사용자 스토리 4개에 수용 기준 16개를 생성한 거예요. 배보다 배꼽이 더 큰 상황이죠. Spec Kit도 마찬가지로 반복적인 마크다운 파일을 잔뜩 만들어내서 코드를 직접 리뷰하는 것보다 스펙 문서를 리뷰하는 게 더 힘든 상황이 벌어졌습니다.
에이전트가 스펙을 무시하는 문제도 있었습니다. 아무리 상세한 스펙과 체크리스트를 줘도 에이전트가 지시를 건너뛰거나 요청하지 않은 기능을 추가했어요. 기존 코드를 잘못 해석해서 중복 구현하는 일도 반복됐고요.
Böckeler는 특히 과거에 실패한 모델 주도 개발(MDD)과의 유사성을 경고합니다. 스펙만 쓰면 코드가 자동 생성된다는 비전은 매력적이지만, 현실에서는 스펙의 경직성과 LLM의 비결정성이라는 양쪽의 단점을 동시에 떠안게 될 수 있다는 거죠.
그렇다고 스펙 주도 개발 자체가 틀린 건 아닙니다. Böckeler도 인정하듯 “스펙을 먼저 작성한다”는 원칙 자체는 확실히 가치가 있습니다. 문제는 스펙의 형태와 수준이에요. 모든 작업에 무거운 요구사항 문서를 만드는 건 과하지만 핵심 로직에 테스트와 타입을 먼저 작성하는 건 여전히 강력합니다.
실전 워크플로우
그러면 스펙 주도 개발을 실제 작업에 어떻게 적용할까요? 여기서는 클로드 코드를 사용하는 예시로 설명하지만, 다른 코딩 에이전트에서도 동일한 원리가 적용됩니다.
1단계: 타입으로 구조를 잡는다
구현에 앞서 데이터 모델과 인터페이스를 먼저 정의합니다. 함수가 받는 매개변수와 반환값의 형태, 모듈 간의 의존 관계를 타입으로 표현하죠.
interface DiscountInput {
price: number;
couponRate: number;
gradeRate: number;
}
type CalculateDiscount = (input: DiscountInput) => number;
이 단계에서는 구현 코드를 한 줄도 작성하지 않습니다. 오직 “무엇을” 만들지에만 집중해요.
2단계: 테스트로 기대 동작을 정의한다
타입이 “구조”를 잡아준다면, 테스트는 “동작”을 정의합니다. 정상 케이스뿐 아니라 경계값, 에러 상황까지 테스트로 작성합니다. 앞에서 본 discount.test.ts가 바로 이 단계의 산출물이죠.
테스트를 작성할 때 핵심은 구현 방법이 아니라 결과에 집중하는 것입니다. “내부적으로 어떤 알고리즘을 쓸지”가 아니라 “이 입력에 대해 이 출력이 나와야 한다”를 정의하세요. 이러면 에이전트가 어떤 구현 전략을 선택하든 테스트만 통과하면 됩니다.
3단계: 에이전트에게 구현을 맡긴다
타입과 테스트가 준비되면 에이전트에게 구현을 요청합니다.
discount.test.ts의 모든 테스트를 통과하도록 discount.ts를 구현해줘.
types.ts에 정의된 타입을 사용해야 해.
에이전트는 테스트를 실행하면서 구현을 진행합니다. 테스트가 하나씩 통과할 때마다 구현이 명세에 가까워지는 거죠. 자연어 프롬프트와의 결정적인 차이가 여기서 드러납니다. 프롬프트는 에이전트가 “이해했는지” 확인할 길이 없지만, 테스트는 통과 여부로 바로 알 수 있으니까요.
4단계: 결과를 검토하고 스펙을 보강한다
에이전트가 모든 테스트를 통과하는 코드를 만들었다면, 코드를 리뷰합니다. 이때 중요한 건 “맞게 동작하는가”가 아니라(그건 테스트가 이미 검증했으니까) “놓친 시나리오는 없는가”입니다.
리뷰하다가 빠진 케이스가 보이면 테스트를 추가하고 다시 에이전트에게 넘깁니다. 이 사이클을 반복하면 스펙이 점점 촘촘해지고, 구현도 그에 맞춰 견고해집니다.
CLAUDE.md로 TDD 습관 만들기
이 워크플로우를 매번 프롬프트로 설명하는 건 번거롭습니다. 대신 CLAUDE.md에 TDD 규칙을 직접 넣어두면 에이전트가 세션 시작부터 스펙 주도 방식으로 작업합니다.
## 개발 워크플로우
새 기능을 구현할 때는 반드시 다음 순서를 따른다:
1. 타입과 인터페이스를 먼저 정의한다
2. 스텁(stub) 함수를 만들어 컴파일이 되는 상태를 확보한다
3. 실패하는 테스트를 작성한다 (Red)
4. 테스트를 통과하는 최소한의 구현을 작성한다 (Green)
5. 코드를 정리한다 (Refactor)
6. 모든 테스트가 통과하는지 확인한 후 커밋한다
기존 함수를 수정할 때는 먼저 해당 함수의 테스트가 있는지 확인하고,
없으면 현재 동작을 검증하는 테스트를 먼저 작성한다.
Red-Green-Refactor는 TDD의 고전적인 사이클이죠. 에이전트에게 이 사이클을 강제하면 “일단 전부 구현하고 나중에 테스트”가 아니라 “테스트 하나 통과시키고 다음으로 넘어가는” 점진적 방식으로 작업하게 됩니다. 한 번에 거대한 코드 덩어리를 생성하는 것보다 이렇게 작은 단위로 쌓아가는 게 훨씬 예측 가능해요.
결국 좋은 스펙은 에이전트에게 “뭘 만들지”를 알려주는 것만이 아니라 스스로 교정할 수 있는 가드레일을 제공합니다. 테스트가 실패하면 에이전트가 알아서 뭐가 잘못됐는지 파악하고 고칠 수 있으니까요.
어디에 스펙을 집중할까
Böckeler의 분석에서 배울 수 있는 가장 중요한 교훈은 이겁니다. 모든 작업에 같은 수준의 스펙을 요구하면 안 된다.
스펙을 먼저 작성하면 좋은 영역은 뚜렷합니다. 비즈니스 로직처럼 입출력이 명확한 순수 함수가 대표적이고, 데이터 변환이나 유효성 검증 로직도 잘 맞습니다. API 엔드포인트의 요청/응답 스키마나 리듀서 같은 상태 관리 로직도 마찬가지예요.
반면 UI 레이아웃이나 스타일링, 초기 프로토타이핑, 인프라 설정(Docker, CI/CD 파이프라인) 같은 작업은 자연어 프롬프트가 더 효율적입니다. 탐색적인 성격의 작업은 스펙을 미리 정의하기 어려우니까요. 이런 영역에서까지 스펙 문서를 만들면 Böckeler가 경험한 것처럼 배보다 배꼽이 더 커지게 됩니다.
결국 핵심은 “틀리면 비용이 큰 부분”에 스펙을 집중하는 것입니다. 할인 계산이 틀리면 매출에 직접 영향이 가지만, 버튼 색상이 조금 다른 건 금방 고칠 수 있잖아요.
에이전트 하네스와의 시너지
스펙 주도 개발은 혼자 작동하는 것보다 하네스 엔지니어링과 결합될 때 진가를 발휘합니다.
하네스 엔지니어링에서 말하는 피드백 루프(린터, 타입 체커, 테스트 스위트)가 에이전트의 작업 품질을 실시간으로 검증합니다. 스펙 주도 개발에서 작성한 테스트가 바로 이 피드백 루프의 중심축이 되는 거예요.
에이전트가 코드를 생성하면 타입 체커가 구조를 잡아주고 테스트가 동작을 확인합니다. 린터가 코드 스타일까지 맞춰주고요. 이 세 겹의 안전망 덕분에 에이전트가 빗나가면 바로 잡힙니다.
CLAUDE.md나 AGENTS.md에는 TDD 워크플로우뿐 아니라 에이전트의 행동 경계도 명시할 수 있습니다. 이걸 세 단계로 나눠서 관리하면 효과적이에요. “반드시 해야 할 것”(테스트 실행, 타입 체크), “먼저 물어볼 것”(데이터베이스 스키마 변경, 새 의존성 추가), “절대 하면 안 되는 것”(시크릿 커밋, vendor 디렉토리 수정)으로요. 테스트와 타입이 “무엇을 만들지”를 정의한다면, 이런 경계 규칙은 “어떻게 만들지”의 안전장치가 되는 셈이죠.
CI 파이프라인에 타입 체크와 테스트를 필수 게이트로 걸어두는 것도 빠뜨릴 수 없습니다. 에이전트가 만든 코드라도 스펙을 통과하지 못하면 머지 자체가 안 되게 만드는 거죠. 스펙 자체를 코드 리뷰의 대상으로 삼는 것도 잊지 마세요. 테스트 코드의 품질이 곧 에이전트 산출물의 품질을 결정하니까요. 테스트를 생략하면 팀 전체의 시간이 낭비된다는 건 이전에도 그랬지만, AI 시대에는 그 영향이 더 커졌습니다.
마치며
한 문장으로 정리하면 이겁니다. 에이전트에게 “뭘 만들어줘”라고 말하지 말고, “이걸 통과시켜줘”라고 말하세요. 테스트와 타입은 자연어보다 훨씬 정밀한 의사소통 도구이면서, 동시에 실행 가능한 검증 수단이기도 하니까요.
다만 Böckeler의 분석이 보여주듯, 스펙의 수준을 작업에 맞게 조절하는 감각이 필요합니다. 모든 걸 무거운 문서로 만들 필요는 없어요. 핵심 로직에는 테스트를 먼저 작성하고 복잡한 모듈에는 타입을 먼저 설계하면 됩니다. 단순한 작업에는 프롬프트 하나로 충분하고요.
오늘 에이전트에게 작업을 맡기기 전에 테스트부터 한 번 작성해보세요. 프롬프트를 아무리 다듬는 것보다 실패하는 테스트 하나가 더 강력한 지시가 됩니다.
This work is licensed under
CC BY 4.0