본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 05. 26. 06:30

IDE 없이 진행하는 명세 기반 개발 (Spec-Driven Development): 단 하나의 PRD 파일로 NestJS, Go

요약

사용자의 요구사항 명세(PRD)를 바탕으로 코드를 자동 생성하는 '명세 기반 개발(Spec-Driven Development)' 방법론과 이를 구현한 오픈 소스 CLI 도구 세트를 소개합니다. IDE나 계정 없이 텍스트 파일 하나로 NestJS, Go, Laravel 등 다양한 스택의 애플리케이션을 구축할 수 있습니다.

핵심 포인트

  • 명세(Spec)를 신뢰할 수 있는 단일 원천으로 활용하는 개발 방식
  • IDE나 네트워크 연결 없이 작동하는 오픈 소스 CLI 도구 제공
  • PRD 파일 하나로 CRUD, 인증, DB 설정이 포함된 앱 생성 가능
  • 모델 주도 아키텍처(MDA) 개념을 현대적 AI 코딩 도구에 적용

Amazon은 2025년에 대기 명단과 함께 명확한 가설을 가지고 Kiro를 출시했습니다. AI 코딩 도구의 문제는 코드를 잘못 작성하는 것이 아니라, 사용자의 요구사항과 연결되지 않은 코드를 작성한다는 것입니다. Kiro의 해답은 명세 기반 개발 (Spec-Driven Development)입니다. 즉, 사용자가 명세 (Spec)를 작성하면 도구가 이를 바탕으로 코드를 생성하며, 명세가 권위 있는 기준 (Authoritative)으로 남는 방식입니다.

Kiro는 좋은 아이디어입니다. 하지만 이는 설치해야 하고, 로그인해야 하며, 여러분의 코드베이스를 믿고 맡겨야 하는 독점적인 IDE (Proprietary IDE)이기도 합니다.

저는 동일한 개념을 오픈 소스 CLI 도구 세트로 구축했습니다. 각 생태계별로 하나씩 구성되어 있으며, 여러분의 팀이 이미 사용 중인 레지스트리 (Registry)에 배포됩니다. IDE는 필요 없습니다. 계정도 필요 없습니다. 네트워크 호출도 없습니다. 텍스트 파일 하나를 넣으면, 작동하는 애플리케이션이 나옵니다.

제가 무엇을 만들었는지, 어떻게 작동하는지, 그리고 왜 아키텍처 (Architecture)가 코드보다 중요한지에 대해 설명하겠습니다.

지금 바로 시도해 보세요 — 스택을 선택하세요

NestJS / TypeScript

npm install -g archiet-microcodegen-nestjs
archiet-microcodegen-nestjs --sample > prd.md
archiet-microcodegen-nestjs prd.md --out ./my-app
cd my-app && npm install && docker compose up

Go Chi

go install github.com/aniekanasuquookono-web/archiet-microcodegen-go@latest
archiet-microcodegen-go prd.md --out ./my-app
cd my-app && make run

Laravel (PHP)

composer global require archiet/microcodegen-laravel
archiet-microcodegen-laravel prd.md --out ./my-app
cd my-app && composer install && docker compose up

Spring Boot (Java)

java -jar archiet-microcodegen-java.jar prd.md --out ./my-app
cd my-app && mvn spring-boot:run

Tauri (Rust + desktop)

cargo install archiet-microcodegen-tauri
archiet-microcodegen-tauri prd.md --out ./my-app
cd my-app && npm install && npm run tauri dev

이 글을 다 읽기도 전에, 여러분의 앱은 완전한 CRUD, httpOnly 쿠키를 사용한 JWT 인증, Postgres 16, 그리고 사용자별 데이터 격리 (Data Isolation) 기능을 갖추게 됩니다.

명세 기반 개발 (Spec-driven development)이 실제로 의미하는 것

명세 기반 개발 (Spec-driven development)은 모델 주도 아키텍처 (Model-driven architecture, MDA)에서 유래한 25년 된 아이디어입니다. 즉, 시스템의 공식 모델 (Formal model)을 작성한 다음, 그 모델로부터 구현 (Implementation)을 도출하는 방식입니다. 모델이 신뢰할 수 있는 단일 원천 (Source of truth)이며, 코드는 모델로부터 생성된 산출물 (Artefact)입니다.

