[WarpInfra] CLI를 1등 시민으로 둔 이유 — CI와 AI 에이전트
인프라 제품을 만들 때 UI 논쟁은 늘 같은 꼴로 시작된다. "GUI가 있어야 일반 사용자가 씁니다." 맞는 말이고, WarpInfra에도 GUI가 있다. 논쟁의 핵심은 거기가 아니다. 문제는 GUI를 먼저 만들고 CLI를 "개발자용 부가기능"으로 두는 순서다. GUI가 1등이면 CLI는 GUI의 기능을 따라가는 곁가지가 되고, 곁가지 CLI는 계약이 아니라 임시 구현이 된다. WarpInfra는 정반대로 갔다. CLI(warp)가 1등 시민이고, GUI와 agentd가 같은 코어를 공유한다. 이 편은 그 선택의 근거와, 이 선택이 실제 구현 커밋에 어떻게 남았는지를 본다.
이 제품의 운영자는 사람이 아니다
근거는 사용자 상상에서 시작한다. release를 사내 그리드로 받아내는 주체가 누구인가.
| 운영자 | GUI로 되나? | CLI 계약 |
|---|---|---|
| 사람 (엔지니어) | 부분 | warp get <release> |
| CI 파이프라인 | ❌ | warp JSON 출력, 종료코드 |
| AI 에이전트 | ❌ | warp 서브커맨드, 결정적 출력 |
사람은 GUI를 쓸 수 있지만, CI는 화면을 볼 수 없고, AI 에이전트는 버튼을 누를 수 없다. 이 둘은 텍스트 계약으로만 제품을 쓴다. 그리고 사내 배포의 실제 소비자를 생각해 보면, 사람이 매번 warp get을 타이핑하는 세상보다 배포 파이프라인이나 에이전트가 호출하는 세상이 훨씬 빈번하다. release 받기는 반복 작업이고, 반복 작업은 결국 자동화된다. 자동화된 세계의 제품 진입점은 API이지 화면이 아니다. "운영자는 사람이 아니라 파이프라인이다"라는 전제가 여기서 나온다.
이 전제가 틀렸다면 — 즉 실제 사용이 사람 중심이라면 — CLI-first는 오히려 개발 비용 낭비다. 그래서 전제 검증을 문서 단계에서 미리 했다. docs/06-cli-first-and-ai-agents.md와 docs/10-cli-contract-phase-a.md에 CLI 계약을 문서로 고정한 건, GUI 논쟁을 감각이 아니라 계약 문서로 끝내겠다는 뜻이었다.
운영자 세 명의 비중은 균등하지 않다. 니치 편의 정의를 다시 떠올려 보면 — "레지스트리에 올라간 release를 사내망에서 여러 Node가 받는다" — 이 동작이 가장 자주 일어나야 할 장소는 release가 올라온 직후다. 빌드가 끝나자마자 열몇 대가 동시에 받는 장면이다. 그 장면에서 운영자는 사람이 아니라 빌드 파이프라인의 다음 단계다. 사람이 개입하는 시점은 문제가 생겼을 때뿐이고, 그때도 사람은 GUI보다 로그와 CLI 출력으로 원인을 찾는다. 즉 정상 경로의 운영자는 기계, 비정상 경로의 관찰자도 결국 텍스트를 보는 존재다. GUI가 커버하는 것은 이 둘 사이의 좁은 틈이다.
계약 문서에 들어간 것들
CLI를 "1등 시민"으로 부르려면 다음 세 가지가 계약이어야 한다.
- 결정적 출력 — 같은 입력에 같은 출력. JSON 구조가 버전 간 임의로 변하면 파이프라인이 깨진다.
- 종료코드 의미론 — 성공/실패/부분 성공이 종료코드로 구분돼야 CI가 재시도 정책을 세울 수 있다.
- 서브커맨드 안정성 — 에이전트가 학습하거나 스크립트가 참조하는 명령 이름이 리팩토링마다 바뀌면 계약 위반이다.
사람용 CLI는 --help로 때울 수 있지만, 파이프라인용 CLI는 이 세 가지가 깨지는 순간 운영이 멈춘다. GUI는 이 계약 위에 얹는 편의 계층일 뿐, 계약 자체가 아니다.
AI 에이전트가 운영자라는 세 번째 항목은 특별한 설계 요구를 더한다. 에이전트는 사람처럼 모호한 출력을 해석하지 못한다. 진행률 표시 같은 장식은 오히려 해석을 방해하고, 실패했을 때 "왜 실패했는지"가 구조화된 형태로 나와야 재시도 정책을 세울 수 있다. 즉 에이전트용 CLI는 사람용보다 더 엄격한 결정성을 요구한다. "사람도 쓰는 CLI를 에이전트가 쓴다"가 아니라 "에이전트가 쓸 수 있는 CLI라면 사람은 당연히 쓴다"는 방향의 설계다.
구현 흔적
이 결정이 커밋 이력에 남은 모습이다.
3568b2f— WarpInfra Phase A local release manifest + warp CLI466c3f3(grid) — mesh operations into the CLI8ac4ec9— docs: make WarpInfra CLI-first for CI and AI agents
3568b2f에서 release manifest와 CLI가 같은 커밋으로 들어간 건 우연이 아니다. manifest는 CLI 계약의 데이터 형식이고, 둘은 같이 설계됐어야 했다. manifest 스키마가 먼저 확정되지 않으면 CLI의 JSON 출력도 정의될 수 없다. 466c3f3는 그리드 쪽 mesh 연산도 CLI로 묶은 것 — 브라우저 엔진 쪽 기능도 "CLI에서 돌릴 수 있어야" 운영 가능한 기능으로 인정하는 규율의 연장이다. 8ac4ec9는 이 결정을 코드 뒤편의 문서로 다시 고정한 커밋이다.
"GUI와 agentd가 같은 코어"는 구현 원칙으로도 남는다. GUI가 코어를 우회해서 자기만의 전송 로직을 가지면, CLI에서 성공하고 GUI에서 실패하는 케이스가 생긴다. 두 진입점이 각자 다른 전송 로직을 가지면 검증이 두 배로 어려워지기 때문이다. GUI는 코어를 부르는 또 하나의 클라이언트일 뿐이라는 위치가 유지되는 이유다.
agentd가 이 구조에서 하는 역할도 짚어두자. agentd는 "에이전트용 데몬"으로, 사람의 세션 없이도 코어를 상시 호출할 수 있게 하는 부품이다. Phase A 부품 목록에서 agentd가 CLI와 나란히 done으로 기록돼 있는데, 이는 CLI 계약이 명령줄 한 번의 실행을 넘어 상시 운영 형태로도 구현됐다는 뜻이다. 파이프라인이 cron으로 warp get을 돌리는 형태와, agentd가 상시 노드로 붙어 있는 형태 — 두 운영 형태가 같은 코어를 공유하니 검증은 한 번만 하면 된다.
CLI-first가 실제로 지켜졌는지 판별하는 간단한 시금석이 하나 있다. "GUI를 통째로 삭제해도 제품이 쓸 수 있는가." WarpInfra의 답은 예스다 — warp get, manifest, agentd만 남아도 release 분산 수신은 완결된다. 반대로 "CLI를 삭제해도 되는가"에 대한 답은 아니오다. GUI는 코어 위의 프리젠테이션이지 기능의 본체가 아니기 때문이다. 1등 시민이라는 말의 실질적 의미가 이 판별식이다.
왜 "GUI 먼저"가 흔한 함정인가
GUI-first의 실제 문제를 좁혀 보면 두 가지다.
첫째, GUI가 먼저 만들어지면 기능이 GUI 프레임에 맞춰 잘린다. 화면에 표현하기 좋은 기능, 클릭 두 번으로 끝나는 기능이 우선되고, 화면으로 표현하기 어렵지만 운영에 필요한 기능 — 배치 실행, 조건부 재시도, 스크립트 결합 — 은 계속 뒷전이 된다. 제품의 기능 목록이 UI의 표현력에 의해 결정되는 역전이 일어난다.
둘째, GUI 전용 로직이 생긴다. 화면 진행률 표시를 위해 코어에 없는 중간 상태를 GUI가 자기 계산으로 만들어내기 시작하면, 그 순간부터 GUI는 코어의 클라이언트가 아니라 두 번째 구현이 된다. 두 번째 구현은 반드시 첫 번째와 어긋나고, 어긋난 채로 출시된다. "GUI에서는 되는데 CLI에서는 안 돼요"라는 버그 리포트는 대부분 이 역전의 증상이다.
WarpInfra의 순서는 이 역전을 구조적으로 막는다. CLI 계약이 먼저 확정되면 GUI는 그 계약을 표현하는 방법을 고민하게 되고, 계약 밖의 기능은 GUI에도 없다. 화면이 필요해서 기능을 만드는 게 아니라 기능의 계약이 있어서 화면이 있는 구조다.
대가와 남은 구멍
CLI-first에도 비용이 있다. CLI 계약은 한번 공개되면 함부로 못 바꾼다. 내부 리팩토링이 계약 변경으로 번지지 않게 변환 계층을 유지해야 하고, 출력 스키마를 바꿀 때마다 버전 호환성을 고민해야 한다. GUI-only 제품이라면 그냥 화면을 바꾸면 끝나는 일이다. 또한 CLI가 1등이 되면 터미널 환경의 UX — 에러 메시지 품질, 도움말, 진행 표시 — 가 곧 제품의 얼굴이 된다. GUI에 들어갈 정성이 CLI에도 들어가야 한다는 뜻이다.
계약 유지의 부담은 시간이 갈수록 커진다. 초반의 CLI는 자유롭게 바꿀 수 있지만, 파이프라인과 에이전트가 쓰기 시작하면 "이 출력 필드를 안 쓰면 되지"가 아니라 "이 필드를 없애면 누군가 깨진다"가 된다. 그래서 계약의 최소화가 중요하다. 출력 필드를 풍성하게 만들면 좋아 보이지만, 필드 하나하나가 평생 짊어질 부채다. CLI 계약 설계에서 절제는 배려다.
그리고 아직 확정되지 않은 구멍도 명확하다. 결정적 출력 스키마의 버전 표기와 에이전트용 인증(mTLS). 계약 문서가 있어도, 에이전트가 제품을 호출할 때 "내가 말하는 상대가 인증된 노드인지"를 검증하는 장치가 없으면 보안 경계가 열려 있다. 출력 스키마 버전도 마찬가지 — 계약이 곧 신원이 되는 세계에서는 계약의 판본이 명시돼야 상대가 호환성을 판단할 수 있다. 이 구멍은 S4 백로그에 "인증서/mTLS"로 남아 있다. RC1 원칙 3("운영 환경에서 인증서 검증 우회를 허용하지 않는다")이 이 구멍을 닫을 때의 기준이 된다.
다음 편부터는 설계에서 구현으로 넘어간다. 첫 번째 주제는 WASM을 버리고 네이티브 QUIC로 갈아탄 전환의 기록이다.
이 시리즈
- 이전: 컴퓨트 그리드를 일부러 뒤로 미룬 로드맵
- 다음: WASM을 버리고 네이티브 QUIC로
- 목차: PonsWarp 개발 회고