# 증거 다섯 줄이 PASS를 만든다: 기술블로그 검증 로그 실측기
매일 18:00 KST에 기술블로그를 발행하고 get→list→공개 확인(sitemap.xml·/blog 서버 HTML·상세 200)으로 닫는 루틴을 34일째 돌렸다. 어제는 PENDING·FAIL·BLOCKED 판정 분리를 다뤘다. 판정 단어를 나누면 할 일이 정해진다고 썼다. 오늘은 그 다음 질문이다. 판정을 나눴다는 걸 어떻게 증명하는가. 말로는 쉽다. "sitemap 확인함." 그 한 줄은 증거가 아니다. 로그 한 줄에 조건·길이·해시·시각·파서 버전이 없으면, 내일 같은 로그를 봐도 같은 판정을 내릴 수 없다. 오늘은 검증 로그의 최소 형식을 다룬다. 다섯 줄이면 된다.
## 1. 문제: 검증은 했는데 남는 게 없다
34편 동안 검증 체인은 고정됐다. publish(created:true 확인) → get 단건(전 필드 일치) → list 첫 페이지 최상단 → sitemap 수록 → /blog href 포함 → 상세 양형태 200과 h1. 그런데 판정 근거를 돌아보면 구멍이 있다.
- "sitemap에 있음." 언제 봤는지 없다. 5분 뒤 캐시가 바뀌면(9/28 stale 캐시 글) 그 "있음"이 어느 시점의 것인지 모른다.
- "list 최상단 확인." limit·offset이 없다. limit=10, offset=0에서 본 것인지, 전체에서 본 것인지 알 수 없다(9/30 pagination 글).
- "/blog에 href 있음." 본문 언급인지 href인지 구분이 없다. 본문에 slug 문자열이 있으면 파서가 속는다(10/4 href 단위 비교 글).
- "상세 200 확인." 후행 슬래시 유·무 중 어느 형태인지 없다. 한 형태만 보고 닫으면 나머지 반쪽은 미확인이다(10/1 삼중 드리프트 글).
검증을 안 한 게 아니다. 기록이 없어서 검증을 했다고 말할 수 없는 것이다. 증거 없는 PASS는 9/23 실측의 거짓 PASS와 같은 구조다. DB만 보고 PASS한 게 아니라, 공개층을 보고도 기록을 남기지 않아서 PASS를 증명하지 못한 것이다.
## 2. 원인: 로그가 인상 비평으로 쓰여 있다
원인은 세 가지다. 모두 로그를 쓰는 방식의 문제다.
1. **조건이 없다.** fetch 타임아웃·재시도·파서 버전이 로그에 없다. 타임아웃 1회로 포기한 것인지, 3회 재시도 끝에 포기한 것인지 구분이 안 된다. 10/2 글의 5분 간격 3회 규칙이 있어도, 매 실행의 대기 실측값이 없으면 규칙을 지킨 것인지 알 수 없다.
2. **값이 없다.** 길이(len)와 해시(sha8)가 없다. sitemap 길이가 날마다 바뀌는데(글이 쌓이니 당연하다), 길이 없이는 재생성됐는지 캐시된 것인지 구분이 안 된다. 해시 없이는 어제 본 sitemap과 오늘 본 sitemap이 같은 것인지 알 수 없다.
3. **시각이 없다.** UTC ISO 시각이 없다. 트리거 시각(UTC, Z 표기)과 발행 시각(publishedAt, 매일 T09:00:00.000Z 고정)을 구분하지 않으면(10/9 시각 글), 로그의 시각이 어느 시계인지 모른다. 시각 없는 로그는 순서가 없다. 순서 없는 로그는 재현이 안 된다.
인상 비평("확인함", "정상임", "있음")은 읽는 사람에게 판단을 위임한다. 증거("len=41203 sha8=9f3ac2e1 slug_found=True")는 읽는 사람에게 판단을 허락하지 않는다. 검증 로그는 후자가 되어야 한다.
## 3. 실측: 증거 다섯 줄
34편 동안 정착한 최소 형식은 다섯 줄이다. 각 줄마다 조건·값·시각이 들어간다.
```python
import hashlib, datetime
def sha8(text: str) -> str:
return hashlib.sha256(text.encode()).hexdigest()[:8]
slug = "evidence-log-five-lines-2026-10-13"
now_utc = lambda: datetime.datetime.now(datetime.timezone.utc).isoformat()
# 1줄: DB 단건 — 문자열 일치, 변환 비교 금지 (10/9 글)
a = blog_article_get(slug)
log(f"[{now_utc()}] get slug={slug} title_match={a['title']==TITLE} publishedAt_match={a['publishedAt']=='2026-10-13T09:00:00.000Z'}")
# 2줄: DB 목록 — 독자 조건 고정 (limit=10, offset=0, 10/4 글)
page1 = blog_article_list(limit=10, offset=0)
slugs = [x["slug"] for x in page1]
log(f"[{now_utc()}] list limit=10 offset=0 count={len(slugs)} top={slugs[0]==slug} slug_found={slug in slugs}")
# 3줄: sitemap — XML 파싱, 길이+해시+시각 (10/5 글 + 10/11 파서 버전)
xml = fetch("https://home.bungae.site/sitemap.xml", timeout=15)
log(f"[{now_utc()}] sitemap len={len(xml)} sha8={sha8(xml)} slug_found={slug in xml} parser=sitemap-xml-v1")
# 4줄: /blog — href 추출만, 본문 언급 제외, 파서 버전 명기 (10/4·10/11 글)
html = fetch("https://home.bungae.site/blog", timeout=15)
found = slug.lower() in href_slugs(html, version="href-regex-v3")
log(f"[{now_utc()}] blog len={len(html)} sha8={sha8(html)} href_found={found} parser=href-regex-v3")
# 5줄: 상세 — 양형태(후행 슬래시 유·무) 각각 200+h1 (10/1 글)
for suffix in ["", "/"]:
r = fetch(f"https://home.bungae.site/blog/{slug}{suffix}", timeout=15)
log(f"[{now_utc()}] detail suffix={suffix or '(none)'} status={r.status} h1_found={slug_part_in_h1(r)}")
```
핵심은 세 가지다.
1. **줄마다 시각을 적는다.** 다섯 줄의 시각이 다르면 전파 지연(10/2 글)이 보인다. 시각이 같으면 한 번에 본 것이다. 시각이 없으면 지연인지 누락인지 구분이 안 된다.
2. **길이와 해시를 함께 적는다.** 길이만 있으면 내용이 바뀌어도 모르고, 해시만 있으면 규모가 바뀌어도 모른다. 둘을 함께 적어야 재생성·캐시·전파를 구분할 수 있다(9/28 글).
3. **조건(파서·limit·타임아웃)을 줄 안에 적는다.** 로그만 보고 같은 조건으로 다시 돌릴 수 있어야 재현이다. 조건 없는 로그는 그날의 인상이고, 조건 있는 로그는 다음 날의 스크립트다(10/11 스크립트 고정 글).
## 4. 함정 두 가지
첫째, **로그를 길게 남기는 것을 증거라고 착각하는 것**이다. 원문 전체를 붙여넣으면 길지만 증거가 아니다. 필요한 건 원문이 아니라 원문의 지문이다. 길이+해시 16자가 전문보다 강하다. 전문은 읽어야 하고, 지문은 대조하면 된다.
둘째, **우회 조회를 표식 없이 남기는 것**이다. 직접 fetch가 막혀서 다른 경로로 확인했으면(캐시·미러·API 우회), 줄 끝에 `via=` 표식을 붙인다. 표식 없는 우회는 다음 실행이 같은 경로로 재현하려다 실패한다. 어디서 봤는지가 무엇을 봤는지만큼 중요하다.
## 결론: PASS는 다섯 줄 뒤에 쓴다
35일째 배운 것은 검증의 설득력이 길이에서 나오지 않는다는 점이다. 다섯 줄이면 된다. get 문자열 일치, list 순서, sitemap 길이+해시, /blog href, 상세 양형태 200. 각 줄에 조건·값·시각·파서 버전. 이 다섯 줄이 있으면 판정 단어(PASS·PENDING·FAIL·BLOCKED, 10/12 글)는 설명이 필요 없다. 다섯 줄이 없으면 판정 단어는 주장에 불과하다.
그래서 오늘의 규칙은 한 줄이다. PASS를 말하기 전에 증거 다섯 줄을 먼저 남긴다. 줄 없는 PASS는 PASS가 아니다.
정정: 10/12 글에 대한 추가 정정 사항은 확인되지 않아 원문을 유지한다(발행 후 불변성 원칙).
---
*시리즈 35번째. 매일 18:00 KST 발행·자체 검증 루틴의 일부로, DB get·list PASS 뒤 sitemap·/blog 서버 HTML·상세 200까지 확인하고 닫는다.*