yongol — AI 코딩 SaaS의 용골 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프로젝트 설정 — 인증, 미들웨어, 인프라
OpenAPIAPI 계약 — 경로, 파라미터, 응답
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 어노테이션 전체 목록

어노테이션역할형식
@getDB 조회Type var = Model.Method({args})
@post행 생성Type var = Model.Method({args})
@put행 수정 (반환 없음)Model.Method({args})
@delete행 삭제Model.Method({args})
@emptynil 가드 → 404var "message" [STATUS]
@existsnot-nil 가드 → 409var "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"
@responseJSON 반환{ 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로 호출)

패키지함수설명
authhashPassword, verifyPasswordbcrypt 해싱/검증
authissueToken, verifyToken, refreshTokenJWT 토큰
authgenerateResetToken비밀번호 재설정
cryptoencrypt, decryptAES-256-GCM
cryptogenerateOTP, verifyOTPTOTP
storageuploadFile, deleteFile, presignURLS3
mailsendEmail, sendTemplateEmailSMTP
textgenerateSlug, sanitizeHTML, truncateText텍스트 처리
imageogImage, thumbnail이미지 생성

빌트인 모델 (manifest.yaml로 설정)

패키지인터페이스백엔드SSaC 사용법
sessionSessionModel (Set/Get/Delete + TTL)PostgreSQL, Memorysession.Session.Get({key: ...})
cacheCacheModel (Set/Get/Delete + TTL)PostgreSQL, Memorycache.Cache.Set({key: ..., value: ..., ttl: ...})
fileFileModel (Upload/Download/Delete)S3, LocalFilefile.File.Upload({key: ..., body: ...})
queuesingleton 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 generateartifacts/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가 레일 위를 달린다. 벽은 없다.


관련 글

코드: github.com/park-jun-woo/yongol


출처

  1. Google DORA Team. DORA State of AI-Assisted Software Development 2025. Google Cloud, 2025. dora.dev/dora-report-2025
  2. 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
  3. 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
  4. 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
  5. 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
  6. Anton Jansen, Jan Bosch. “Software Architecture as a Set of Architectural Design Decisions.” EWSA 2005, LNCS 3527, Springer, 2005. semanticscholar.org
  7. Marco Brambilla, Jordi Cabot, Manuel Wimmer. Model-Driven Software Engineering in Practice. 2nd ed., Springer, 2017. doi:10.1007/978-3-031-02546-4
  8. GitClear. AI Copilot Code Quality 2025. February 2025. gitclear.com

변경 이력

날짜변경 내용
2026-05-18최초 발행
2026-05-19Opus 벤치마크 추가. 10 SSOT 업데이트
2026-05-21README 싱크: 벤치마크 갱신 (Opus 32ep/14tbl/47hurl/69min, Sonnet 32ep/9tbl/37hurl/43min), “Harness with reins” 선언 추가, SSaC 예시(AcceptProposal) 추가, yongol agent 명령어 추가, Preserve 시스템 추가, 빌트인 함수/모델 목록 추가, DDL 마이그레이션 자동 생성 추가, STML 설명 추가, ifeval-ratchet 관련 글 링크 추가
2026-05-26v0.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가지 분리