이 방식이 주류가 되지 못했던 이유는 공식 모델을 작성하기 위해 UML 도구와 엔터프라이즈 아키텍트 (Enterprise architect)가 필요했기 때문입니다. Kiro의 통찰(그리고 저의 통찰)은 일반 텍스트로 된 요구사항 파일 자체가 공식 모델이라는 점입니다. 단지 이를 진지하게 받아들일 파서 (Parser)가 필요할 뿐입니다.

파이프라인은 네 단계로 구성됩니다. 저는 모든 언어에 대해 이 네 단계를 모두 구현했으며, 이것이 각 생성기 (Generator)가 사용자가 채워 넣어야 할 빈칸이 있는 템플릿이 아니라, 진정으로 정확한 결과물을 생성하는 이유입니다.

네 단계

1단계 — parse_prd(text) → Manifest (매니페스트)

텍스트 파일을 읽습니다. LLM이 아닌 정규 표현식 (Regex)을 사용하여 모든 엔티티 정의 (예: Task, Project, User), 타입이 포함된 필드 이름, 사용자 스토리 (User stories), 그리고 통합 참조 (Integration references)를 추출합니다. 출력값은 언어에 종속되지 않는 매니페스트 (Manifest)입니다.

PRD 스니펫 예시:

"시스템은 Project와 Task를 관리합니다. Project는 name, description, status를 가집니다.

Task는 title, body, due_date를 가지며 Project에 속합니다."

매니페스트 출력:

Entities: [Project, Task]
Fields:
Project: name (string, required), description (text), status (string)
Task: title (string, required), body (text), due_date (string), project_id (FK→Project)

2단계 — manifest_to_genome(manifest) → Genome (게놈)

매니페스트를 아키텍처 게놈 (Architectural Genome)으로 변환합니다. 이는 ArchiMate 3.2 요소 카테고리인 ApplicationComponent, ApplicationService, DataObject, ApplicationInterface를 사용하는 타입이 지정된 중간 표현 (Intermediate representation)입니다.

모든 엔티티는 자동으로 id, user_id (테넌트별 격리를 위함), 그리고 created_at을 부여받습니다. 관계는 명시적으로 만들어집니다. 게놈은 여전히 언어에 종속되지 않으며, NestJS 출력, Go 출력, 그리고 Laravel 출력을 동일하게 구동합니다.

{
"solution_name": "TaskManager",
"entities": [
{
"name": "Task",
"archimate_type": "DataObject",
"fields": {
"id": { "type": "integer", "required": true },
"user_id": { "type": "integer", "required": true },
"title": { "type": "string", "required": true },
"due_date": { "type": "string", "required": false },
"created_at": { "type": "datetime", "required": true }}
},
"relationships": [
{ "type": "association", "target": "Project", "cardinality": "many-to-one" }
]
}
]
}

이 부분이 템플릿 시스템과 차별화되는 지점입니다. Genome은 기계가 읽을 수 있는 형태의 아키텍처 문서입니다. Stage 3과 Stage 4는 순수한 렌더링 과정이며, 시스템이 무엇인지에 대한 결정을 절대 내리지 않습니다.

Stage 3 — render_genome(genome) → {path: content}

언어별 스테이지입니다. NestJS 렌더러는 TypeORM을 사용한 TypeScript를 생성합니다. Go 렌더러는 GORM을 사용한 관용적인 Chi 핸들러를 생성합니다. Laravel 렌더러는 Eloquent 모델, Form Requests가 포함된 컨트롤러, 그리고 Blade-free API 리소스를 생성합니다. Rust 렌더러는 rusqlite를 사용한 Tauri IPC 명령을 생성합니다.

Stage 1과 Stage 2는 모든 열 개의 패키지에서 공유됩니다. 오직 Stage 3만 다릅니다. 이것이 바로 일주일 만에 열 개의 에코시스템을 배포할 수 있었던 이유입니다. 아키텍처적 사고가 열 번이 아닌 한 번 이루어졌기 때문입니다.

Stage 4 — pack(files) → ZIP 또는 디렉토리

디스크에 쓰거나 ZIP으로 묶습니다. 모든 구현에서 순수 stdlib를 사용하며, 외부 zip 라이브러리는 필요하지 않습니다.

