2026년 4월에 이 매거진은 「모든 서버가 앱이다」를 실었다. 앱을 아이콘과 스토어에서 떼어내 화면·도구·데이터 셋으로 환원하고, 서버는 이미 그중 둘을 갖고 있었으니 빠진 건 화면 한 겹뿐이라는 글이었다.
그 글의 진짜 주제는 등호가 아니라 그 다음이었다. 서버가 앱이 된다는 건 모든 능력이 화면에 노출된다는 뜻이 아니라, 무엇을 표면에 올리고 무엇을 잠가 둘지 결정하는 일이다. 카페 서버가 주문과 재고는 열고 매출 집계는 사장 화면에만 두고 데이터베이스 삭제는 어떤 도구로도 내놓지 않는다는 장면으로 그걸 그렸다.
그리고 글은 이렇게 끝났다 — "다음 글들에서 본다."
그 약속을 지금 갚는다. 문제는 그때 글이 딛고 선 발이 전부 서술이었다는 것이다. "존재하지 않는 버튼은 뚫을 수 없다"는 명제는 그럴듯하지만, 도구 목록이 실제로 잠겨 있다는 걸 보여야 성립한다. 보여주지 않으면 그건 주장이지 보안이 아니다.
그래서 이 글은 새 비전을 말하지 않는다. 먼저 한 주장을 실제로 돌아가는 것들에서 확인한다.
도구 목록이 곧 표면이다
먼저 가장 단순한 확인부터. 표면이 무엇인지는 서버에게 물어보면 나온다.
책상 위 STM32H723 보드에 붙어 tools/list 를 부르면 이렇게 답한다.
{"tools":[
{"name":"led.set","description":"Turn the on-board LED on or off",
"inputSchema":{"type":"object","properties":{"on":{"type":"boolean"}},"required":["on"]}},
{"name":"sys.info","description":"Report LED state and uptime",
"inputSchema":{"type":"object","properties":{}}}]}
둘뿐이다. 이 보드는 그것 말고도 할 수 있는 게 많다 — 플래시를 지우고, 클럭을 바꾸고, 부트로더로 넘어갈 수 있다. 그 능력들은 보드 안에 있되 목록에는 없다. 그리고 목록에 없으면 부를 방법이 없다. 화면 정의를 아무리 조작해도, 없는 이름은 호출되지 않는다.
이게 「모든 서버가 앱이다」가 말한 두 번째 경계의 가장 앙상한 형태다. 방어 코드가 아니다. 애초에 길을 내지 않은 것이다.
(이 응답은 실보드에서 받은 것이다. 그 사슬 전체는 보드가 자기 화면을 건넨다에 있다.)
결제 경로 — 안 만든 것이 세일즈 포인트가 되는 자리
더 흥미로운 건 그 선택이 약점이 아니라 강점이 되는 경우다.
카드 결제 단말에 POS 화면을 얹는 샘플을 만들었다. 매장 서버가 주문을 받고 요금을 세고 결제를 건다. 그런데 결제 핸들러가 하는 일은 이게 전부다.
final amount = unpaid.fold<int>(0, (sum, o) => sum + (o['price'] as int));
final result = await terminal.call('terminal.authorize', {'amount': amount});
카드를 읽지 않는다. 승인 여부를 판단하지 않는다. VAN에 접속하지 않는다. 얼마인지 세어서 묻고, 답을 기록한다. 그 사이에서 일어나는 모든 일은 인증받은 하드웨어 안에서 일어나고, 이 코드는 그 안을 한 번도 열지 않는다.
주장으로 끝내지 않기 위해 어댑터가 단말과 실제로 주고받은 줄을 그대로 싣는다.
=> {"id":2,"tool":"terminal.authorize","args":{"amount":11000}}
<= {"id":2,"ok":true,"result":{"approved":true,"approvalCode":"A4101","last4":"4242","brand":"SIM","amount":11000}}
응답에 없는 것을 봐 달라. 카드번호가 없다. 트랙 데이터가 없다. 승인 코드와 뒷자리 네 개뿐이다. 단말 종단이 그 이상을 내주는 도구를 갖고 있지 않기 때문이다 — 그리고 실제 결제기도 그 이상을 내주지 않는다.
여기서 4월 글이 못 본 것이 하나 드러난다. 그 글은 "내놓지 않음"을 위험을 줄이는 선택으로 그렸다. 실제로 만들어 보니 그건 절반이었다. 단말 제조사에게 이건 파는 이유다. 인증받은 보안 영역을 열지 않는다는 것이, 화면 축을 얹으면서 인증을 다시 받지 않아도 된다는 뜻이기 때문이다. 안 만들어서 안전한 게 아니라, 안 만들어서 팔린다.
(전체 사슬과 코드는 칩 하나에 붙이면, 붙인 기기가 단말이 된다에.)
판단을 도구 밖에 두기 — 4월에 예고했던 바로 그것
4월 글은 마지막 문단에서 두 사례를 예고했다. 하나는 영상을 한 프레임도 건드리지 않는 블랙박스, 다른 하나는 "진단은 사람에게 남기면서 진료의 흐름만 화면으로 정리하는 의사의 도구 — 판단의 권한을 도구 밖에 두는 사례" 였다.
그 형태를 공장 설비로 만들었다. 의료가 아닌 이유는 뒤에 적는다.
설비 서버가 기계 상태를 내주는 핸들러다.
// 서버는 사실과, 그 사실이 한계와 어떻게 비교되는지를 말한다.
// 기계가 "괜찮다"고는 말하지 않는다 — 그 단어는 점검표를 든
// 사람의 것이다.
final overdue = (m['runHours'] as int) > (m['serviceEveryHours'] as int);
final vibrationOver =
(m['vibrationMm'] as num) > (m['vibrationLimitMm'] as num);
return _json({
'id': id, ...m,
'serviceOverdue': overdue,
'vibrationOverLimit': vibrationOver,
});
serviceOverdue: true 는 사실이다. safe: false 는 판단이다. 서버는 앞엣것만 내고, 뒤엣것을 낼 도구를 갖고 있지 않다.
그 위에 LLM을 얹었다. 여기가 4월에는 상상할 수 없던 지점이다 — 도구를 고를 줄 아는 것이 붙었을 때 "안 만든 것"이 여전히 안 만들어진 채로 있는가.
답은 로그에 있다.
Q: how is CONV-03 doing?
grounded=true calls=1 equipment.read(id: CONV-03) -> overdue=true vibrationOver=true
A: CONV-03 needs attention — service is overdue (9310 h against a 8000 h interval)
and vibration is above limit (5.2 mm against 4.5 mm).
Q: what is the weather like?
grounded=false calls=0
A: I can look up machines, their current readings, and the plant checklist
for a machine type. Ask me about one of those.
두 번째가 핵심이다. 공장에 답할 도구가 없는 질문에 대해 도구 호출이 0건이다. 없는 능력을 만들어 내지 않았다. 그리고 그 사실이 화면에 뜬다 — 근거 0건인 답은 다른 얼굴로 표시된다.
이건 4월 글의 명제를 한 칸 확장한다. "존재하지 않는 버튼은 뚫을 수 없다"에는 이제 "그리고 지어낼 수도 없다"가 붙어야 한다. 도구 목록이 표면을 정의하는 것은 사람이 누를 때만이 아니라, 모델이 고를 때도 그렇다.
(그 배선과 근거 표시 구조는 대답 옆에 근거를 붙인다에.)
경계는 위에서만 그어지지 않는다
4월 글은 경계를 둘로 갈랐다. 받는 쪽(런타임이 정의를 불신하고 정해진 조합만 허용)과 내보내는 쪽(애초에 도구로 안 만듦).
만들어 보니 셋째가 있었다. 하드웨어 자신이다.
온실 릴레이 노드의 C 코드다.
/* The relay clamps its own range. Whatever the rule upstream
* decided, the hardware still refuses to exceed itself. */
if (v < 0) v = 0;
if (v > 100) v = 100;
n->value = v;
위에서 규칙이 무엇을 결정했든, 릴레이는 자기를 넘어서지 않는다. 결제 단말 시뮬도 같은 자리에 같은 것을 갖고 있다.
if (!json_num(line, "amount", &amount) || amount <= 0) {
printf("{\"id\":%ld,\"ok\":false,\"error\":\"amount must be positive\"}\n", rid);
}
이게 왜 별개의 경계인가. 앞의 둘은 소프트웨어의 선의에 기댄다. 런타임이 제대로 검증하기를, 서버 개발자가 위험한 도구를 안 만들기를. 셋째는 그렇지 않다. 장치가 자기 물리적 한계를 스스로 지킨다. 서버가 뚫려도, 런타임이 속아도, 릴레이는 100을 넘기지 않는다.
안전은 규칙의 선의가 아니라 릴레이의 성질이어야 한다. 4월 글의 경계 이분법에 이 한 줄을 더한다.
(온실 사슬은 센서를 갈아 끼워도 제어는 그대로에.)
표면과 능력이 갈라지는 자리
번들앱을 만들다 우연히 가장 선명한 그림이 나왔다.
무인매장 점주 앱을 JSON 네 개로 만들고, 그 JSON을 고쳐 세탁소로 바꿔 봤다. 라벨은 바뀌었다 — LAUNDRY — 24H, Machines running. 그런데 품목은 여전히 아이스크림이었다. Cone vanilla, Bar mint, Tub 474ml.
처음엔 데모의 흠으로 보였다. 다시 보니 이 글의 논지가 그림 하나로 찍힌 것이었다. 번들이 가진 것은 표면이고, 능력은 서버가 갖고 있다. 화면을 아무리 고쳐도 품목이 안 바뀌는 건 실패가 아니라 경계가 지켜지고 있다는 증거다.
4월 글은 이 갈라짐을 "무엇을 도구로 고르느냐가 얼굴을 결정한다"로 표현했다. 반쯤 세탁소가 된 화면은 그 문장의 반대편을 보여 준다 — 얼굴을 바꿔도 능력은 안 따라온다.
(번들의 실체는 폴더 하나가 앱이다에.)
4월 글에서 물려야 할 것들
같은 글을 다시 실으면서, 그때 잘못 적은 것을 그대로 두면 재작성가 아니다.
"서버를 만들 줄 알면 이미 그 언어의 8할을 안다"는 문장을 뺀다. 재 본 적 없는 숫자다. 실제로 만들어 보니 서버 개발자가 화면 정의에서 처음 만나는 것들이 있었다 — 상태 바인딩, 페이지마다 따로인 초기값, 액션이 도구를 부르는 형태. 어렵진 않지만 "이미 안다"는 아니다. 측정하지 않은 비율을 자신감의 근거로 쓰지 않는다.
"약속된 언어"의 이름을 밝힌다. 4월 글은 끝까지 무명으로 뒀다. 열려 있어야 성립한다고 주장하는 글이 그 열린 규약의 지시 대상을 흐리면 검증할 수가 없다. 그 규약은 MCP(Model Context Protocol)이고, 이 시리즈의 모든 샘플이 그것으로 말한다. 위에 실린 tools/list 응답과 initialize 왕복이 그 규약의 실제 바이트다.
카페 장면은 예화였다고 명시한다. 4월 글의 그 장면은 "~라고 하자"로 시작하는 사고실험인데, 구체적으로 그려져 관측처럼 읽혔다. 이 글에서 카페 자리를 대신하는 것들 — 매장 서버, 설비 서버, 온실, 보드 — 은 전부 실제로 돌린 것이고, 각각 어디서 왔는지 링크로 달았다. 상상과 관측을 같은 서체로 쓰지 않는다.
"지난 Vol들에서 거듭 봤다"를 고쳐 쓴다. 4월 글은 패널·칩·계측기에서 서버가 화면을 정의하는 걸 봤다고 적었다. 그 편들은 당시 실 렌더도 실행 로그도 없었다. 본 게 아니라 그렇게 적었을 뿐이다. 지금은 다르다. 이 글이 인용하는 것은 전부 실행 로그와 실렌더 캡처가 있는 편들이다.
이 샘플이 안 한 것
4월 글의 정직 섹션 셋은 그대로 유효하다. 등호는 자동이 아니고, 모든 것이 화면을 가질 필요는 없고, 기존 시스템이 곧장 변신하지 않는다. 만들어 보니 전부 맞았다.
여기에 이번에 알게 된 것을 더한다.
이 글이 검증한 것은 "안 만든 능력은 안 닿는다"까지다. 그건 도구 목록이 표면이라는 구조의 결과이고, 위 로그들이 그것을 보인다. 그러나 "그러므로 안전하다"는 더 큰 명제이고 이 글은 그걸 증명하지 않았다. 인증·권한·전송 보안·서버 자체의 침해는 전부 이 글 밖이다. 목록에 없는 것이 안 불린다는 사실과, 시스템이 안전하다는 것은 다른 크기의 주장이다.
의료를 피한 이유. 4월 글이 예고한 "의사의 도구"를 공장 설비로 바꿔 만들었다. 판단 권한을 도구 밖에 둔다는 구조는 같지만, 임상 판단은 틀렸을 때의 무게가 다르고, 그 무게를 감당할 검증을 이 시리즈가 하지 않았다. 구조가 같다고 영역이 같지는 않다. 의료 사례는 그 검증을 갖춘 뒤에 별도로 다룬다.
블랙박스 사례는 아직 안 갚았다. 4월 글이 예고한 둘 중 영상 쪽 — 영상을 한 프레임도 건드리지 않으면서 로그와 설정만 여는 사례 — 은 이 글에서 다루지 않았다. 결제 경로 사례가 같은 형태이긴 하나 같은 사례는 아니다. 미회수로 적어 둔다.
그리고 이 글에는 새 샘플이 없다. 여기 인용된 로그와 코드는 전부 앞선 다섯 편에서 만든 것이다. horizon 글의 일은 새로 짓는 게 아니라 이미 지은 것을 딛고 서는 것이고, 4월 글의 문제는 딛을 것이 없는데 딛는 척했다는 것이었다.
다시, 무엇을 내놓지 않을 것인가
4월 글의 마지막 문단을 다시 쓴다. 그때는 이렇게 끝냈다 — "무엇을 도구로 만들지 않을지가, 무엇을 만들지만큼 중요한 설계가 되는 자리를 다음 글들에서 본다."
봤다. 그리고 본 것을 정리하면 이렇다.
- 도구 목록이 곧 표면이다. 보드는 둘만 내놓았고, 나머지 능력은 부를 이름이 없다.
- 안 만든 것이 팔리는 이유가 된다. 결제 보안 경로를 안 여는 것이 제조사에게는 인증을 다시 안 받아도 된다는 뜻이다.
- 모델이 붙어도 없는 도구는 안 생긴다. 답할 도구가 없는 질문에 호출 0건, 그리고 그 사실이 화면에 표시된다.
- 경계는 셋이다. 받는 쪽 런타임, 내보내는 쪽 설계, 그리고 자기 한계를 스스로 지키는 하드웨어.
- 표면을 바꿔도 능력은 안 따라온다. 반쯤 세탁소가 된 화면이 그 선을 보여 준다.
서버가 앱이 되어도 건드리면 안 되는 것은 그대로 둔다 — 4월에 그렇게 적었다. 지금은 거기에 한 줄을 붙일 수 있다. 그렇게 두었다는 것을, 도구 목록과 통신 로그로 보일 수 있다.
보이지 않는 경계는 경계가 아니라 약속이다. 약속은 지켜지지 않을 수 있고, 지켜졌는지 확인할 방법이 없으면 지켜졌다고 말할 수도 없다.
makemind.dev 「탐구」 — 그때 미룬 약속을 실행 로그와 실보드 응답으로 갚았다.