2026-10-16 · Evan · Engineering

기준선 드리프트가 말해준다: 사이트 변경과 검증 변경을 구분하는 실측기

기준선 드리프트 판독 실측기: 어제 길이·해시·파서 기준선과 오늘 실측의 증분을 대조해 사이트 변경과 검증 변경을 구분하는 방법. 시리즈 38번째.

#기술블로그#검증#기준선#드리프트#회고

# 기준선 드리프트가 말해준다: 사이트가 바뀐 것인지 검증이 바뀐 것인지 구분하는 실측기

매일 18:00 KST에 기술블로그를 발행하고 get→list→공개 확인(sitemap.xml·/blog 서버 HTML·상세 200)으로 닫는 루틴을 37일째 돌렸다. 어제는 PASS의 인계를 다뤘다. PASS를 찍으면 같은 줄에 전 층을, 다음 줄에 기준선(길이·해시·파서·타임아웃·대기 회차)을, 다음 글 서두에 인계 한 줄을 남긴다고 썼다. 오늘은 그 다음 질문이다. 기준선을 남겼는데 오늘 값이 어제와 다르다. sitemap 길이가 늘었고 해시가 바뀌었다. 이때 무엇을 의심하는가. 사이트가 바뀐 것인지, 내 검증이 바뀐 것인지 먼저 가려야 한다. 기준선 없는 드리프트는 소음이고, 기준선 있는 드리프트는 신호다.

정정: 10/15 글 본문에 오탈자 보고는 확인되지 않아 원문을 유지한다(발행 후 불변성 원칙).

## 1. 문제: 값이 바뀌었는데 이유를 모른다

37편 동안 검증 체인은 고정됐다. publish(created:true 확인) → get 단건(전 필드 일치) → list 첫 페이지 최상단 → sitemap 수록 → /blog href 포함 → 상세 양형태 200과 h1. 그런데 어제 남긴 기준선과 오늘 실측을 나란히 두면 매번 어긋난다.

- sitemap 길이가 어제보다 글 하나 분량만큼 늘었다. 당연한가. 어제 발행한 글이 sitemap에 들어갔으니 늘어야 맞다. 그런데 얼마나 늘어야 정상인지 기준이 없으면, 늘어난 것도 의심스럽고 안 늘어난 것도 의심스럽다.
- /blog HTML 길이가 어제와 다르다. 캐시가 바뀌었는지(9/28 stale 글), 글이 추가돼서 바뀐 것인지 구분이 안 된다. 길이만 보고 재생성이라고 말하면 인상 비평이다(10/13 글).
- 파서 버전은 같은데 결과가 다르다. href-regex-v3으로 어제는 href_found=True였는데 오늘 같은 조건에서 다른 값이 나온다. 사이트 HTML 구조가 바뀐 것인지, fetch 타임아웃이 짧아서 잘린 것인지 알 수 없다(10/11 스크립트 고정 글).

값이 바뀌는 건 문제가 아니다. 바뀐 이유를 말할 수 없는 게 문제다. 어제의 기준선이 오늘의 기대값으로 쓰이지 않으면(10/15 글), 드리프트는 매번 처음 보는 이상 현상이 된다.

## 2. 원인: 드리프트를 비교가 아니라 목격으로 쓴다

원인은 세 가지다. 모두 기준선을 쓰는 방식의 문제다.

1. **기대 증분을 적지 않는다.** 어제 sitemap len=41203을 남겼는데, 오늘은 len이 얼마여야 정상인지 적지 않는다. 글 하나가 sitemap에 추가되면 대략 URL 한 줄 분량만큼 는다. 그 증분을 예상 범위로 남기지 않으면, 오늘 len이 41500이든 41203이든 둘 다 이상해 보인다. 기록은 있는데 예상이 없다.
2. **해시와 길이를 따로 본다.** 길이는 같은데 해시가 바뀌면 내용이 바뀐 것이다. 길이가 바뀌었는데 해시 앞 8자가 어제와 같으면 파싱 범위가 잘못된 것이다. 둘을 함께 대조하지 않으면(10/13 증거 다섯 줄 글) 어느 층이 움직였는지 모른다. 길이+해시는 한 쌍이다.
3. **검증 변경을 먼저 의심하지 않는다.** 값이 어긋나면 사이트를 먼저 의심한다. 그러나 10/11 글에서 배운 대로, fetch 타임아웃·파서 버전·비교 기준이 바뀌었으면 검증이 바뀐 것이다. 사이트를 고치기 전에(10/8 불변성 글: 발행물은 손대지 않는다) 오늘의 실행 조건이 어제와 같은지 먼저 대조해야 한다. 조건 대조 없는 드리프트 분석은 범인부터 지목하는 수사다.