생성된 NestJS 앱 (NestJS 개발자를 위한)

task-manager PRD를 생성기에 실행하면 다음 결과가 나옵니다:

src/
auth/
auth.module.ts
auth.controller.ts ← /auth/register, /auth/login, /auth/me, /auth/logout
jwt.strategy.ts ← httpOnly 쿠키를 읽고 Authorization 헤더는 사용하지 않음
jwt-auth.guard.ts
task/
task.controller.ts ← GET /tasks, POST /tasks, GET /tasks/:id, PUT, DELETE
task.service.ts ← 모든 메서드가 userId로 필터링함
task.entity.ts ← TypeORM @entity, @column, @PrimaryGeneratedColumn
dto/
create-task.dto.ts ← class-validator 데코레이터를 사용하여 실패 시 422를 올바르게 처리함
update-task.dto.ts
docker-compose.yml ← Postgres 16, healthcheck 기반 시작
ARCHITECTURE.md ← ArchiMate 3.2 요소 인벤토리
openapi.yaml ← 기계가 읽을 수 있는 API 계약
test/
task.controller.spec.ts ← 컨트롤러별 happy-path Jest 테스트

생성기가 강제하는 세 가지 보안 속성 (AI 어시스턴트들이 흔히 틀리는 부분):

  1. httpOnly 쿠키 사용, localStorage 아님. JwtStrategy는 req.cookies['access_token']에서 읽어옴. AuthController는 httpOnly: true, secure: true, sameSite: 'strict'로 설정함. localStorage를 사용하는 것은 XSS 취약점임. 생성기는 이를 옵션으로 제공하지 않음.
  2. 모든 쿼리에서의 사용자별 격리(Per-user isolation). taskService.findAll(userId)는 WHERE user_id = $1을 실행함. 모든 서비스 메서드는 가드로부터 인증된 사용자의 ID를 받음. 다른 사용자의 데이터를 반환하는 코드 경로는 없음.
  3. 정확한 HTTP 상태 코드. 생성되는 것은 문서가 아니라 실제 코드임. 생성 시 201, 유효성 검사 실패 시 422, 인증 실패 시 403, 찾을 수 없을 때 404를 사용함.

생성된 Laravel 앱 (PHP/Laravel 개발자를 위한)

Laravel은 생태계가 크고 관습적(opinionated)이기 때문에 가장 많이 검색되는 스캐폴딩 대상 중 하나임. 생성기는 다음 결과물을 산출함:

app/
Models/
Task.php ← Eloquent 모델 (model), $fillable, $casts, userId 스코프 (scope)
Http/
Controllers/
TaskController.php ← 전체 CRUD 리소스 컨트롤러 (resource controller)
Requests/
StoreTaskRequest.php ← FormRequest 검증 (validation), 실패 시 422 반환
Resources/
TaskResource.php ← API 리소스 (resource), 내부 필드 숨김
routes/
api.php ← Route::apiResource + 인증 미들웨어 (auth middleware)
database/
migrations/
create_tasks_table.php ← user_id 외래 키 (FK) 인덱스 포함
docker-compose.yml
ARCHITECTURE.md
openapi.yaml

Task 모델에는 인증된 사용자에 따라 자동으로 필터링하는 글로벌 스코프 (global scope)가 포함되어 있습니다. 이는 Laravel의 관용구 (idioms)로 표현된, 동일한 테넌트 격리 (per-tenant isolation) 원칙입니다.

생성된 Go 앱 (Go 개발자용)

Go 개발자들은 마법 같은 동작 (magic)을 싫어합니다. 생성된 앱은 net/http (라우팅을 위해 Chi 사용), GORM, 그리고 golang-jwt/jwt를 사용합니다. 리플렉션 (reflection)이 과도한 프레임워크는 사용하지 않습니다.

func (h *TaskHandler) ListTasks(w http.ResponseWriter, r *http.Request) {
userID := r.Context().Value(contextKeyUserID).(int64)
tasks, err := h.service.FindAllByUser(r.Context(), userID)
// ...
}

func (s *TaskService) FindAllByUser(ctx context.Context, userID int64) ([]Task, error) {
var tasks []Task
result := s.db.WithContext(ctx).Where("user_id = ?", userID).Find(&tasks)
return tasks, result.Error
}

