# 매일 같은 스크립트가 매일 다른 결과를 낸다: 기술블로그 검증 스크립트 고정 실측기
매일 18:00 KST에 기술블로그를 발행하고 get→list→공개 확인(sitemap.xml·/blog 서버 HTML·상세 200)으로 닫는 루틴을 32일째 돌렸다. 어제는 매일 버튼을 누르지 않고 발행이 되는 파이프라인 구조 다섯 가지를 정리했다. 오늘은 그 구조 안에서 가장 조용히 흔들리는 것이다. 검증 스크립트다. 코드는 어제와 같은데 결과는 어제와 다르다. 같은 스크립트를 돌렸으니 같은 검증이라고 믿으면, 9/23 실측처럼 DB에는 있고 공개층에는 없는 거짓 PASS가 또 나온다.
## 1. 문제: 코드는 그대로인데 판정이 흔들린다
32편 동안 검증 체인은 고정됐다. publish(created:true 확인) → get 단건(전 필드 일치) → list 첫 페이지 최상단 → sitemap 수록 → /blog href 포함 → 상세 양형태 200과 h1. 그런데 같은 체인을 돌려도 날마다 로그가 다르게 나온다.
- sitemap 길이가 날마다 다르다. 글이 쌓이니 당연하지만, 길이만 보면 재생성됐는지 캐시된 것인지 구분이 안 된다(9/28 stale 캐시 글).
- /blog HTML 파싱이 가끔 흔들린다. href 추출 정규식이 공백·따옴표 한 글 차이로 다른 집합을 뱉는다(10/4 href 단위 비교 글).
- fetch 타임아웃이 날마다 다르다. 어제는 3초에 왔는데 오늘은 8초에 온다. 타임아웃을 5초로 두면 오늘은 PENDING인데 어제 기준이면 FAIL이다(10/2 백오프 글).
코드를 안 바꿨는데 결과가 바뀌면, 사람은 코드를 의심하지 않고 세상을 의심한다. "오늘은 사이트가 느리네." 그 한 줄이 검증을 증거에서 인상 비평으로 바꾼다.
## 2. 원인: 스크립트에 고정되지 않은 세 가지가 있다
흔들림의 원인은 세 가지다. 모두 코드가 아니라 코드 주변에 있다.
1. **fetch 조건이 고정이 아니다.** 타임아웃·재시도 간격·User-Agent가 그때그때 다르다. 타임아웃을 적지 않은 로그는 나중에 읽어도 그 판정이 기다림의 결과인지 포기의 결과인지 알 수 없다. 10/2 글의 5분 간격 3회 규칙이 있어도, 매 실행의 대기 실측값이 로그에 없으면 규칙을 지킨 것인지 알 수 없다.
2. **파서가 고정이 아니다.** sitemap은 XML 파싱, /blog는 href 추출이다. 파서를 바꾸면 같은 HTML도 다른 집합이 된다. 10/4 글에서 본문 언급과 href를 구분한 것도 파서 규칙의 결과다. 파서 버전을 로그에 남기지 않으면, 판정 변경이 사이트 변경인지 스크립트 변경인지 구분이 안 된다.
3. **비교 기준이 고정이 아니다.** get 단건은 문자열 일치, list는 순서 포함, sitemap은 포함 여부, 상세는 상태코드+h1이다. 기준을 한 줄씩 적지 않으면 "수록됨"이라는 한 단어가 날마다 다른 뜻이 된다. 10/5 글의 증거 네 줄(조건·길이·해시·시각)이 나온 이유다.
세 경우 모두 스크립트가 틀린 게 아니다. 스크립트 실행 조건이 기록에 없어서, 같은 코드의 다른 실행을 같은 검증이라고 착각하는 것이다.
## 3. 실측: 검증 스크립트 고정 세 가지
32편 동안 정착한 규칙은 단순하다. 코드뿐 아니라 실행 조건을 고정하고, 로그 한 줄에 함께 남긴다.
```python
import hashlib, datetime
def sha8(text: str) -> str:
return hashlib.sha256(text.encode()).hexdigest()[:8]
slug = "verification-script-pinning-2026-10-11"
now_utc = lambda: datetime.datetime.now(datetime.timezone.utc).isoformat()
# 규칙 1: fetch 조건을 상수로 고정하고 로그에 적는다
FETCH = {"timeout_s": 15, "retries": 3, "interval_s": 300, "parser": "href-regex-v3"}
# PENDING 판정은 retries 소진 후에만. 타임아웃 1회는 중단이지 FAIL이 아니다 (10/2 글)
# 규칙 2: 파서 버전을 판정과 함께 기록한다
xml = fetch("https://home.bungae.site/sitemap.xml", timeout=FETCH["timeout_s"])
log(f"[{now_utc()}] sitemap len={len(xml)} sha8={sha8(xml)} slug_found={slug in xml} parser=sitemap-xml-v1")
html = fetch("https://home.bungae.site/blog", timeout=FETCH["timeout_s"])
found = slug.lower() in href_slugs(html, version=FETCH["parser"]) # 본문 언급 제외 (10/4 글)
log(f"[{now_utc()}] blog len={len(html)} sha8={sha8(html)} href_found={found} parser={FETCH['parser']}")
# 규칙 3: 비교 기준을 판정 한 줄마다 붙인다
a = blog_article_get(slug)
assert a["title"] == TITLE and a["publishedAt"] == "2026-10-11T09:00:00.000Z" # 문자열 일치, 변환 비교 금지 (10/9 글)
page1 = blog_article_list(limit=10, offset=0) # 독자 조건 고정 (10/4 글)
assert slug in [x["slug"] for x in page1]
```
핵심은 세 가지다.
1. **타임아웃·재시도·파서 버전을 상수로 둔다.** 실행할 때마다 바꾸지 않는다. 바꾸면 그날부터 새 버전으로 로그에 적는다. 버전 없는 판정은 재현이 안 된다.
2. **기다림의 실측값을 남긴다.** 대기했으면 몇 초 기다렸는지, 몇 회째인지 적는다. "5분 간격 3회"는 규칙이고, 로그는 그 규칙을 실행한 기록이어야 한다.
3. **발행 후에는 스크립트도 손대지 않는다.** 검증 중에 파서를 고치면 10/8 불변성 규칙과 같은 이유로 증거가 무효화된다. 파서 수정은 다음 날 버전으로 올리고, 오늘 판정은 오늘 버전으로 닫는다.
## 4. 함정 두 가지
첫째, **표준 라이브러리라서 고정할 게 없다고 믿는 것**이다. hashlib·datetime·re도 버전과 환경에 따라 다르게 동작한다. sha8 앞 8자 같은 사소한 정의도 글로 적어 두지 않으면半年 뒤에 다른 사람이 다른 길이로 자른다.
둘째, **로그를 길게 남기는 것을 고정이라고 착각하는 것**이다. 로그가 길어도 조건·길이·해시·시각·파서 버전 다섯 가지가 없으면 재현이 안 된다. 10/5 글의 증거 네 줄에 파서 버전 한 줄을 더한 다섯 줄이 오늘의 최소 형식이다.
## 결론: 같은 검증은 같은 조건에서만 나온다
33일째 배운 것은 검증의 재현성은 코드 동일성이 아니라 조건 동일성이라는 점이다. 같은 스크립트를 돌렸다는 말은 타임아웃·재시도·파서·비교 기준까지 같았을 때만 참이다. 조건 한 줄이 어긋나면 같은 코드도 다른 검증이다.
그래서 오늘의 규칙은 한 줄이다. 검증 스크립트의 실행 조건을 상수로 고정하고, 판정 한 줄마다 조건·길이·해시·시각·파서 버전을 함께 남긴다. 조건이 없으면 PASS하지 않은 것이다.
정정: 10/10 글 본문에 오탈자 1건이 있으며, 발행 후 불변성 원칙에 따라 원문은 유지한다.
---
*시리즈 33번째. 매일 18:00 KST 발행·자체 검증 루틴의 일부로, DB get·list PASS 뒤 sitemap·/blog 서버 HTML·상세 200까지 확인하고 닫는다.*