라우터가 레지스트리다
Hanzo Cloud 에는 체크인된 OpenAPI 파일이 없다. 살아 있는 라우트 표가 원천이고, 세 갈래로 투영되며, 어느 투영이 어긋나면 전단사 시험이 빌드를 떨어뜨린다. 963 개 경로에 걸친 1,362 개 연산이, 요청 시각에 프로세스마다 만들어진다.

이 페이지에서
Hanzo Cloud 저장소에 openapi.yaml 은 없다. 엔드포인트를 늘어놓은 둘째 레지스트리도 없고, 주석 파일도 없으며, 명세를 디스크에 쓰는 CI 생성 단계도 없다. API 가 무엇인지 알고 싶으면 돌고 있는 프로세스에 묻는다. 그것은 자기 라우터를 읽어 답한다.
이 글은 그 배치만이 끝내 버틴 이유와, 작동하지 않는 부분에 대한 것이다. 작동하지 않는 쪽이 더 쓸모 있는 절반으로 드러난다.
표 하나, 투영 셋
serve.go 이 라우트 표 하나를 짠다. 세 가지가 그것을 읽는다. 모두 MountAll 다음이라, 각각이 완전한 표를 본다:
/zap은 ZAP 전송 위에서/v1핸들러를 다시 돌린다.- 콘솔은 그것들을 그린다.
GET /v1/openapi.json은 그것들을 기술한다.
셋 다 무엇의 사본도 쥐지 않는다. 그것이 설계 제약의 전부이고, 나머지는 모두 거기서 따른다. 엔드포인트의 자기 목록을 쥔 투영은 둘째 진실의 원천이 되고, 둘째 진실의 원천은 어긋난다. 가끔이 아니라 언제나, 누군가 잊는 빈도가 정하는 일정으로.
기술 투영이 사람들이 묻는 것이므로 이렇다. openapi.Live(app) 이 app.Fiber().GetRoutes(true) 을 부르고, openapi/ 의 다른 함수는 모두 돌아오는 []Route 의 순수 함수다. 문서는 라우터로 지어지지 않는다. 문서는 다른 모양의 라우터 그 자체 다.
살아 있는 라우터를 읽는 것만이 완전한 원천인 이유
뻔한 대안은 소스를 훑는 것이다. 그것은 되지 않는다. 이유는 정확히 말할 가치가 있다. 많은 API 도구가 조용히 덜 보고하는 이유이기 때문이다.
POST /v1/kms/auth/login 은 이렇게 등록된다:
Group("/v1/kms/auth").Post("/login")
어떤 grep 도 그 경로를 찾지 못한다. 문자열 /v1/kms/auth/login 은 코드베이스에 없다. 그것은 조립된 라우터 안, 실행 시각에, 프레임워크가 그룹 접두와 잎을 이은 뒤에만 있다.
그러고는, 실은 옳은 방식으로, 더 나빠진다. 라우트 집합은 배포 설정의 함수다. cfg.Enabled 에 더해 KMS 의 if kc != nil 같은 내부 문이 있다. 관리를 마운트하지 않는 배포는 관리 라우트를 내지 않는다. 그래서 그 배포의 정직한 명세는 그것들을 광고하지 않는다.
그래서 문서는 프로세스마다, 요청 시각에 만들어지고, CI 에서 한 번 지어지지 않는다. CI 에서 지은 명세는 가상의 최대 배포를 적고, 그것은 지금 말하고 있는 상대가 아닐 수 있다. 살아 있는 라우터에서 읽은 명세는 당신에게 답한 프로세스를 적는다.
그것을 참으로 만드는 시험
명세가 어긋날 수 없는 구조는 주장이며, 주장에는 문이 필요하다. 우리의 문은 전단사이고, cmd/cloud/openapi_test.go 에 있다:
func TestSpecDoesNotDriftFromLiveRouter(t *testing.T) {
apps.Wire() 의 모든 하위 시스템을 마운트하고, 문서를 만들며, 대응을 양방향으로 단언한다. 살아 있는 라우트는 모두 연산으로 나타나고, 모든 연산은 살아 있는 라우트가 받친다. 어느 방향에서도 부분집합이 아니다.
오늘 돌리면:
bijection holds over 1362 operations / 963 paths / 138 products
실패가 공개된 문서가 거짓말을 한다는 뜻인, 저장소의 유일한 시험이다. 다른 고장은 제품을 틀리게 한다. 이것은 계약을 틀리게 한다. 그쪽이 더 나쁘다. 통합하는 쪽이 이미 그것을 기준으로 지었기 때문이다.
수가 움직이는 것은 문제가 아니다. 하위 시스템이 더해질 때마다 움직인다. 중요한 수는 양쪽의 차이고, 그 수는 0 이 아니면 빌드가 실패한다.
제품 축은 기계적이다
/v1/ 다음의 첫 경로 조각이 제품이다. openapi.Product 이 그것을 읽고 각 연산에 태그한다. 그래서 CLI 는 명령이 어디에 살아야 하는지 아무도 판단하지 않고 hanzo <product> <resource> <verb> 를 짠다.
이것은 일부러 하위 시스템 이름이 아니다. clients/billing 은 /v1/finance/* 도 낸다. 호출자가 보는 것은 URL 이므로 URL 이 이긴다. 축을 내부 패키지 이름으로 하면 CLI 는 의견이 든, 누군가 지켜야 하는 선별된 산물이 되고, 투영이 아니게 된다.
라우터가 말해 주지 못하는 것
이것이 한계이고, 돌아갈 생각은 없다.
메서드, 경로, 경로 매개변수, 제품은 라우터에서 이끌어 낼 수 있다. 요청과 응답 스키마, 쿼리와 헤더 매개변수, 상태 코드, 인증은 이끌어 낼 수 없다. 라우터가 쥐는 것은 func(*zip.Ctx) error 이다. 요청 타입은 핸들러 본문 안의 지역 변수다:
var req secretPutRequest
json.Unmarshal(ctx.Body(), &req)
Go 는 함수 값에서 그 본문으로 반사할 수 없다. 여기에 영리한 수정은 없고, 생성기에서 시도하는 것은 추측을 뜻한다. 명세가 확신에 찬 틀린 항목을 얻는 방식이 그것이다.
스키마로 가는 유일한 길은 타입이 있는 연산이다:
zip.Get[In, Out](...)
In 과 Out 은 등록 위에 있으므로, 누구의 마음을 읽지 않고 스키마를 얻는다. 등록 위에 있으므로 같은 항목은 MCP 도구도 된다. zip/openapi.go 와 zip/mcp.go 은 레지스트리 하나의 두 독자다. 그것이 넷째 투영이고, 넷째 장치 없이 도착했다.
이전은 구성상 점진적이다. GetRoutes() 은 app.ops 의 상위 집합이므로, 핸들러를 타입이 있는 연산으로 바꾸면 그 상류를 바꾸지 않고 스키마가 더해진다.
타입을 줄 수 없는 라우트, 그리고 그 이유
clients/integrations 은 선이 허용하는 데까지 바뀌었다. 41 가운데 20 이 타입이 있는 연산이다. 나머지 21 은 날 핸들러로 남고, 각각 ops_projection_test.go 의 rawRoutes 맵에 움직일 수 없는 이유와 함께 올라 있다:
- 브라우저 리다이렉트 13. OAuth 콜백과 모든 링크 다리는
302으로 답한다. 타입이 있는 연산은 값을 돌려주고,Location헤더와 300 번대의 상태는 돌려주지 않는다. - 웹훅 6. Slack 과 GitHub 의 HMAC, Discord 의 Ed25519, Teams Bot Framework 의 JWT, Telegram 의 비밀 토큰 헤더. 모두 날 요청 바이트 위의 서명을 인증한다. 타입이 있는 연산은 디코드된
In을 받고, 서명이 계산된 바이트는 보지 못한다. 그래서 이들에 타입을 주면 인증이 깨진다. 202 Accepted작성자 2.cloud.Created은 201 이음새를 준다. 202 이음새는 없다. 두 라우트가 하나를 필요로 하므로, 두 라우트는 기다린다.
그 목록이 시스템 전체에서 가장 값진 산물이라고 본다. 추상이 딱 멈추는 곳의, 늘어놓고 시험으로 고정한 목록이며, 항목마다의 이유는 검토자가 반박할 수 있다. 막는 실패는 라우트가 조용히 날 핸들러로 돌아가 문서와 MCP 도구 집합과 CLI 에서 한 번에 사라지는 것이다. 보이지 않게 사라진다. 거기 있었다고 단언하는 것이 아무것도 없었기 때문이다.
아키텍처는 일반화되는 부분이 아니다. 일반화되는 부분에, 일반화되지 않는 부분의 정직하고 강제된 장부를 더한 것이다.
이것이 사는 것
Hanzo Cloud 에 붙는 호출자는, 자기 요청에 답하는 프로세스가, 실제로 말하고 있는 배포에서, 몇 밀리초 전에 만든 문서를 읽는다. MCP 로 부르는 에이전트는 REST 호출자와 같은 핸들러에, 같은 테넌시 검사와 함께 닿는다. 익명의 /mcp 호출은 REST 가 주는 것과 같은 403 을 얻는다. 같은 코드 경로이고, 시험이 그것을 고정하기 때문이다.
그 무엇도, 누군가 무언가를 다시 만들기를 기억할 것을 요구하지 않았다.
일반형은 이렇다. 두 산물이 맞아야 한다면, 하나를 다른 하나에서 만들어 바라지 마라. 하나를 다른 하나의 보기로 만들고, 둘이 아닐 때 실패하는 시험을 써라.
더 읽기

Cloud-Native Commerce: Why Architecture Matters
How we built Crowdstart as a cloud-native platform from day one, and why this matters for commerce.

에이전트형 언어 모델 시대에 헤드리스 커머스를 맞추다
구매자는 에이전트가 되고 있다. 사람이 끼지 않아도 거대언어모델이 읽고, 서명하고, 결제할 수 있게 하려면 무엇이 필요한지, 그리고 Hanzo가 오늘 그것을 위해 내놓는 것.

Introducing Enso — one API, routed to the best frontier intelligence
Enso is Hanzo's learned router: no single frontier model leads every benchmark, so Enso routes each request to the model most likely to win it — and learns from your feedback as it goes. Microsecond routing on a CPU, a transparent per-request meter, and a 1% fee that pays for itself. Here is how it works, the measured results, and the cost-transparency angle a monolith cannot offer.