생성된 Makefile에는 make build, make test, make migrate가 포함됩니다. 마법은 전혀 없습니다.

Tauri 생성기는 의도적으로 다릅니다

데스크톱 앱은 제약 사항이 다릅니다. 생성된 Tauri 앱은 Postgres 대신 SQLite를 사용합니다. 인증 (Auth)은 애플리케이션 메모리 내에서 Argon2id + UUID 세션 토큰을 사용합니다. Tauri에는 HTTP 계층이 없으며, 오직 IPC 명령 (commands)만 존재합니다.

#[tauri::command]
async fn list_tasks(state: State<'_, AppState>, session: String) -> Result<Vec<Task>, String> {
let user_id = state.sessions.lock().unwrap()
.get(&session).copied().ok_or("Unauthorised")?;
// 모든 쿼리는 user_id로 필터링합니다 — 동일한 원칙, 다른 관용구 (idiom)
}

이것이 Amazon Kiro와 비교했을 때 어떤 차이가 있는지

Kiro와 이러한 생성기(generators)들은 동일한 핵심 통찰을 공유합니다: 명세 우선(spec-first) 방식이 프롬프트와 기도(prompt-and-pray) 방식보다 더 나은 소프트웨어를 만들어낸다는 점입니다. 차이점은 실무적인 부분에 있습니다:

Amazon Kiroarchiet-microcodegen
전달 방식 (Delivery)독점적 IDE 플러그인사용자의 레지스트리 상 CLI (npm / go / cargo / composer)
네트워크 (Network)AWS 계정 필요완전 오프라인
명세 형식 (Spec format)Kiro의 requirements.md + design.md모든 일반 텍스트 PRD 또는 Markdown 파일
출력물 (Output)IDE 내부의 코드 제안즉시 실행 가능한 완전한 애플리케이션
스택 (Stacks)IDE에 구애받지 않는 편집NestJS, Go, Laravel, Spring Boot, Tauri, FastAPI, Flask, Django, Rails, .NET
모델 (Model)LLM 기반결정론적(Deterministic) 4단계 파이프라인 — LLM 미사용
소스 (Source)폐쇄형 (Closed)오픈 소스 (Open source)

이들은 서로 다른 도구입니다. 만약 이미 Kiro를 사용하고 있다면, Kiro가 확장을 도와주기 전에 초기 프로젝트를 부트스트랩(bootstrap)하는 용도로 이 생성기들을 사용할 수 있습니다.

왜 순수 표준 라이브러리(stdlib)인가 — Karpathy의 제약 조건

각 생성기는 외부 의존성(external dependencies)이 전혀 없는 1,400줄 미만의 단일 파일로 구성됩니다. Go 생성기는 archive/zip, encoding/json, os만을 사용합니다. Node.js 생성기는 fs, path, crypto, zlib만을 사용합니다. Rust 생성기는 오직 std만을 사용합니다.

이는 Andrej Karpathy의 micrograd 철학에서 기인합니다: 만약 의존성이 없는 작은 파일 안에 전체 알고리즘을 표현할 수 없다면, 당신은 그것을 완전히 이해하지 못한 것입니다. 모든 생성기는 단 한 번의 읽기로 감사(auditable)가 가능합니다. 전이적 의존성(transitive dependencies), 공급망 리스크(supply-chain risks), 버전 충돌이 없습니다.

또한 이는 의존성의 API가 변경되어도 생성기가 절대 깨지지 않음을 의미합니다.

코드베이스에서 살아남는 아키텍처 문서

생성된 모든 프로젝트에는 타입이 지정된 ArchiMate 3.2 인벤토리가 포함된 ARCHITECTURE.md가 포함됩니다:

ApplicationComponent: TaskManagerAPI

  • Realises: TaskManagementService

DataObject: Task

  • Fields: id, user_id, title, body, due_date, status, created_at
  • Association: Task → Project (many-to-one)

6개월 후, 새로운 엔지니어가 ARCHITECTURE.md를 읽고 구현 내용을 해독할 필요 없이 시스템을 이해하게 됩니다.

배포 (Distribution): 왜 10개의 레지스트리가 중요한가

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0