로컬 개발 서버는 가끔 성격이 이상한 자동차 같다.
방금 전까지 잘 달리던 차가 다음 랩에서는 갑자기 멈춘다. 엔진은 켜져 있다. 바퀴도 돈다. 그런데 관중석에서 보면 차가 움직이지 않는다. 개발자 버전으로 번역하면 이렇다.
“API는 떠 있는데 화면이 안 떠요.”
이번에 손본 것은 QueryPie 로컬 서버 시작/중지 스크립트다. 목표는 단순했다. 더 화려하게 만드는 것이 아니라, 매번 덜 흔들리게 만드는 것.
문제는 서버 하나가 아니었다
로컬 QueryPie는 혼자 뜨는 앱이 아니다. 작은 팀처럼 움직인다.
- API는 HTTP와 gRPC 포트를 연다.
- Engine은 별도 gRPC/HTTP 포트를 연다.
- ARiSA는 프록시 역할을 한다.
- CommandPie는 터미널/웹소켓 쪽을 담당한다.
- Front는 개발 서버로 뜬다.
- Envoy는 사용자가 접속하는 제품형 입구가 된다.
그러니까 http://127.0.0.1:18080이 안 뜬다고 해서 “프론트가 죽었네?”라고 바로 말할 수 없다. 실제로는 Front가 4001에서 멀쩡히 살아 있는데, Envoy가 거기까지 못 가는 경우도 있다.
이번에도 그랬다.
Front는 살아 있었다. API도 건강했다. 그런데 Envoy가 host.docker.internal을 IPv6 주소로 잡고, host의 Front dev server에 붙지 못했다. 사용자는 18080만 보니까 “화면이 안 뜬다”고 느끼지만, 내부에서는 마지막 릴레이 주자가 바통을 떨어뜨린 셈이다.
첫 번째 개선: 입구가 길을 잃지 않게 하기
Envoy는 Docker 컨테이너 안에서 돈다. 컨테이너 안에서 host 머신의 Front dev server를 찾아가야 한다. 이때 host.docker.internal이 어떤 주소로 풀리는지가 중요하다.
가끔 Docker Desktop 환경에서 이 이름이 IPv6로 먼저 잡힌다. 그런데 실제 연결은 되지 않는다. 그래서 Envoy는 이런 상태가 된다.
“저기요, Front가 저쪽에 있다면서요?”
그리고 503.
이번 개선에서는 Envoy의 host-backed cluster에 IPv4 우선 전략을 명시했다.
front_httpengine_grpccommandpie_http
핵심은 dns_lookup_family: V4_ONLY다.
이건 거창한 기능이 아니다. 하지만 로컬 런타임에서는 이런 작은 결정이 엄청 크다. 길 찾기 앱이 자꾸 막힌 골목을 안내한다면, 목적지가 아무리 가까워도 도착하지 못한다.
두 번째 개선: 다른 경기장에 주차하지 않기
QueryPie는 QCP 패치 워크트리로 자주 바뀐다. 오늘은 기본 querypie-mono, 내일은 특정 패치 워크트리에서 서버를 띄운다.
여기서 무서운 문제는 “현재 코드 경로”와 “acpx workspace”가 어긋나는 경우다.
예를 들어 스크립트는 A 워크트리를 보고 있는데, acpx는 B 워크스페이스를 바라보고 있을 수 있다. 그러면 MySQL/Redis 포트나 workspace offset이 어긋난다. 시작은 했는데 API가 엉뚱한 Redis를 찾거나, 마이그레이션이 다른 기준으로 돈다.
이런 문제는 로그를 보기 전까지 꽤 그럴듯해 보인다. 그래서 더 나쁘다.
이번 개선에서는 start/stop 스크립트가 먼저 확인한다.
- 내가 받은
QUERYPIE_ROOT는 실제 git checkout/worktree인가? - 현재
acpx workspace가 이 경로와 일치하는가? - 다르면 해당 worktree 이름으로 자동 보정을 시도한다.
- 그래도 안 맞으면 늦게 실패하지 않고 초반에 멈춘다.
이건 마치 피트 크루가 출발 전에 묻는 것과 같다.
“지금 우리 차 맞죠?”
세 번째 개선: 실패해도 블랙박스를 남기기
기존에는 마지막까지 성공해야 runtime-state.env가 의미 있게 남았다. 문제는 실제 장애가 가장 자주 나는 곳이 마지막 단계라는 점이다.
API도 떴고, Engine도 떴고, Front도 떴는데, Envoy readiness에서 실패한다. 그러면 전체 스크립트는 실패한다. 그런데 상태 파일은 비어 있거나 이전 상태라면 다음 사람이 다시 처음부터 추리해야 한다.
그래서 상태 파일을 단계별로 남기도록 바꿨다.
예를 들면 이런 식이다.
status=starting,stage=api_readystatus=starting,stage=front_readystatus=failed,stage=envoy_startstatus=ready,stage=ready
이제 실패해도 “어디까지 왔는지”가 남는다. 디버깅에서 이건 꽤 큰 차이다.
실패한 러너에게 “어디서 넘어졌어?”라고 물을 수 있게 된 것이다.
네 번째 개선: 실패한 무대의 로그만 바로 보여주기
서버 시작 실패 때 가장 귀찮은 일은 로그 찾기다.
API가 실패했는지, Front가 실패했는지, Envoy가 실패했는지에 따라 봐야 할 로그가 다르다. 사람은 알고 있지만, 매번 손으로 찾는 건 반복 노동이다.
그래서 실패 stage에 따라 관련 로그를 바로 보여주도록 했다.
- API 단계 실패 →
api.logtail - Engine 단계 실패 →
engine.logtail - Front 단계 실패 →
front.logtail - Envoy 단계 실패 → Docker 로그, raw Front check, Envoy check,
host.docker.internal해석 결과
이건 “로그를 많이 뿌리자”가 아니다. 필요한 순간에 필요한 로그만 보여주자는 쪽에 가깝다.
좋은 도구는 개발자를 Sherlock Holmes로 만들지 않는다. 그냥 현장 CCTV를 바로 틀어준다.
다섯 번째 개선: HTML만 보고 성공이라고 하지 않기
/login이 200이라고 해서 화면이 잘 뜬다는 뜻은 아니다.
HTML은 왔지만, 거대한 JS bundle이 Envoy를 통과하지 못하면 브라우저 화면은 여전히 하얗게 남을 수 있다. 사용자는 “화면이 안 떠요”라고 말한다. 서버는 “저는 200 줬는데요?”라고 말한다. 둘 다 맞는 말이라서 더 피곤하다.
그래서 Envoy readiness에 bundle check를 추가했다.
/main.bundle.js/vendors.bundle.js
이제 스크립트가 성공하려면 제품형 입구인 18080을 통해 HTML뿐 아니라 실제 화면을 만드는 주요 자산도 내려와야 한다.
이건 작은 차이지만, 사용자가 보는 성공 조건에 훨씬 가깝다.
여섯 번째 개선: stop도 더 정직하게 만들기
로컬 서버 중지는 생각보다 어렵다. pid file에 남은 프로세스도 있고, port를 점유한 orphan process도 있고, Docker container가 포트를 다시 잡는 경우도 있다.
이번에는 stop script도 개선했다.
QUERYPIE_ROOT가 실제 checkout/worktree인지 검증한다.- acpx workspace가 root와 맞는지 확인하고 보정을 시도한다.
- 프로세스를 죽이기 전에
ps로 어떤 프로세스인지 보여준다.
“죽였습니다”보다 좋은 메시지는 “이 프로세스를 죽였습니다”다.
특히 로컬 개발 환경에서는 실수로 다른 서버를 건드리지 않는 것이 중요하다. 포트만 보고 죽이는 스크립트는 강력하지만, 그래서 더 설명이 필요하다.
이번 개선의 핵심은 빠른 성공보다 빠른 실패다
좋은 start script는 단순히 빨리 성공하는 스크립트가 아니다.
좋은 start script는 세 가지를 잘해야 한다.
- 맞는 환경이면 빠르게 성공한다.
- 틀린 환경이면 초반에 실패한다.
- 실패하면 다음 행동을 알려준다.
이번 개선은 이 세 방향으로 움직였다.
Envoy가 길을 잃지 않게 했다. Workspace가 엇갈리지 않게 했다. 실패해도 상태를 남기게 했다. 그리고 200 OK라는 얕은 안심 대신, 실제 화면에 필요한 bundle까지 확인하게 했다.
배운 점
로컬 런타임 안정화는 대단한 알고리즘 문제가 아닐 때가 많다.
대부분은 이런 것들이다.
- 이름이 같은데 경로가 다른 workspace
- 떠 있는 것처럼 보이지만 연결되지 않는 port
- HTML은 오지만 JS가 오지 않는 화면
- 성공해야만 남는 state file
- 너무 늦게 드러나는 dependency 문제
이런 문제는 하나씩 보면 작다. 하지만 매일 반복되면 개발자의 집중력을 갉아먹는다.
그래서 로컬 서버 스크립트는 단순 자동화가 아니다. 팀의 리듬을 지키는 피트 크루다.
차가 빨리 달리게 하는 것도 중요하지만, 더 중요한 건 매번 같은 순서로 점검하고, 문제가 생기면 어디가 문제인지 바로 말해주는 것이다.
오늘의 결론.
로컬 서버가 흔들릴 때는 코드를 의심하기 전에 피트 크루를 의심해보자. 그리고 피트 크루에게 좋은 체크리스트와 블랙박스를 쥐여주자.
그러면 다음번 http://127.0.0.1:18080은 조금 덜 무섭다.