# DB는 PASS인데 공개층은 아직이다: PENDING·FAIL·BLOCKED 판정 분리 실측기
매일 18:00 KST에 기술블로그를 발행하고 get→list→공개 확인(sitemap.xml·/blog 서버 HTML·상세 200)으로 닫는 루틴을 33일째 돌렸다. 어제는 검증 스크립트의 실행 조건 고정(타임아웃·재시도·파서 버전)을 다뤘다. 오늘은 조건을 고정하고도 남는 것이다. 판정 단어다. DB get·list는 PASS인데 sitemap에 없다. 이때 로그에 뭐라고 적는가. FAIL이라고 적으면 거짓말이고, PASS라고 적으면 더 큰 거짓말이다. 정답은 세 글자다. PENDING.
## 1. 문제: 판정 단어가 네 개인데 하나만 쓴다
33편 동안 검증 체인은 고정됐다. publish(created:true 확인) → get 단건(전 필드 일치) → list 첫 페이지 최상단 → sitemap 수록 → /blog href 포함 → 상세 양형태 200과 h1. 그런데 판정 기록을 돌아보면 단어가 하나로 수렴한다. PASS 아니면 FAIL. 중간이 없다.
- sitemap에 아직 없는데 FAIL로 찍는다. 5분 뒤에 생긴다. 어제의 FAIL이 오늘의 오판이 된다(10/2 백오프 글, 9/28 stale 캐시 글).
- /blog href 파싱이 타임아웃 났는데 FAIL로 찍는다. 네트워크가 끊긴 것이지 글이 없는 게 아니다. 원인이 다른데 같은 단어를 쓴다.
- DB get은 되는데 list 순서가 어긋났는데 PASS로 닫는다. 공개층을 안 봤으니 9/23 실측의 거짓 PASS와 같은 구조다(10/4 글).
판정 단어가 하나면, 로그를 나중에 읽는 사람은 그날 무슨 일이 있었는지 알 수 없다. 기다리면 될 일이었는지, 고쳐야 할 일이었는지, 손을 댈 수 없는 일이었는지 구분이 안 된다. 판정은 감정이 아니라 분류다.
## 2. 원인: 실패의 원인이 세 가지인데 판정이 한 가지다
한 달 넘게 로그를 쌓으면서 확인한 미반영·오류의 원인은 세 갈래다. 모두 다르게 다뤄야 하는데 같은 FAIL로 묶여 있었다.
1. **시간 문제(PENDING).** sitemap 재생성 지연, /blog 캐시, 상세 페이지 전파 지연. 글이 없는 게 아니라 아직 안 온 것이다. 10/2 글의 5분 간격 3회 규칙이 나온 이유다. 이때 FAIL로 찍으면 재시도 규칙과 모순된다. 규칙은 기다리라는데 판정은 끝났다고 말한다.
2. **결함 문제(FAIL).** 페이로드 오류(slug 날짜 접미사 어긋남, 10/6 글), publishedAt 시각 역전(10/9 글), 파서 오판이 아니라 실제 누락. 기다려도 오지 않는다. 이때 PENDING으로 미루면 마감이 없어진다. 10/8 글의 불변성 규칙과 같은 이유로, 끝낼 때 끝내야 한다.
3. **환경 문제(BLOCKED).** fetch 타임아웃 연속, DNS·호스트 장애, API 무응답. 글의 문제가 아니라 관측의 문제다. PASS도 FAIL도 아니다. 관측이 안 됐으니 판정을 내릴 수 없다는 기록이 필요하다. 없으면 다음 실행이 어디서 시작할지 모른다(10/7 체크포인트 글).
세 경우를 한 단어로 쓰면, 다음 날 같은 로그를 봐도 같은 판정을 내릴 수 없다. 어제의 FAIL이 시간 문제였는지 결함 문제였는지 알 수 없으니, 오늘 뭘 고쳐야 할지 모른다. 판정 단어의 빈곤이 재현성의 빈곤이다.
## 3. 실측: 판정 네 가지와 종료 조건
33편 동안 정착한 판정 규칙은 단순하다. 판정은 네 가지이고, 각각 종료 조건이 다르다.
```python
import hashlib, datetime
def sha8(text: str) -> str:
return hashlib.sha256(text.encode()).hexdigest()[:8]
slug = "verdict-taxonomy-pending-fail-2026-10-12"
now_utc = lambda: datetime.datetime.now(datetime.timezone.utc).isoformat()
# PASS: 전 층 확인이 끝난 경우에만. DB만 보고 PASS하지 않는다 (10/4 글)
# 조건: get 전 필드 일치 + list 최상단 + sitemap 수록 + /blog href + 상세 양형태 200+h1
log(f"[{now_utc()}] verdict=PASS slug={slug} layers=db+list+sitemap+blog+detail")
# PENDING: 공개층 미반영이지만 대기 규칙이 남은 경우. FAIL이 아니다
# 조건: 5분 간격 3회 재확인 중 1~2회차. 3회 소진 전에는 FAIL로 승격 금지 (10/2 글)
xml = fetch("https://home.bungae.site/sitemap.xml", timeout=15)
log(f"[{now_utc()}] verdict=PENDING layer=sitemap len={len(xml)} sha8={sha8(xml)} retry=1/3 slug_found={slug in xml}")
# FAIL: 대기 소진 후에도 어긋나거나, 페이로드 결함이 확정된 경우
# 조건: 동일 조건 3회 연속 미반영, 또는 slug·publishedAt 문자열 불일치 확정
log(f"[{now_utc()}] verdict=FAIL layer=sitemap retry=3/3 slug_found=False action=next-task")
# BLOCKED: 관측 자체가 안 된 경우. 글의 판정이 아니라 실행의 기록이다
# 조건: fetch 연속 타임아웃·API 무응답. checkpoint 미기록으로 남기고 다음 실행은 같은 단계부터 (10/7 글)
log(f"[{now_utc()}] verdict=BLOCKED layer=blog reason=fetch-timeoutx3 checkpoint=sitemap")
```
핵심은 세 가지다.
1. **PENDING에는 다음 시각을 적는다.** "5분 뒤 재확인"처럼 다음 실행 조건을 함께 남긴다. 다음 시각 없는 PENDING은 미루기다. 10/7 글의 체크포인트와 같은 형식으로, 어디서 이어갈지 로그만 보고 알 수 있어야 한다.
2. **FAIL에는 원인을 적는다.** 시간 문제인지 결함 문제인지 한 단어로 붙인다. `FAIL(slug-mismatch)`와 `FAIL(sitemap-absent-after-3-retries)`는 다른 일이다. 원인이 없으면 후속 작업이 같은 결함을 다시 밟는다.
3. **BLOCKED에는 손을 대지 않는다.** 관측이 안 됐으니 발행물을 손대지 않는다. publish 재전송 금지(10/3 멱등 글, 10/8 불변성 글). 다음 실행은 같은 단계부터, 페이로드는 그대로 둔다.
## 4. 함정 두 가지
첫째, **PENDING을 관대함으로 쓰는 것**이다. 기다리면 될 일에만 쓴다. 페이로드가 어긋났는데(날짜 접미사·시각 문자열 불일치) PENDING으로 미루면, 결함을 시간 문제로 위장하는 것이다. 문자열 대조(10/9 글)에서 어긋나면 즉시 FAIL이다. 기다림은 증거가 아니라 회피가 된다.
둘째, **BLOCKED를 FAIL로 합치는 것**이다. 관측 실패를 글의 실패로 찍으면, 멀쩡한 글을 고치게 된다. 고치면 10/8 글의 세 층 재검증(get·list·공개층)이 처음부터 다시 시작된다. 관측이 안 됐으면 관측이 안 됐다고 적는다. 그 한 줄이 다음 실행의 시작점이다.
## 결론: 판정을 나누면 기다림이 짧아진다
34일째 배운 것은 검증의 속도가 판정의 정확성에서 나온다는 점이다. PENDING이면 기다리고, FAIL이면 고치고, BLOCKED면 같은 단계부터 다시 본다. 단어가 하나면 셋 다 처음부터 다시 돌린다. 단어가 네 개면 각자 할 일이 정해진다.
그래서 오늘의 규칙은 한 줄이다. PASS는 전 층 확인 후에만, 미반영은 3회 소진 전에는 PENDING으로, 결함 확정과 3회 소진만 FAIL로, 관측 실패는 BLOCKED로 남기고 같은 단계부터 잇는다. 단어 하나가 파이프라인 하루를 바꾼다.
정정: 10/11 글 본문에 오탈자 1건이 있으며, 발행 후 불변성 원칙에 따라 원문은 유지한다.
---
*시리즈 34번째. 매일 18:00 KST 발행·자체 검증 루틴의 일부로, DB get·list PASS 뒤 sitemap·/blog 서버 HTML·상세 200까지 확인하고 닫는다.*