출처에 닿지 못한 검증 — 확인 불가는 실패와 같은 코드로 끝나지 않는다
작성 2026-08-10 · 최종 갱신 2026-08-10
검증 게이트가 인용이 틀렸다고 판정한 것과 출처에 아예 닿지 못한 것을 같은 종료코드(1)로 끝내고 있었다. 두 상태는 다음 행동이 정반대인데(전자는 인용 정정, 후자는 재실행) 게이트는 문구로만 구분하고 코드로는 구분하지 않았다. 이 로그의 다른 게이트 두 개는 이미 그 구분을 갖고 있다. 세 상태(통과·검증 실패·확인 불가)를 종료코드 0·1·75 로 나누고, 출처 도달 여부를 먼저 확인해 닿지 않으면 예산을 태우지 않고 끝내며, 확인 불가도 게시는 막되 데이터 결함으로 표시하지 않는다. 확인 불가 상태에서 그 게이트와 무관한 산출물의 게시를 함께 막을지는 이 기록에서 정하지 않고 현재 그렇게 되고 있다는 사실만 적는다.
맥락
2026-08-10 설계로그 인용 게이트가 두 번 연속 실패했다. 11:16 실행은 541초, 13:02 실행은 541초를 쓰고 각각 조회 32건이 전부 '조회 실패 — urlopen error timed out' 이었다. 성공한 조회가 한 건도 없다.
같은 날 10:54 실행은 같은 러너 환경에서 36초에 끝났고 일치 19건·문언 동일 65건·조회 실패 0건이었다. 두 실패 사이 다른 환경에서 같은 OC 키로 같은 엔드포인트를 호출하면 1.2초에 응답했다.
즉 데이터에는 아무 문제가 없고 러너에서 출처에 닿지 못한 것이다. 게이트의 출력 문구는 그 구분을 이미 적고 있었다 — '이 실행은 드리프트 없음을 뜻하지 않는다 — 재실행할 것'.
그러나 종료코드는 1 이다. 인용이 틀렸을 때와 같은 코드이고, 워크플로는 둘을 구분하지 않는다. 배포 스텝은 두 경우 모두 skipped 로 끝난다.
이 저장소의 다른 게이트 두 개는 이 구분을 이미 갖고 있다. scripts/_r15c_build.py 는 검증 전에 출처 도달을 핑으로 확인하고 닿지 않으면 본검증을 생략한 채 게시 보류(EX_TEMPFAIL)로 끝낸다. scripts/generate_daily_tip.py 는 EX_TEMPFAIL=75 를 상수로 두고 확인 불가일 때 그 코드를 반환한다.
러너에서 law.go.kr 도달이 막히는 것은 새로 발견된 사정이 아니다. scripts/register_missing_precedents.py 와 scripts/register_casenote_assets.py 는 'GitHub 러너는 law.go.kr 접속이 막혀(EX_TEMPFAIL) 실시간 검증이 안 되는' 상황을 전제로 만들어진 도구다.
2026-08-06 에 이 게이트의 실행 예산을 워크플로 timeout 이 아니라 코드로 옮긴 이유도 같은 축이었다 — 점검이 돌지 않은 것과 결과가 깨끗한 것을 구분해야 한다는 것이다. 그때 구분한 것은 출력 문구였고 종료코드는 그대로 두었다.
예산 상향은 이 상태에 대한 처방이 아니다. 느려서 못 끝낸 것이 아니라 한 건도 되지 않았기 때문이며, 예산을 두 배로 늘리면 실패를 두 배 느리게 확인할 뿐이다.
검토한 대안
실행 예산을 올린다.
성공한 조회가 0건이므로 시간 문제가 아니다. 예산은 실패를 늦출 뿐이고, 워크플로 timeout 아래에 예산을 두기로 한 2026-08-06 의 구조도 흔들린다.
확인 불가를 통과로 처리해 배포를 진행시킨다.
확인하지 못한 것을 확인된 것으로 적는 것이며 이 로그의 fail-closed 를 정면으로 무너뜨린다. ADR-0008 이 적은 '관측할 수 없는 것을 이상 없음으로 적지 않는다'와 같은 지점이다.
세 상태를 종료코드로 구분하고, 출처 도달 여부를 먼저 확인한다.
다음 행동이 다른 두 상태를 코드로 나눈다. 이 저장소의 다른 게이트 두 개가 이미 쓰는 형태이므로 새 규약을 만드는 것이 아니라 빠진 곳을 맞추는 것이다. 도달 확인을 먼저 하면 예산을 태우지 않고 끝난다.
설계로그 인용 게이트를 배포 워크플로에서 떼어 내 사후 탐지로만 돌린다.
2026-07-27 에 이 게이트를 배포 스텝 앞에 인라인으로 둔 이유가 있다 — 형제 워크플로로 분리하면 같은 push 에서 병렬 실행되어 차단이 아니라 사후 경보가 된다. 예방 게이트를 탐지로 바꾸는 것은 이 사고와 별개의 판단이므로 여기서 하지 않는다.
결정
- 외부 출처에 도달해야 성립하는 검증 게이트는 결과를 세 상태로 구분한다 — 통과, 검증 실패(자료에 결함이 있다), 확인 불가(출처에 닿지 못했거나 예산을 넘겼다). 종료코드를 각각 0·1·75 로 하고 세 상태의 출력 문구를 다르게 낸다. 75 는 sysexits.h 의 EX_TEMPFAIL 이며 이 저장소의 다른 게이트 두 개가 이미 쓰는 값이다.
- 검증을 시작하기 전에 출처에 닿는지 먼저 확인한다. 닿지 않으면 본검증을 돌리지 않고 즉시 확인 불가로 끝낸다. 닿지 않는 상태에서 예산을 전부 태우는 것은 확인을 더 하는 것이 아니라 진단을 늦추는 것이다.
- 확인 불가도 게시를 막는다. 다만 그것을 검증 실패로 표시하지 않는다. 경보 제목과 로그 첫 줄에 데이터 결함이 아니라는 것과 다음 행동이 재실행이라는 것을 적는다.
- 확인 불가가 연속 2회 지속되면 사람에게 알린다. 조용한 재시도만 반복하지 않는다. 2회라는 값의 출처는 ADR-0039 결정 1 의 ③(이 로그가 명시적으로 정한 값)이며 법령이나 관측에서 나온 값이 아니다.
- 확인 불가 상태에서 그 게이트와 무관한 산출물의 게시를 함께 막을지는 이 기록에서 정하지 않는다. 다만 현재 함께 막히고 있다는 사실과 그 비용을 여기에 적어 둔다 — 스텝 순서 때문에 그렇게 되고 있을 뿐이고 그렇게 하기로 정한 기록은 어디에도 없다. 정하지 않았다는 것을 적는 것이 정한 것처럼 보이게 두는 것보다 낫다(ADR-0039 와 같은 취급).
근거
- 두 상태의 다음 행동이 정반대다. 검증 실패는 인용을 고쳐야 하고 확인 불가는 그대로 두고 다시 돌려야 한다. 같은 코드로 끝나면 받는 쪽이 무엇을 해야 하는지 알 수 없고, 최악의 경우 멀쩡한 자료를 고치려 든다.
- 이 구분은 새로 만드는 규약이 아니다. scripts/_r15c_build.py 와 scripts/generate_daily_tip.py 가 이미 EX_TEMPFAIL 로 같은 구분을 하고 있으며, 설계로그 게이트만 빠져 있었다. 한 곳에서 방어하는 결함이 다른 곳에 없으면 그쪽이 뚫려 있는 것이라는 2026-08-09 의 교훈이 그대로 적용된다.
- 도달 확인을 먼저 하는 것도 이미 있는 형태다. _r15c_build.py 는 '망 장애 상태로 통째로 돌린 뒤 끝에서 EX_TEMPFAIL' 하는 낭비를 막으려고 핑 선점검을 둔다. 2026-08-10 의 두 실행이 각각 541초를 태운 것이 정확히 그 낭비다.
- 결정 3 이 게시를 계속 막는 이유는 ADR-0008 의 구조와 같다. 확인하지 못한 상태를 확인된 것으로 취급하지 않는다. 다만 막는 것과 결함으로 표시하는 것은 다른 일이며, 후자는 사실이 아닌 것을 적는 일이다(ADR-0016 이 검증 등급을 나눈 것과 같은 이유).
- 결정 5 를 비워 둔 이유는 그 판단이 이 사고보다 크기 때문이다. 무관한 산출물을 함께 막을지는 게시 파이프라인 전체의 구조 문제이고, 오늘 막힌 것이 설계로그였다는 사정만으로 정할 일이 아니다. ADR-0010 이 성공과 중단의 기준을 착수 전에 고정하라고 한 취지에도, 급한 사고를 계기로 그 기준을 즉석에서 바꾸지 않는 편이 맞다.
이 결정이 만드는 한계
- 확인 불가가 길어지면 게시가 그만큼 오래 멈춘다. 이 결정은 멈춤을 줄이지 않고 멈춘 이유가 정확히 보이게 할 뿐이다. 멈춤 자체를 줄이려면 결정 5 를 정하거나 출처 도달을 확보해야 한다.
- 도달 확인이 거짓 보류를 만들 수 있다. 확인용 호출만 실패하고 본조회는 되는 경우가 있으면 검증을 건너뛴 채 보류로 끝난다. 보류는 게시를 막는 쪽이므로 안전한 방향의 오류이지만, 잦아지면 게이트가 실질적으로 죽는다. 임계를 실측 없이 좁히지 않는다.
- 결정 4 의 2회는 우리가 정한 값이고 관측에서 나오지 않았다. 실제 도달 실패의 지속 분포를 모르므로 이 값이 너무 이르거나 늦을 수 있다.
- 결정 5 를 미정으로 둔 결과 무관한 산출물의 동반 차단은 계속된다. 오늘처럼 설계로그와 무관한 콘텐츠가 함께 멈추는 일이 다시 일어나며, 그때 이 기록이 그것을 의도된 상태가 아니라 미정 상태로 설명한다.
- 게이트가 세 상태를 내면 그 상태를 소비하는 쪽(워크플로 조건·경보 문구)도 세 갈래가 된다. 조건이 늘면 어느 한 갈래가 조용히 죽는 경로도 늘어난다. 2026-08-06 이 'cancelled 는 failure 가 아니다'로 겪은 것과 같은 종류의 위험이며, 상태를 늘리는 대가다.
폐기 조건
- 러너에서 출처 도달이 안정적으로 회복될 때 — 결정 2 의 선점검 비용을 재평가한다. 도달이 안정적이면 선점검은 매 실행에 붙는 순수 비용이 된다.
- 확인 불가의 지속 분포를 관측할 수 있게 될 때 — 결정 4 의 2회를 그 관측값으로 교체하고 값의 출처를 ③ 에서 관측으로 바꾼다.
- 결정 5 를 정할 때 — 이 기록을 개정한다. 정한 내용이 '함께 막는다'여도 그 사실을 적는다.
- 검증이 닿아야 할 외부 출처가 law.go.kr 외로 늘어날 때 — 세 상태 구분을 그 출처의 게이트에도 적용한다.
근거자료
인용 법령
인용한 법령이 없습니다.
참고자료
외부 참고자료를 부착하지 않았다. 이 결정의 근거는 전부 이 프로젝트 자신의 산출물이다 — 2026-08-10 의 실행 로그 세 건(성공 1·실패 2, 각 소요와 조회 성공 건수), scripts/_r15c_build.py 와 scripts/generate_daily_tip.py 의 EX_TEMPFAIL 처리, scripts/register_missing_precedents.py 의 전제 서술, .github/workflows/deploy-hosting.yml 의 스텝 순서. ADR-0016 결정 1 의 등급으로는 자체자료에 해당하며, 외부 문헌으로 뒷받침되는 주장은 이 기록에 없다.
산출물·측정 기록
- 설계로그 인용 게이트 실행 결과 3건 (Deploy Firebase Hosting, 2026-08-10)run 31381069905(10:54) 36초·일치 19건·조회 실패 0건 / run 31382718193(11:16) 541초·조회 실패 32건·성공 0건 / run 31390697157(13:02) 541초·조회 실패 32건·성공 0건. 세 실행 모두 같은 러너 환경·같은 OC 키이며, 두 실패 사이 다른 환경에서 같은 엔드포인트가 1.2초에 응답했다. 결정 1·2 의 실측 근거다.
- 이미 EX_TEMPFAIL 구분을 갖고 있는 게이트 두 개 (scripts/_r15c_build.py · scripts/generate_daily_tip.py)generate_daily_tip.py 는 EX_TEMPFAIL=75 를 상수로 두고 확인 불가일 때 그 코드를 반환한다. _r15c_build.py 는 검증 전 핑으로 출처 도달을 확인하고 닿지 않으면 본검증을 생략한 채 게시 보류(EX_TEMPFAIL)로 끝낸다. 결정 1 이 새 규약이 아니라 빠진 곳을 맞추는 것이라는 근거다.
- 배포 워크플로의 스텝 순서 (.github/workflows/deploy-hosting.yml)인용 게이트가 배포 스텝 앞에 인라인으로 놓여 있어, 게이트가 실패하면 설계로그와 무관한 정적 콘텐츠의 배포 스텝까지 skipped 로 끝난다. 결정 5 가 '현재 그렇게 되고 있다'고 적은 상태의 출처이며, 그렇게 하기로 정한 기록은 없다.
작성 기록
이 기록의 조사·실측 정리·선택지 정리와 문장 초안은 사람이 지시하고 LLM(Claude)이 작성했다. 채택안(선택지 C)과 결정 1~5 의 문장은 운영자가 2026-08-10 에 검토해 채택했다. 결정 5 를 비워 두는 것도 운영자의 선택이며, LLM 초안은 그 항목을 '별도로 정한다'로 제시했다. 작성 주체를 적는 형식은 ADR-0035 에서 신설했다.
담당 리더
마디L60
시스템 총괄 · Master API Gateway사람의 자격이나 직위에 대응하지 않는 시스템 역할 표기다. 이 로그의 설계 결정은 사람이 작성하고 사람이 책임진다(ADR-0004·ADR-0016).
설계로그는 특정 법률 분야가 아니라 시스템 전체의 설계 결정을 다룬다. 개별 도메인 리더가 아니라 60명 리더 시스템의 운영·품질을 총괄하는 리더가 담당한다.
리더 프로필 보기