CLI 하나로 스튜디오 전체를 운영할 수 있을까? - 난테 스튜디오 아키텍처 3편
앱 출시, 광고, 시크릿까지 사내 CLI 하나로 처리합니다. 한 달 7만 번 넘게 실행되는데, 그중 대부분은 에이전트가 실행합니다. 에이전트가 쓰기 좋은 CLI를 만들려고 정한 규칙을 정리했습니다.
난테 스튜디오에서 앱을 출시하고, 광고를 운영하고, 시크릿을 꺼내 쓰는 일은 nst라는 사내 CLI 하나로 합니다. Rust로 만든 바이너리 하나에 최상위 명령이 41개, 실제로 실행되는 하위 명령이 446개 있는데요. 이 명령을 가장 많이 실행하는 쪽은 사람이 아니라 에이전트입니다. 그래서 CLI가 헷갈리거나 위험하면 그 영향이 스튜디오 전체로 바로 번집니다.
그런데 사람이 쓰기 편한 CLI와 에이전트가 쓰기 편한 CLI는 조금 다릅니다. 사람은 "정말 삭제할까요?"를 읽고 판단하지만, 에이전트는 확인 창을 띄울 수 없는 환경에서 명령을 실행하는 경우가 많아요. 사람은 실패 메시지가 이상하면 한 번 더 해 보지만, 에이전트는 메시지를 그대로 믿고 다음 행동을 정합니다. 실제로 토큰이 멀쩡한데도 "다시 로그인하세요"라는 메시지 때문에 작업이 멈추는 일이 반복됐습니다.
지난 30일 동안 nst는 컴퓨터 4대에서 71,980번 실행됐습니다. 이번 글에서는 이 CLI의 구조와 에이전트를 위해 둔 규칙, 그리고 가장 많이 쓰이는 시크릿 기능을 공유하려고 합니다.
어떤 명령이 가장 많이 쓰일까
nst의 최상위 명령은 스튜디오가 다루는 영역을 그대로 따릅니다. 시크릿, Play 스토어, App Store, 광고, 광고 수익, 시장 조사, 분석, 출시 같은 식이에요. 명령은 많지만 실제로 쓰이는 명령은 몇 개에 몰려 있습니다.
그림 1. 지난 30일 동안 가장 많이 실행된 명령 10개입니다. 막대 옆 숫자는 전체 71,980번 중 차지한 비율입니다.
상위 10개가 전체의 71%를 차지했습니다. 그중 두 개가 시크릿을 꺼내는 명령인데, 둘만 합쳐도 40%예요. 에이전트는 무슨 일을 하든 먼저 키를 꺼내야 하니까요. 그래서 시크릿 기능은 빨라야 하고, 무엇보다 사람을 부르면 안 됩니다. 에이전트가 시크릿을 꺼낼 때마다 로그인을 요구하면, 스튜디오의 작업 40%가 사람을 기다리게 됩니다.
CLI와 MCP는 어떻게 이어질까
에이전트가 nst에 닿는 길은 세 가지입니다. 맥의 Claude Code는 명령을 직접 실행합니다. MCP로 붙는 클라이언트는 로컬 MCP 서버를 거치고, Claude 앱처럼 셸이 없는 곳은 원격 MCP 서버를 거칩니다.
그림 2. 에이전트가 nst와 데이터에 닿는 세 가지 길입니다. 원격 MCP는 읽기만 할 수 있습니다.
- 로컬 MCP 서버: 맥에서 돌아가고,
nst의 모든 명령을 그대로 감쌉니다. 위험한 명령도 포함입니다. - 원격 MCP 서버: Cloudflare Worker에서 돌아갑니다.
nst를 실행하지 않고 같은 데이터를 직접 읽으며, 지표를 읽는 도구만 엽니다. 무언가를 바꾸는 도구나 오래 걸리는 도구는 없습니다.
셋이 서로 어긋나지 않게 하는 장치가 하나 있습니다. nst의 명령 트리 전체를 JSON 파일 하나로 뽑아 저장소에 둡니다. 명령을 추가하거나 바꾸고 이 파일을 갱신하지 않으면 푸시가 막힙니다. 명령 트리가 바뀌었는데 MCP 서버를 하나도 고치지 않았으면 경고가 뜹니다.
에이전트가 쓰기 좋은 CLI는 무엇이 다를까
에이전트가 매일 쓰다 보니 규칙이 몇 개 생겼습니다.
- 위험한 명령은
--yes가 있어야 실행됩니다. 삭제, 운영 배포, 광고 켜기, 예산 올리기 같은 명령이에요. 터미널에서 사람이 실행하면 확인을 묻고, 터미널이 없으면 묻지 않고 거절합니다. 예전에는 터미널이 없을 때 확인 없이 그냥 진행되는 경로가 있었고, 그 길로 에이전트가 운영 데이터를 바꾼 적이 있습니다. --json출력은 약속입니다. 스킬과 훅, 크론, MCP 서버가 이 출력을 읽기 때문입니다. 명령이나 필드를 추가하면 마이너 버전을 올리고, 필드를 없애거나 타입을 바꾸면 메이저 버전을 올립니다.- 모르는 값은 0이 아닙니다. 광고비를 못 읽었는데 0으로 계산하면, 손해 본 달이 이익 본 달로 보입니다. 지금은
null로 내보내고 이유를 함께 적습니다. - 에러 메시지는 해결 방법까지 말합니다. 에이전트는 에러 메시지를 읽고 다음 명령을 정하니까요. 무엇이 실패했는지만 말하면 에이전트가 엉뚱한 해결책을 시도합니다.
15만 줄은 어떻게 나눴을까
지금 nst는 Rust 코드 15만 8천 줄, 테스트 약 2,400개입니다. 처음에는 이 전부가 패키지 하나에 들어 있었어요. 그러다 보니 한 줄을 고쳐도 푸시할 때 전체를 다시 린트하고 테스트를 전부 다시 돌렸습니다. 당시 패키지는 11만 2천 줄, 테스트는 1,682개였고, 한 줄 수정의 푸시 검사가 중간값으로 252초 걸렸습니다.
나누려고 보니 문제가 있었습니다. 광고, Play 스토어, 출시, 리포트 같은 큰 영역 8개가 서로를 참조하는 고리로 묶여 있었어요. 7만 줄이 한 덩어리였던 셈입니다. Rust는 크레이트 사이의 순환 참조를 허용하지 않으니, 이 상태로는 어느 것도 따로 뗄 수 없었습니다.
그래서 순서를 바꿨습니다. 먼저 순환을 찾고, 여러 영역이 같이 쓰는 부분을 아래쪽 공통 크레이트로 내렸습니다. Google API 클라이언트, 스토어 페이지 수집기, 폰트 같은 것들이에요. 그다음 아무것도 참조하지 않는 영역부터 하나씩 크레이트로 뺐습니다.
그림 3. 패키지 하나였던 nst를 크레이트 19개로 나눴습니다. 바이너리 크레이트에는 아직 옮기지 못한 영역이 남아 있습니다.
지금은 크레이트가 19개이고, 바이너리 크레이트는 4만 7천 줄입니다. 푸시할 때는 바뀐 크레이트와 그 크레이트를 참조하는 쪽만 검사합니다. 나머지 영역도 같은 순서로 옮기고 있습니다.
Rust를 계속 쓰는 데도 이유가 있습니다. 한때 시크릿 기능 전체를 Go로 똑같이 옮겨 본 적이 있는데요. 결국 Rust에 남기로 했습니다. 응답 형식이 바뀌었을 때 Rust는 시끄럽게 실패하고, Go는 빈 필드를 0으로 채우고 조용히 넘어갑니다. 출력을 보고 광고비와 출시를 결정하는 CLI에서는 시끄럽게 실패하는 쪽이 안전합니다.
시크릿은 어떻게 꺼내 쓸까
시크릿은 스튜디오가 운영하는 볼트 서버에 암호화해서 저장합니다. 꺼내는 방법은 두 가지입니다.
- 맥: 병호 님이 로그인한 세션을 씁니다. 세션은 쓸 때마다 연장되기 때문에, 매일 쓰는 동안에는 다시 로그인할 일이 없습니다.
- 클라우드: 클라우드 세션은 로그인할 사람이 없으니 서비스 토큰을 씁니다. 토큰 하나는 프로젝트 하나, 환경 하나만 읽을 수 있습니다. 기본은 읽기 전용이고, 만료되고, 언제든 취소할 수 있고, 쓸 때마다 기록이 남습니다.
꺼낸 시크릿은 셸에 남기지 않습니다. nst vault run -- 명령으로 실행하면 그 명령의 프로세스에만 환경 변수로 들어가고, 명령을 실행한 셸은 값을 보지 못합니다. 에이전트의 대화 기록에 키가 찍히지 않게 하려는 거예요.
토큰이 접근할 수 있는 범위를 검사하는 코드도 한 번 다시 썼습니다. 처음에는 라우터마다 미들웨어를 붙여서 검사했는데, 경로가 겹치는 라우터가 여러 개라 바깥쪽 미들웨어가 먼저 요청을 통과시키는 경우가 있었습니다. 지금은 요청 URL을 읽어 한 함수에서 판단합니다. 보안 경계가 라우터를 등록한 순서에 따라 달라지면 안 되니까요.
왜 자꾸 다시 로그인하라고 했을까
이번 달에 CLI 전체를 다시 리뷰하면서 가장 먼저 고친 문제입니다. 리뷰 에이전트 다섯이 각자 다른 영역을 맡았는데, 그중 셋이 같은 코드를 가리켰습니다.
- CLI는 세션 토큰이 만료되기 직전에 토큰을 갱신합니다.
- 에이전트는 명령 여러 개를 동시에 실행하고, 각 프로세스가 따로 갱신을 요청합니다.
- 볼트 서버는 1분에 받는 갱신 요청 수를 제한하니, 동시에 몰린 요청 일부가 거절됩니다.
- CLI는 거절 이유를 보지 않고 "다시 로그인하세요"를 보여 줬습니다. 토큰은 아직 유효한데도요.
고친 방법은 이렇습니다. 갱신은 파일 잠금을 잡은 프로세스 하나만 하고, 나머지는 잠금이 풀린 뒤 갱신된 파일을 읽습니다. 자격 증명 파일은 임시 파일에 쓴 뒤 한 번에 바꿔서, 반쯤 쓰인 파일을 읽는 일이 없게 했습니다. 서버가 토큰을 거부했을 때만 로그인을 요구하고, 요청 수 제한이나 서버 오류는 잠시 뒤 다시 시도합니다. 이 수정은 3.5.0에 들어갔고, 고치면서 로그인을 새로 묻는 경로는 하나도 추가하지 않았습니다.
사내 CLI를 에이전트와 같이 쓰고 있다면
먼저, 터미널이 없을 때의 동작을 확인해 보세요. 확인 창이 뜨는 명령을 파이프로 실행해 보면 됩니다. 그냥 진행된다면 에이전트도 그냥 진행합니다.
다음으로, 에러 메시지를 원인별로 나눠 보세요. 네트워크 오류, 서버 오류, 권한 오류가 같은 문장으로 나오면 에이전트는 그 문장이 시키는 대로만 움직입니다.
마지막으로, 실행 기록을 남겨 두면 좋습니다. 어떤 명령이 많이 쓰이는지 알아야 어디를 먼저 빠르고 안전하게 만들지 정할 수 있습니다. 저희는 기록을 보고 나서야 실행의 40%가 시크릿이라는 걸 알았습니다.
마치며
CLI를 에이전트가 쓰기 좋게 만드는 일은 생각보다 사람을 위한 일이기도 했습니다. 에이전트가 엉뚱한 메시지 때문에 멈추면, 결국 그 메시지를 읽는 사람은 병호 님이니까요.
어려웠던 건 크레이트를 나누는 일보다 순환을 찾는 일이었습니다. 어느 영역이 어디를 부르는지 한 번 그려 보기 전까지는, 왜 뗄 수 없는지조차 보이지 않았습니다.
다음 글에서는 앱 수십 개에서 나오는 데이터를 서버 없이 어떻게 모으는지 다룹니다. 사내 도구를 에이전트와 함께 쓰고 있다면 조금이나마 도움이 되었으면 합니다.
난테 스튜디오 아키텍처 시리즈의 3편입니다. 전체 목록은 1편에 있습니다.