presearch 는 매일 아침 08시(KST)에 검색량과 주가를 모아 BigQuery 에 적재한다. Cloud Scheduler 가 Cloud Run Job 을 깨우는 구조다. 그 수집이 멈췄다.
Table of contents
Open Table of contents
상황 — 게이트가 막았다
로그에 남은 에러는 이랬다.
표시 검색량 목표일 관측 커버리지 부족: target=2026-07-26 fresh=0/400
400개 종목 중 목표일 데이터를 확보한 게 0개라는 뜻이다. 커버리지가 부족하면 화면에 반쪽짜리 데이터를 올리지 않고 멈추도록 게이트를 걸어뒀는데, 그 게이트가 작동한 것이다.
시도 — 인프라부터 의심했다
처음 떠올린 건 인프라였다. Cloud Run 콜드스타트로 작업이 중간에 끊겼거나, 네이버 API 가 일시적으로 죽었거나.
둘 다 아니었다. 네이버 API 를 직접 호출해보니 응답은 정상이었고, 최신 발행일이 07-25 까지였다. 내가 요구한 07-26 데이터는 애초에 존재하지 않았다.
원인 — 발행 지연을 달력으로 계산했다
코드는 목표일을 “달력상 어제” 로 잡는다. 오늘이 27일이면 26일 데이터를 달라고 한다. 문제는 네이버 데이터랩의 발행이 하루 남짓 밀리고, 주말에는 더 밀린다는 점이다. 월요일 아침에 일요일 데이터를 찾으면 그건 아직 세상에 없다.
정리하면 이렇다.
- 게이트는 설계대로 작동했다. 버그가 아니었다.
- 틀린 건 “어제 데이터는 오늘 아침에 있다” 는 내 가정이었다.
- 게다가 목표일이 매일 하루씩 앞서나가므로 자가 회복이 안 된다. 하루 실패하면 다음 날도 실패한다. 기다려서 풀리는 종류의 고장이 아니었다.
이 세 번째가 제일 아팠다. 재시도로 넘어갈 수 있는 실패였다면 아침에 알아채지도 못했을 것이다.
두 번째 핫픽스 — 종목마다 갈려서 발행된다
첫 번째 핫픽스를 배포하고 첫 실행에서 또 터졌다. 이번엔 네이버가 종목마다 다른 날짜까지 발행하는 경우였다. 전체가 07-25 까지 오는 게 아니라, 어떤 종목은 25일까지, 어떤 종목은 24일까지 온다.
이 케이스를 첫 핫픽스 때 이미 떠올렸었다. 그런데 아직 관측한 적 없는 가설이라 백로그로 미뤘다. 미룬 지 몇 시간 만에 프로덕션에서 만났다.
못 고친 것 — 스냅샷 로딩 37초
API 가 스냅샷을 읽는 데 걸리는 37초 자체는 못 줄였다.
BigQuery 6개 테이블을 직렬로 SELECT * 전량 조회한다. 주가 테이블만 약 12만 행이고, 컬럼 프루닝도 기간 프루닝도 없다. 구조를 손대야 하는 일이라 이번엔 범위를 넘어섰다.
대신 캐시 전략으로 사용자가 그 37초를 기다리지 않게만 만들었다. 캐시 TTL 을 데이터 갱신 주기(하루)에 맞추고, 기동 시 프리로딩과 stale-while-revalidate 를 넣었다. 결과는 /api/meta 첫 응답 33.6초 → 0.58초 (로컬 macOS curl 실측).
로딩 자체는 백로그로 남겼다. 이게 풀려야 min-instances 를 0으로 내려 비용을 줄이는 선택지가 생긴다. 지금은 느린 기동을 인스턴스를 띄워두는 돈으로 가리고 있는 셈이다.
이번에 처음 알게 된 것
주제가 흩어져 있지만 다시 밟을 만한 것들이라 같이 적어둔다.
1. gcloud run services describe 요약과 실제 적용값이 다르다. 요약에는 이렇게 보였다.
Scaling: Auto (Min: 0, Max: 20)
그런데 실제 적용값은 리비전 템플릿의 Min: 1, Max: 3 이었다. 요약만 보고 “min-instances 가 0이구나” 라고 판단하면 정반대로 간다.
2. Apple Silicon 에서 그냥 빌드하면 Cloud Run 이 거부한다. --platform linux/amd64 를 빼면 arm64 이미지가 나오고, 배포 단계에서 막힌다.
docker build --platform linux/amd64 -t <image> .
3. gcloud --set-env-vars 에 쉼표가 든 값을 넣을 때는 이스케이프가 아니라 대체 구분자를 쓴다. \, 로 이스케이프하면 파서가 거부한다. ^@^ 같은 대체 구분자를 앞에 붙여야 통과한다.
gcloud run deploy <svc> --set-env-vars "^@^KEY1=a,b@KEY2=c"
4. 분기 커버리지 100% 인 코드에서 동시성 결함 3개가 나왔다. 커버리지는 “어느 줄이 실행됐나” 를 세는 지표이고, “어떤 순서로 얽혔나” 는 세지 않는다. 100% 를 채웠다고 동시성이 검증된 건 아니다.
회고 — 두 가지 판단이 틀렸다
하나. 발생 확률을 과소평가했다. 종목별 발행 갈림을 백로그로 미룬 근거는 “실패 비용이 낮다” 였다. 그건 맞았다. 하지만 발생 확률을 같이 보지 않았다. 실패 비용이 낮아도 확률이 높으면 지금 처리하는 게 싸다. 다음부터는 비용과 확률을 같이 적어두려 한다.
둘. 테스트를 믿는 방식이 틀렸다. 문서에서 환경변수가 빠지는 사고를 막으려고 테스트를 걸어뒀고, “이건 테스트가 잡는다” 고 판단했다. 실제로 그 줄을 주석 처리해봤더니 테스트가 통과했다.
막으려던 사고에 대해 거짓 초록불을 주는 테스트였다. 이건 없느니만 못하다. 없으면 최소한 불안해서 손으로 확인하는데, 있으면 확인을 안 하게 된다.
이후로는 방어 장치를 만들면 실제로 그 사고를 재현해서 빨간불이 뜨는지 확인한다. 테스트를 작성한 게 아니라 테스트가 작동하는지를 확인하는 단계를 하나 더 두는 것이다.
더 공부해볼 것
- BigQuery 비용·성능 최적화 —
SELECT *를 피하고 필요한 컬럼만, 파티션·클러스터링으로 스캔 범위를 줄이는 방법. 37초를 줄이려면 여기부터다. (Optimize query computation · Control costs) - Cloud Run 최소 인스턴스와 콜드스타트 트레이드오프 —
min-instances를 1로 두고 지불하는 비용 대 0으로 내리고 감수하는 지연. 어느 지점에서 갈리는지 계산해보고 싶다. (Minimum instances) gcloud인자 이스케이프 규칙 — 대체 구분자가 왜 필요한지, 어떤 플래그에 적용되는지. (gcloud topic escaping)- 동시성 결함을 잡는 테스트 — 커버리지로는 안 잡히는 race 를 어떻게 재현 가능한 테스트로 만드는가. 결정론적 스케줄링이나 반복 실행 전략이 있는지 확인 필요.
- 뮤테이션 테스팅 — 거짓 초록불을 자동으로 찾아내는 접근. 코드를 일부러 망가뜨렸을 때 테스트가 실패하는지 검사한다. (mutmut)
- 외부 데이터의 신선도를 가정하지 않는 설계 — 목표일을 달력으로 계산하는 대신 “제공자가 실제로 발행한 최신 날짜” 를 먼저 조회하는 쪽이 맞아 보인다. 다만 종목별로 발행일이 갈리는 상황에서 어디까지를 정상으로 볼지는 아직 정리가 안 됐다. (네이버 데이터랩 검색어 트렌드 API)