Image: AI generated
AI가 코드를 덮어쓰는 문제를 겪고 있다면, 바이브 코딩이 200 엔드포인트에서 무너졌다면, AI의 작업 대상을 코드가 아닌 명세로 바꾸고 싶다면 — yongol이 그 용골이다.
200번째 엔드포인트
바이브 코딩으로 SaaS를 만든다. 처음에는 빠르다. 5개 테이블, 12개 엔드포인트 — 20분이면 돌아간다.
그런데 50개 엔드포인트를 넘기면 이상한 일이 생긴다. AI가 어제 만든 패턴을 오늘 다르게 만든다. 100개를 넘기면 기존 기능이 조용히 깨진다. 200개를 넘기면 새 기능 하나 추가하는 데 처음 10개를 만들 때보다 10배가 걸린다.
DORA 2025 보고서가 이것을 실증했다 — AI 도구는 처리량을 2-18% 높이지만, 변경 실패율과 재작업을 동시에 증가시킨다[1]. AI는 기존 프로세스의 약점을 증폭하는 “거울이자 승수"라는 것이다.
모델이 멍청해서가 아니다.
결정과 구현
소스 코드에는 세 가지가 섞여 있다:
- 사용자 결정 — 이 컬럼은
BIGINT이다. 이 엔드포인트는 소유자만 접근한다. 페이지네이션은 커서 방식이다. - 비즈니스 로직 — 가격 정책, 워크플로우, 생명주기 규칙.
- 구현 세부사항 — 변수명, 라이브러리 호출 순서, 에러 래핑.
AI가 이 코드를 읽을 때, 어떤 줄이 결정이고 어떤 줄이 세부사항인지 구별하지 못한다. 그래서 “리팩토링"이나 “정리"를 할 때, 결정을 세부사항으로 착각하고 조용히 덮어쓴다. 사용자는 동작이 이미 틀어진 뒤에야 알아챈다. 1972년 Parnas가 “변경될 가능성이 높은 설계 결정을 인터페이스 뒤에 숨겨라”[2]고 했을 때, 그는 사람을 위해 말한 것이었다. 그런데 AI가 코드를 편집하는 지금, 결정과 세부사항의 구분이 매체 자체에 존재하지 않으면 아무도 — 사람이든 모델이든 — 그 구분을 지킬 수 없다.
이것이 200 엔드포인트에서 바이브 코딩이 무너지는 이유다. 더 큰 모델을 써도 해결되지 않는다. 매체(raw code) 자체가 결정을 보존하지 못하기 때문이다. 모든 모델이 결국 같은 벽에 부딪힌다.
용골
배를 만들 때 가장 먼저 놓는 뼈대가 용골이다. 선체의 무게를 지탱하고, 좌우 흔들림을 막고, 나머지 모든 구조물이 용골 위에 올라간다. 용골 없이 만든 배는 잔잔한 바다에서는 뜨지만, 파도가 치면 뒤틀린다.
바이브 코딩으로 만든 SaaS가 그렇다. 작을 때는 뜬다. 커지면 뒤틀린다.
yongol은 AI 코딩 SaaS의 용골이다.
Harness with reins — 더 큰 모델이 아니라, 더 정밀한 고삐. 결정론적 validator가 모든 산출물을 판정하고, 래칫이 진행을 강제하고, 완료 여부를 기계가 결정한다.
결정을 코드 밖으로
yongol의 핵심은 단순하다. 결정을 코드에서 분리한다.
10개의 선언적 명세(SSOT)가 각각 하나의 관심사만 담당한다:
| SSOT | 담당 |
|---|---|
| features.yaml | 기능 카탈로그 — 무엇을 만들 것인가 |
| manifest.yaml | 프로젝트 설정 — 인증, 미들웨어, 인프라 |
| OpenAPI | API 계약 — 경로, 파라미터, 응답 |
| SQL DDL + sqlc | 데이터 모델 — 테이블, 컬럼, 제약, 쿼리 |
| SSaC | 서비스 흐름 — 엔드포인트 내부의 결정 순서 |
| Rego | 인가 정책 — 누가 무엇을 할 수 있는가 |
| Mermaid stateDiagram | 상태 전이 — 엔티티의 생명주기 |
| FuncSpec | 커스텀 함수 — CRUD로 표현 안 되는 로직 |
| Hurl | 테스트 시나리오 — smoke, scenario, invariant 3분류 |
| STML | 프론트엔드 — Semantic Template Markup Language (data-* 속성 기반 HTML) |
10종 중 8종은 업계 표준(OpenAPI, SQL, sqlc, Rego, Mermaid, Hurl, YAML)이다. SSaC와 STML만 yongol이 만든 DSL이다. AI가 새로 배워야 하는 것을 최소화한다.
각 SSOT에는 결정만 들어간다. 구현 세부사항은 없다. AI는 SSOT를 편집하고, yongol generate가 SSOT에서 코드를 렌더링한다. 결정은 SSOT에 영구히 살고, 코드는 일회용 투영이다.
정합성을 강제한다
결정을 10개 파일에 분산했으니, 파일 간 모순이 생길 수 있다. DDL에는 BIGINT인데 OpenAPI에는 string이라면? SSaC에서 @auth를 선언했는데 Rego에 해당 규칙이 없다면? 상태 다이어그램에는 전이가 있는데 SSaC에 해당 함수가 없다면?
모순된 SSOT는 오염된 결정이다. 코드가 아무리 깔끔해도 결정이 어긋나면 동작이 틀어진다.
yongol validate가 이것을 잡는다.
✓ manifest ✓ openapi_ddl ✓ ssac_rego
✓ openapi ✓ openapi_ssac ✓ ssac_authz
✓ ddl ✓ hurl_openapi ✓ ssac_sqlc
✓ query ✓ hurl_statemachine ✓ ddl_statemachine
✓ ssac ✓ hurl_manifest ✓ ddl_rego
✓ statemachine ✓ openapi_manifest ✓ rego_manifest
✓ rego ✓ ssac_ddl ✓ stml_openapi
✓ hurl ✓ ssac_statemachine
✓ funcspec ✓ ssac_func
0 errors, 0 warnings
먼저 각 SSOT를 개별 검증하고, 그 다음 레이어 간 교차 검증을 실행한다. ~287개의 규칙이 10개 SSOT 사이의 모든 심볼 참조를 검사한다. 모순이 하나라도 있으면 컴파일을 거부한다. Torres 등의 체계적 문헌 리뷰[3]는 대부분의 모델 관리 도구가 단일 모델 내부 정합성만 다루고 이종 모델 간 교차 검증은 미해결 과제로 남아 있다고 지적했다 — yongol validate가 채우는 빈자리가 정확히 그것이다.
AI는 자유롭게 쓴다. 레일을 벗어나면 validate가 즉시 잡는다. 레일 위의 자유.
yongol next — 래칫 명령어
yongol validate가 모든 에러를 한 번에 보여준다면, yongol next는 에러를 하나씩 보여준다. 이것이 래칫이다.
$ yongol next specs/
[ERROR] DDL-003: users.id must be BIGINT, got INT
file: specs/db/users.sql:2
▶ Fix this error. Then run `yongol next specs/`.
AI 에이전트에게 필요한 지시는 한 문장이다: “yongol next specs/를 실행하고, 에러가 0이 될 때까지 수정해.”
에러를 고치면 다음 에러가 나온다. 전부 통과하면 멈춘다:
$ yongol next specs/
✓ All validations passed. 0 errors.
에이전트가 “다 했습니다"라고 선언하는 것이 아니라, 기계가 “아직 남았다” 또는 “전부 통과"를 판정한다. 종료 판단권이 에이전트에게 없다.
프로젝트 생성과 기능 관리
yongol init
features.yaml에서 SSOT 스캐폴딩을 자동 생성한다.
yongol init Myapp features.yaml "My workflow automation SaaS"
cd Myapp && yongol validate specs # 0 errors
manifest, OpenAPI operationId 스텁, SSaC 스텁 파일, Rego 인가 규칙, Hurl 스모크 테스트, sqlc 설정이 한 번에 생성된다. 프로젝트가 즉시 yongol validate 통과 상태로 시작한다.
yongol features add / remove
기능을 추가하거나 제거한다:
yongol features add new_features.yaml # 새 operationId의 SSaC 스텁 생성
yongol features remove ExportWorkflow --yes # operationId 삭제 + SSaC 스텁 삭제
yongol import
외부 OpenAPI(Stripe, GitHub 등)에서 Go 클라이언트 패키지를 생성한다:
yongol import https://api.stripe.com/openapi.yaml ./external/
생성된 함수를 SSaC에서 @call <pkg>.<Func>({...})로 호출한다.
operationId가 키스톤이다
10개 레이어를 어떻게 묶는가? PascalCase 식별자 하나로.
ExecuteWorkflow라는 operationId를 입력하면:
── Feature Chain: ExecuteWorkflow ──
OpenAPI api/openapi.yaml POST /workflows/{id}/execute
SSaC service/workflow/execute_workflow.ssac @get @empty @auth @state @call @publish @response
DDL db/workflows.sql CREATE TABLE workflows
DDL db/execution_logs.sql CREATE TABLE execution_logs
Rego policy/authz.rego resource: workflow
StateDiag states/workflow.md diagram: workflow → ExecuteWorkflow
FuncSpec func/billing/check_credits.go @func billing.CheckCredits
FuncSpec func/billing/deduct_credit.go @func billing.DeductCredit
FuncSpec func/worker/process_actions.go @func worker.ProcessActions
FuncSpec func/webhook/deliver.go @func webhook.Deliver
Hurl tests/scenario-happy-path.hurl scenario: scenario-happy-path.hurl
API 스펙부터 DB 스키마, 인가 정책, 상태 전이, 함수 구현, 테스트 시나리오까지 — 기능 하나의 전체 지형이 한 화면에 보인다. Grep 수십 번이 명령어 하나로 대체된다.
operationId가 키스톤인 이유는, 풀스택 애플리케이션에서 기능의 단위가 API 엔드포인트이기 때문이다. 사용자가 버튼을 누르면 API가 호출되고, 그 API가 나머지 모든 레이어를 관통한다. 이 이름 하나가 10개 레이어를 물리적으로 체이닝한다.
SSaC — 왜 커스텀 DSL인가
yongol의 10 SSOT 중 8종은 업계 표준이다. SSaC(Service Sequences as Code)와 STML만 yongol이 만들었다. SSaC는 서비스 흐름 결정을 캡처한다.
SSaC가 채우는 빈 자리. 선언적 도구의 스펙트럼을 보면, 한쪽 끝에 계약 표준(OpenAPI, SQL, Rego)이 있다 — 무엇을 선언하지만 어떤 순서로는 아니다. 반대쪽 끝에 워크플로우 런타임(Temporal, Inngest, Restate)이 있다 — 이것들은 코드다. 결정과 구현 세부사항이 같은 파일에서 재결합한다. SSaC는 그 사이 빈 자리에 앉는다: “하나의 엔드포인트 내부에서 무슨 일이, 어떤 순서로, 어떤 가드와 함께 일어나는가.”
SSaC의 전체 어노테이션은 16개다. 한 페이지 매뉴얼로 학습 가능하다.
SSaC 어노테이션 전체 목록
| 어노테이션 | 역할 | 형식 |
|---|---|---|
@get | DB 조회 | Type var = Model.Method({args}) |
@post | 행 생성 | Type var = Model.Method({args}) |
@put | 행 수정 (반환 없음) | Model.Method({args}) |
@delete | 행 삭제 | Model.Method({args}) |
@empty | nil 가드 → 404 | var "message" [STATUS] |
@exists | not-nil 가드 → 409 | var "message" [STATUS] |
@auth | 인가 검증 | "action" "resource" {inputs} "message" [STATUS] |
@state | 상태 기계 전이 | diagram {inputs} "transition" "message" [STATUS] |
@call | 함수 호출 | [Type var =] pkg.Func({args}) |
@eval | 술어 가드 (true → 에러) | pkg.Func({args}) "message" STATUS |
@publish | 큐 발행 | "topic" {payload} |
@subscribe | 큐 트리거 함수 | "topic" |
@verify-password | 로그인 (타이밍 방어) | Model.col=source Model.hash vs source -> var STATUS "msg" |
@response | JSON 반환 | { field: var, ... } 또는 var |
@no-pagination | 페이지네이션 룰 면제 | (함수 레벨) |
@state-neutral | 상태 기계 룰 면제 | (함수 레벨) |
SSaC 예시 — AcceptProposal
인가 + 이중 상태 기계 + 에스크로 + 큐:
package service
import "github.com/org/project/internal/billing"
// @get Proposal p = Proposal.FindByID({ID: request.id})
// @empty p "Proposal not found" 404
// @get Gig gig = Gig.FindByID({ID: p.GigID})
// @empty gig "Gig not found" 404
// @auth "AcceptProposal" "gig" {ResourceID: request.id} "Forbidden" 403
// @state proposal {status: p.Status} "AcceptProposal" "Cannot accept" 409
// @state gig {status: gig.Status} "AcceptProposal" "Cannot accept on gig" 409
// @put Proposal.UpdateStatus({ID: p.ID, Status: "accepted"})
// @put Gig.AssignFreelancer({ID: gig.ID, FreelancerID: p.FreelancerID, Status: "in_progress"})
// @call billing.HoldEscrowResponse escrow = billing.HoldEscrow({GigID: gig.ID, Amount: gig.Budget})
// @publish "proposal.accepted" {GigID: gig.ID, FreelancerID: p.FreelancerID}
// @get Proposal updated = Proposal.FindByID({ID: p.ID})
// @response { proposal: updated }
func AcceptProposal() {}
16줄. 10개 어노테이션. 두 개의 상태 기계, 인가, 에스크로, 큐 이벤트, 응답 — 모든 결정이 보이고, 모든 세부사항이 없다.
벤치마크: ZenFlow
ZenFlow — 멀티테넌트 워크플로우 자동화 SaaS.
| 단계 | 내용 | 시간 | 누적 |
|---|---|---|---|
| 초기 빌드 | 10 엔드포인트, 6 테이블, 인증, 상태 기계 | 13분 | 13분 |
| + 버전 관리 | 워크플로우 복제, 버전 목록 | 6분 | 19분 |
| + 웹훅 | 웹훅 CRUD, 큐 백엔드 | 6분 | 25분 |
| + 템플릿 마켓플레이스 | 커서 페이지네이션, 크로스 조직 복제 | 3분 | 28분 |
| + 파일 첨부 | 실행 리포트, 파일 백엔드 | 4분 | 32분 |
| + 스케줄링 | 크론 스케줄링, 세션 백엔드 | 6분 | 38분 |
| + 감사 로그 | 오프셋 페이지네이션, 캐시 백엔드 | 3분 | 41분 |
| + 대시보드 | 관계 조인, func 응답 타입 | 7분 | 48분 |
| + 일괄 작업 | jsonb 일괄 삽입 | 14분 | 62분 |
| + 외부 API | 지오코딩 func, 컬럼 추가 | 3분 | 65분 |
| + 조건부 업데이트 | 센티넬 패턴, 자동 할당 | 4분 | 69분 |
최종: 32 엔드포인트, 14 테이블, 47 Hurl 요청. 11/11 단계 통과.
기능을 추가할수록 속도가 느려지지 않았다. 기존 테스트가 깨지지 않았다. 200 엔드포인트의 벽이 존재하지 않았다.
Opus 4.7 벤치마크 — 32 엔드포인트, 14 테이블, 47 Hurl 요청, ~69분. Sonnet 4.6 벤치마크 — 32 엔드포인트, 9 테이블, 37 Hurl 요청, ~43분.
yongol agent
SSOT 파일을 LLM이 validate-fix 루프로 자동 수정한다.
yongol agent specs/ --model ollama:gemma4:e4b --max-rounds 20
validate 에러를 LLM에게 피드백하고, LLM이 수정하고, 다시 validate하는 루프. 0 에러가 될 때까지 반복한다. 로컬 4.5B 모델(Gemma4)로도 작동한다.
지원 백엔드: ollama (로컬), xai (Grok), gemini.
생성된 코드를 편집할 수 있는가
가능하다. yongol generate는 재실행 시 사용자 편집을 보존한다:
- 모든 생성 파일에
//yg:checked llm=yongol-gen hash=<8hex>어노테이션이 붙는다. - 해시가 달라지면 해당 파일은 보존(preserved) 상태로 표시되고, 다음
generate에서 건너뛴다. yongol status로 보존 파일과 계약 드리프트(PRV-01/PRV-02에러)를 확인할 수 있다.- 의도를 기록하려면
//yg:preserve reason="..."를 추가한다(선택). 보존 해제는 파일 삭제로.
빌트인 함수와 모델
기본 함수 (SSaC에서 @call로 호출)
| 패키지 | 함수 | 설명 |
|---|---|---|
auth | hashPassword, verifyPassword | bcrypt 해싱/검증 |
auth | issueToken, verifyToken, refreshToken | JWT 토큰 |
auth | generateResetToken | 비밀번호 재설정 |
crypto | encrypt, decrypt | AES-256-GCM |
crypto | generateOTP, verifyOTP | TOTP |
storage | uploadFile, deleteFile, presignURL | S3 |
mail | sendEmail, sendTemplateEmail | SMTP |
text | generateSlug, sanitizeHTML, truncateText | 텍스트 처리 |
image | ogImage, thumbnail | 이미지 생성 |
빌트인 모델 (manifest.yaml로 설정)
| 패키지 | 인터페이스 | 백엔드 | SSaC 사용법 |
|---|---|---|---|
session | SessionModel (Set/Get/Delete + TTL) | PostgreSQL, Memory | session.Session.Get({key: ...}) |
cache | CacheModel (Set/Get/Delete + TTL) | PostgreSQL, Memory | cache.Cache.Set({key: ..., value: ..., ttl: ...}) |
file | FileModel (Upload/Download/Delete) | S3, LocalFile | file.File.Upload({key: ..., body: ...}) |
queue | singleton Pub/Sub (Publish/Subscribe) | PostgreSQL, Memory | @publish "topic" {payload} |
DDL 마이그레이션 자동 생성
yongol generate는 DDL 변경을 감지하여 마이그레이션 파일을 자동 생성한다.
specs/db/
└── users.sql # SSOT — 여기서 편집
artifacts/db/
├── .latest_schema.sql # 베이스라인 스냅샷
└── migrations/
├── 0001_initial.up.sql
├── 0001_initial.down.sql
├── 0002_add_users_email.up.sql
└── 0002_add_users_email.down.sql
모호한 변경(컬럼 이름 변경, 타입 캐스팅, NOT NULL 백필)은 DDL 주석 힌트(-- @rename, -- @cast, -- @backfill, -- @data_migration, -- @allow_destructive)로 구분한다. 6개 규칙(MIG-001~MIG-006)이 위험한 변경을 게이트한다. 실제 DB 적용은 golang-migrate, flyway 등 표준 도구에 위임한다.
왜 더 큰 모델이 답이 아닌가
“GPT-6이 나오면 해결될 거야.”
해결되지 않는다. 문제는 모델의 지능이 아니라 매체다.
코드라는 매체는 결정과 구현을 구분하지 않는다. 어떤 모델이든 코드를 읽으면 결정과 세부사항이 뒤섞인 텍스트를 본다. 모델이 아무리 똑똑해도, 매체가 구분을 제공하지 않으면 구분할 수 없다.
yongol은 매체를 바꾼다. AI가 편집하는 대상을 코드에서 선언적 명세로 옮긴다. 명세에는 결정만 있고 구현 세부사항이 없으므로, AI가 결정을 세부사항으로 착각할 일이 없다. 결정의 생존이 모델 크기와 무관해진다.
작은 LLM이 SSOT만 편집하고, validate가 매 실수마다 정밀한 피드백을 주면, 훨씬 큰 모델이 raw code를 편집하는 것과 같은 수준의 결정 무결성을 유지할 수 있다. yongol이 그 차이를 메운다.
런타임 테스트
Hurl 테스트는 전부 사용자가 작성한다. specs/tests/에 작성하면 yongol generate가 artifacts/tests/로 미러링한다. 검증 시 XOH-01~09 규칙이 Hurl을 OpenAPI, 상태 기계, manifest.auth와 교차 검증한다.
hurl --test --variable host=http://localhost:8080 artifacts/my-project/tests/*.hurl
세 가지 분류:
- smoke.hurl — 엔드포인트 스모크 테스트
- scenario-*.hurl — 비즈니스 시나리오 테스트
- invariant-*.hurl — 엔드포인트 간 불변식 테스트
현재 상태
Go+Gin 백엔드 생성: Beta — 엔드 투 엔드 작동. React 프론트엔드 생성: Alpha (작업 중).
시작
방법 1: 스킬 설치 (권장)
npx skills add park-jun-woo/yongol
AI 에이전트(Claude Code, Cursor, Copilot 등)에 yongol skill을 설치하면, 에이전트가 워크플로우를 자동으로 학습한다.
/yongol 인증과 CRUD가 있는 멀티테넌트 투두 SaaS를 만들어.
방법 2: 직접 설치
Go 1.25+ 및 gcc(cgo 의존: pg_query_go가 DDL 파싱을 위해 libpg_query를 링크)가 필요하다.
git clone https://github.com/park-jun-woo/yongol && cd yongol
make install
yongol validate examples/zenflow
0 errors, 0 warnings.
이 명세 위에서 AI에게 기능을 추가하라고 시켜보라. validate가 레일을 깔고, AI가 레일 위를 달린다. 벽은 없다.
관련 글
- SSaC — Service Sequences as Code — yongol의 키스톤 DSL. 엔드포인트 내부 결정을 선언한다.
- Feature Chain — operationId 하나로 풀스택을 추적한다 — operationId로 8개 레이어를 관통하는 추적.
- Ratchet Pattern — 에이전트가 끝까지 가게 만드는 방법 — validate가 에이전트에게 피드백을 주는 구조의 이론적 배경.
- IFEval을 역이용하는 래칫 코드 — 아첨 편향을 이용한 코드 생성 루프와 Reins.
코드: github.com/park-jun-woo/yongol
출처
- Google DORA Team. DORA State of AI-Assisted Software Development 2025. Google Cloud, 2025. dora.dev/dora-report-2025
- David L. Parnas. “On the Criteria to Be Used in Decomposing Systems into Modules.” Communications of the ACM 15(12): 1053-1058, 1972. doi:10.1145/361598.361623
- Weslley Torres, Mark G.J. van den Brand, Alexander Serebrenik. “A Systematic Literature Review of Cross-Domain Model Consistency Checking by Model Management Tools.” Software and Systems Modeling 20(3): 897-916, 2021. doi:10.1007/s10270-020-00834-1
- Deepak Babu Piskala. “Spec-Driven Development: From Code to Contract in the Age of AI Coding Assistants.” arXiv:2602.00180, January 2026. arxiv.org/abs/2602.00180
- Ehsani et al. “When AI Code Doesn’t Stick: An Empirical Study on Reverted Changes Introduced by AI Coding Agents.” MSR 2026 Mining Challenge, April 2026. 2026.msrconf.org
- Anton Jansen, Jan Bosch. “Software Architecture as a Set of Architectural Design Decisions.” EWSA 2005, LNCS 3527, Springer, 2005. semanticscholar.org
- Marco Brambilla, Jordi Cabot, Manuel Wimmer. Model-Driven Software Engineering in Practice. 2nd ed., Springer, 2017. doi:10.1007/978-3-031-02546-4
- GitClear. AI Copilot Code Quality 2025. February 2025. gitclear.com
변경 이력
| 날짜 | 변경 내용 |
|---|---|
| 2026-05-18 | 최초 발행 |
| 2026-05-19 | Opus 벤치마크 추가. 10 SSOT 업데이트 |
| 2026-05-21 | README 싱크: 벤치마크 갱신 (Opus 32ep/14tbl/47hurl/69min, Sonnet 32ep/9tbl/37hurl/43min), “Harness with reins” 선언 추가, SSaC 예시(AcceptProposal) 추가, yongol agent 명령어 추가, Preserve 시스템 추가, 빌트인 함수/모델 목록 추가, DDL 마이그레이션 자동 생성 추가, STML 설명 추가, ifeval-ratchet 관련 글 링크 추가 |
| 2026-05-26 | v0.6.10 싱크: yongol next 래칫 명령어 추가, yongol init/features add/features remove 프로젝트 생성·관리 추가, yongol import 외부 OpenAPI 임포트 추가, SSaC 어노테이션 전체 목록(16개) 추가 (@eval, @subscribe, @verify-password, @no-pagination, @state-neutral), Hurl 테스트 3분류(smoke/scenario/invariant), 런타임 테스트 섹션, Preserve 상세화(PRV 에러 코드), DDL 마이그레이션 힌트 확장(@data_migration, @allow_destructive, MIG 규칙), 현재 상태(Go+Gin Beta, React Alpha), 설치 방법 2가지 분리 |