배경
공유호스팅 스케줄러가 리소스 정책으로 두 번 차단됐다. 규칙을 찾아 지켰는데도 정책은 상대가 정하고 언제든 바뀐다. 그래서 고치는 대신 안 쓰기로 했다 — 외부 스케줄러가 우리 서버의 웹 엔드포인트를 호출하게 바꿨다.
★★★ 웹 요청은 스케줄러와 다른 리소스 풀이라 그 차단과 무관하다. 제약을 뚫은 게 아니라 제약이 없는 자리로 옮긴 것이다. 이 컬렉션의 막힌 것을 뚫는 것과 필요 없게 만드는 것은 다른 수단이다와 같은 형태다.
★★ 규칙을 다 지켜도 정책이 바뀌면 또 막히는 자원이라면, 준수가 아니라 이탈이 답일 수 있다. 판단 기준은 "그 규칙을 우리가 통제하는가"다.
★★ 그런데 따라오는 것이 셋 있다.
★★★ ① 타임아웃 상한 — "빨리 끝내기"가 아니라 "기다리지 않기"로 푼다
외부 스케줄러의 요청 타임아웃은 30초 고정이고 올릴 수 없다. 큐 작업은 수 분 걸린다. → 엔드포인트가 작업을 백그라운드로 던지고 즉시 응답한다(0.04초).
★★★ ⚠️ 그런데 즉시 응답은 조용한 실패를 만든다. 스케줄러는 200을 받고 성공으로 기록한다. 실제 작업은 실패했을 수 있다. 이 컬렉션의 "응답만 보고 완료로 적는다"와 정확히 같은 구조다 — 다만 이번엔 설계상 그렇게 만든 것이라 더 눈에 안 띈다.
★★ 호출 성공률은 아무것도 알려주지 않는다. → 작업 쪽이 완료 시각을 따로 기록하고, 그 기록의 신선도를 감시하라. "마지막 완료가 N분 전보다 오래됐으면 경보" — 감시해야 할 것은 호출이 아니라 완료다.
★★★ ② 같은 스크립트라도 호출 경로가 다르면 관측값이 다르다
프로세스 수를 보고 실행을 막는 게이트가 있었다. 웹에서 호출하니 평상시 관측값이 셸에서 재던 것의 두 배로 나왔다 — 셸 기준으로 맞춘 임계는 항상 막는 값이 됐다. 게이트를 전부 다시 잡아야 했다.
★★★ 같은 이름의 지표가 두 모집단을 세고 있었다. → 게이트 값 옆에 "어느 경로에서 잰 값인지"를 적어라. 이 컬렉션의 기준선에 모집단을 함께 저장하라와 같은 규칙이다.
⚠️ ★ 그리고 새 풀의 한계가 옛 한계와 같은 것인지 확인하라. 무관한 별개 한계인지, 같은 한계인데 세는 방식만 다른 것인지 — 둘은 전혀 다른 결론으로 이어진다.
⚠️ ★★★ ③ 외부 서비스에 묶였다 — 데드맨 스위치가 필요하다
주문 알림이 이제 외부 SaaS의 가용성에 묶여 있다. 그쪽이 멈추면 알림이 멈추고 우리는 아무 신호도 받지 못한다.
★★ "외부가 호출해 주는" 구조는 실패 방향이 침묵이다. → 내부에 최소한의 스케줄을 남겨 "N분간 외부 호출이 없으면 경보"를 두라. 우리는 보조 항목 몇 줄을 내부에 남겨 그 역할을 겸하게 했다.
⚠️ ★★★ 마지막으로 — 토큰을 URL 쿼리스트링에 넣지 마라
엔드포인트를 run.php?t=<토큰>&j=<작업> 형태로 만들기 쉽다. 그 토큰은 최소 네 곳에 평문으로 남는다.
Referer 헤더 — 그 페이지가 외부 리소스를 참조하면 함께 나간다★ POST 바디나 커스텀 헤더로 옮겨라. 대부분의 스케줄러 서비스가 둘 다 지원한다. 당장 어렵다면 최소한 작업별 토큰 분리 + 회전 주기 + 요청 IP 제한.
⚠️ ★ 그리고 그 엔드포인트는 웹 공개 경로에 있다. 디렉터리 리스팅이 꺼져 있는지, run.php.bak 같은 편집 잔재가 없는지 확인하라 — .php가 아닌 확장자로 남은 백업은 소스를 그대로 노출한다.