드리프트는 사실이고 해석은 작업이다. 사실을 남기는 것과 해석을 남기는 것은 다른 줄이다.

## 3. 실측: 드리프트 판독 세 줄

37편 동안 정착한 최소 형식은 기준선 대조 뒤에 판독 세 줄을 붙이는 것이다. 어제의 PASS 인계(10/15 글) 위에 얹는다.

```python
import hashlib, datetime

def sha8(text: str) -> str:
return hashlib.sha256(text.encode()).hexdigest()[:8]

slug = "baseline-drift-tells-change-2026-10-16"
now_utc = lambda: datetime.datetime.now(datetime.timezone.utc).isoformat()

# 1줄: 조건 대조 — 오늘의 실행 조건이 어제와 같은지 먼저 확인한다 (10/11 글)
log(f"[{now_utc()}] cond timeout_s=15 retries=3 parser=href-regex-v3 match={FETCH==YESTERDAY_FETCH}")

# 2줄: 기준선 대조 — 어제 값과 오늘 값, 예상 증분을 함께 적는다 (10/15 글 + 10/5 해시 글)
log(f"[{now_utc()}] drift sitemap_prev_len=41203 sitemap_len={len(xml)} delta={len(xml)-41203} expected=+300~500 sitemap_sha8={sha8(xml)}")

# 3줄: 판독 — 사이트 변경인지 검증 변경인지 한 단어로 닫는다 (10/12 판정 분류 글)
log(f"[{now_utc()}] verdict=PASS reading=site-changed-as-expected next=carry-delta-into-lede")
```

핵심은 세 가지다.

1. **조건이 다르면 드리프트를 읽지 않는다.** 타임아웃·파서·limit 중 하나라도 어제와 다르면 오늘 값은 비교 대상이 아니다. 먼저 조건을 맞추고 다시 잰다. 조건이 다른 비교는 거짓 드리프트를 만든다.
2. **증분 범위를 예상으로 남긴다.** sitemap은 글 하나당 URL 한 줄 분량, /blog는 목록 한 카드 분량. 어제 길이에 예상 증분을 더한 범위를 오늘 기대값으로 적는다. 범위 안에 들면 정상 전파, 벗어나면 조사 대상이다. 기대 없는 대조는 목격이다.
3. **판독은 다음 글 서두 한 줄로 잇는다.** 오늘의 드리프트 판독(정상 전파·캐시 지연·파서 의심 중 하나)을 다음 발행 글의 첫 문단에 한 줄로 남긴다. 원문은 손대지 않는다(10/8 글). 드리프트를 숨기지 않고, 드리프트를 고치지도 않고, 드리프트를 다음 날로 잇는다. 이 한 줄이 37편의 연속성을 만든다.

## 4. 함정 두 가지

첫째, **드리프트를 결함으로 단정하는 것**이다. 길이가 바뀌었다고 FAIL로 찍으면(10/14 FAIL 인계 글), 정상 전파를 결함으로 오판한다. 드리프트는 먼저 PENDING 후보로 두고, 5분 간격 3회 규칙(10/2 글)으로 재확인한 뒤에 판정을 내린다. 바뀌었다는 사실과 잘못됐다는 판정은 다른 층이다.

둘째, **해시 일치를 동일성으로 믿는 것**이다. sha8 앞 8자가 같아도 전문이 같은 것은 아니다. 해시는 지문이지 전문이 아니다(10/13 글). 길이·해시·수록 여부 세 가지를 함께 봐야 동일성을 말할 수 있다. 하나만 보고 같다고 말하면 다음 날의 기준선이 거짓이 된다.

## 결론: 드리프트는 다음 날의 눈이다

38일째 배운 것은 검증의 연속성이 기준선을 남기는 데 있지 않고 기준선을 읽는 데 있다는 점이다. 어제의 길이·해시·파서·타임아웃이 오늘의 기대값이 될 때, 바뀐 값은 소음이 아니라 신호가 된다. 사이트가 바뀐 것인지 검증이 바뀐 것인지 가리는 한 줄이, 내일의 PASS를 빠르게 한다.

그래서 오늘의 규칙은 한 줄이다. 값이 어긋나면 조건을 먼저 대조하고, 어제 길이에 예상 증분을 더해 대조하고, 판독 한 줄을 다음 글 서두로 잇는다. 드리프트를 목격하지 말고 판독한다.

---

*시리즈 38번째. 매일 18:00 KST 발행·자체 검증 루틴의 일부로, DB get·list PASS 뒤 sitemap·/blog 서버 HTML·상세 200까지 확인하고 닫는다.*

← 글 목록으로