--- kind: CASE slug: a14-f018-webplatformstartupvalidator title: 운영 런북이 시작 검증기의 판정을 근거로 삼는데 그 검증기를 부르는 곳이 없다 topic: web-inbound-and-http-surface project: clean-architecture-backend-template status: 게시 전 sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 evidenceCapturedOn: 2026-09-03 rootTreeNode: case:a14-f018-webplatformstartupvalidator body: case-a14-f018-webplatformstartupvalidator.body.md assets: - key: a14-f018-webplatformstartupvalidator file: ../../../final/evidence/rendered/a14-f018-webplatformstartupvalidator.svg - key: a14-f018-webplatformstartupvalidator-run file: ../../../final/evidence/rendered/a14-f018-webplatformstartupvalidator-run.svg evidence: - ../../../final/evidence/raw/a14-f018-webplatformstartupvalidator.txt - ../../../final/evidence/raw/a14-f018-webplatformstartupvalidator-run.txt source: - 원본 분석 절은 analysis/14-adapter-inbound-web.md#L1500 이다. --- # 운영 런북이 시작 검증기의 판정을 근거로 삼는데 그 검증기를 부르는 곳이 없다 플랫폼 시작 검증기가 선언된 통제 중 배선되지 않은 것이 있으면 기동을 거부하도록 만들어져 있다. 부르는 곳이 없다. 그런데 운영 런북이 그 검증기가 돈다는 전제로 장애 조사 첫 단계를 적어 두었다. ## 관계 - **라우트 게이트가 실물을 읽어 오는 한 조각만 아무도 만들지 않는다** 원문이 §44.1·§44.2 로 나란히 적은 짝이다. 저쪽은 수집기가 생성되지 않고 여기는 검증기가 시작 훅에 걸리지 않는다. - **@Bean이 있다는 것은 조립 증거가 아니다** 거기서는 등록된 빈이 조립을 뜻하지 않는다고 적었다. 여기는 그보다 앞이다. 클래스에 스테레오타입이 없어 스캔에도 걸리지 않는다. - **시작 검증기가 도는지는 그 능력에 자동설정 루트가 있는지와 일치한다** 이 리프에 그 루트가 없다. 자동설정 진입 목록이 `mvc` 와 `webflux` 뿐이고 `admin` 은 없다. - **그러나 R0 경계가 문서에만 있고 compile 경로에서 닫히지 않는다** 둘 다 문서가 코드보다 앞서 나갔다. 저쪽은 경계 서술이고 여기는 장애 대응 절차다. ## 문제 플랫폼에 시작 검증기가 있고 이름이 실행 시점을 약속한다. 부르는 곳을 확인했다. ## 결론 없다. admin/platform 의 두 파일 중 main 에서 쓰이는 것은 스냅숏 하나뿐이고, 그것을 쓰는 곳도 검증기다. 검증기를 만드는 다섯 자리는 모두 시험이다. 스테레오타입도 없다. 출하보다 허용적인 스캔을 걸어도 두 클래스는 빈이 되지 않는다. 리프의 자동설정 진입점 둘도 mvc 와 webflux 뿐이라 admin 을 올리지 않는다. src 에서 *StartupValidator 로 선언된 클래스를 모두 찾으면 여덟이고, 넷은 자기 파일 밖 main 에 이름이 나오고 넷은 나오지 않는다. 나오는 넷 중 파일서버·HTTP 클라이언트·GraphQL 플랫폼은 app-bootstrap 이나 자기 리프 자동설정이 빈으로 걸었고, Mongo 는 기동 검사 빈이 안에서 직접 만들어 부른다. 나오지 않는 넷은 웹 플랫폼·웹소켓 플랫폼·gRPC 플랫폼·gRPC 서블릿이다. 범위를 이름에서 런북으로 옮기면 더 많이 나온다. 런북들이 백틱으로 지목한 이름 중 main 에 같은 이름의 파일이 있으면서 자기 파일 밖 main 참조가 0 인 것이 열하나다. 그중 둘이 이 리프 런북에 있고, 둘 다 같은 추론 형태다. :27 이 이 검증기를 그렇게 쓰고, :89 가 WebCorsPolicyValidator 를 같은 문장 구조로 쓴다. 코퍼스가 그 두 번째 건은 이미 따로 적어 두었다. 원문에 없는 대목은 여기다. docs/web/runbook.md:27 이 이 검증기를 장애 조사 근거로 쓴다. 검증기가 돌아야 성립하는 추론이고, 돌지 않으므로 그 결론은 근거를 잃는다. 검증기 자체는 동작한다. 통제가 빠진 스냅숏에는 그 이름을 들어 거부하고 다 채워진 스냅숏은 통과시킨다. 원문이 인용한 "Better to refuse to start" 는 NginxInternalUriMapper:30 의 attestMapping() 자바독이다. 그 메서드를 파일서버 기동 검사가 FileserverStartupConfiguration:87 에서 부르므로 인용은 맞다. 판정은 P3 이고 원문과 같다. ## 검증 환경 OpenJDK : 21.0.12 Spring Boot : 4.0.8 확인 방식 : 두 클래스의 이름을 src 전체 자바 파일에서 전수 검색, 리프 자동설정 진입점과 스테레오타입 확인, 같은 스캔을 건 컨텍스트에서 빈 수 확인, 검증기를 두 스냅숏에 직접 실행, 운영 런북 두 편 대조, src 의 *StartupValidator 선언을 검색으로 모아 main 참조 수 비교, 원문이 인용한 문장의 실제 자리와 그것을 부르는 코드 확인 소스 수정 : x ## 재현 조건 원문 근거는 분석 문서의 #L1500 절이다. 1. 검증기 자바독을 읽어 이름이 약속하는 시점과 거부 형태를 확인한다. 2. admin/platform 두 클래스를 자기 파일 밖에서 쓰는 자리를 main 과 test 로 갈라 센다. 3. 리프의 자동설정 진입 목록과 두 클래스의 스테레오타입 유무를 본다. 4. 출하 루트가 이 리프에 거는 것과 같은 스캔을 admin 에 걸고 두 타입의 빈 수를 센다. 5. app-bootstrap 이 다른 시작 검증기를 거는 형태를 읽고, 원문이 인용한 근거 문장이 어느 파일에 있고 무엇이 그것을 부르는지 확인한다. 6. 검증기를 직접 만들어 통제가 빠진 스냅숏과 다 채워진 스냅숏에 각각 넣는다. 7. 운영 런북에서 이 검증기를 근거로 쓰는 대목을 읽는다. 8. src 에 선언된 *StartupValidator 를 모두 찾아 각각 자기 파일 밖 main 파일 수를 센다. 9. 런북들이 백틱으로 지목한 이름을 모아, main 에 같은 이름의 파일이 있으면서 자기 파일 밖 main 참조가 0 인 것을 고른다. ## 본문 검증기 자바독이 실행 시점과 거부 형태를 함께 적어 두었다. 기동 시점에 보는 이유는 그러지 않으면 장애로 알게 되기 때문이고, 경고가 아니라 거부인 이유는 경고를 남기는 검증기는 아무도 읽지 않기 때문이다. ## 두 클래스를 쓰는 자리는 시험뿐이다 :::evidence key="a14-f018-webplatformstartupvalidator" alt="저장소 루트에서 돌린 정적 검색 출력 84줄. 검증기 자바독의 실행 시점과 거부 근거, admin/platform 두 클래스를 자기 파일 밖에서 쓰는 자리가 main 과 test 로 갈려 나오고 검증기 쪽은 test 뿐이다. 이어서 운영 런북이 이 검증기를 근거로 쓰는 세 줄, app-bootstrap 이 파일서버와 HTTP 클라이언트 시작 검증기를 빈으로 거는 두 자리, 이 리프의 자동설정 진입 목록 둘과 그 목록에서 admin 이라는 문자열을 담은 항목 수, 검증기 클래스의 스프링 스테레오타입 수, `src` 에 선언된 `*StartupValidator` 여덟과 각각의 이름이 자기 파일 밖 main 자바 파일 몇 개에 나오는지가 차례로 보인다. 자바독 언급도 그 수에 들어간다. 끝으로 런북들이 백틱으로 지목한 이름 중 main 참조가 0 인 열하나와, 그중 이 리프 런북이 같은 추론을 거는 두 자리가 나온다. 원문이 인용한 근거 주석과 그것을 부르는 기동 검사 자리도 그 사이에 있다." caption="admin/platform 두 클래스의 참조와 런북·형제 배선·검증기 여덟·런북 지목 식별자 대조 — 84줄 · exit 0" zoom="true" ::: 검증기 이름이 자기 파일 밖에 나오는 여섯 줄이 모두 그 시험 파일이다. 그중 다섯이 생성이고 하나는 시험 클래스 자신의 선언이다. 스냅숏은 main 에서 검증기 하나가 쓴다. 리프의 자동설정 진입점은 `mvc` 와 `webflux` 둘이고 `admin` 을 올리는 항목은 없다. 두 클래스에 스프링 스테레오타입도 붙어 있지 않다. ## 같은 저장소가 이미 쓰는 배선 형태가 있다 ```java // FileserverStartupConfiguration.java:35-37 · :47-49 @Bean @ConditionalOnMissingBean FileserverStartupValidator fileserverStartupValidator() { ... @Bean FileserverStartupCheck fileserverStartupCheck( FileserverStartupValidator validator, ``` 검증기를 빈으로 만들고, 그것을 인자로 받는 두 번째 빈이 기동 시점에 실제 검사를 돌린다. HTTP 클라이언트 쪽도 같은 모양이다. 플랫폼 쪽에는 둘 다 없다. ## 런북이 이 검증기를 근거로 쓴다 ```text docs/web/runbook.md:27-29 - **Check first:** the platform snapshot's `uninstalledControls()`. `WebPlatformStartupValidator` fails startup on a missing required control, so a running instance with one missing means it was not in the required list. ``` 통제가 설정만 되고 배선되지 않은 장애를 다루는 절이다. 조사자에게 미설치 통제를 먼저 보라고 하고, 인스턴스가 떠 있다는 사실 자체를 근거로 삼아 그 통제는 필수 목록에 없었다고 결론짓게 한다. 검증기가 돌아야 성립하는 추론이다. 돌지 않으므로 누락된 통제가 필수 목록에 있었더라도 인스턴스는 그대로 떠 있고, 조사자는 반대 결론에 이른다. ## 넣어 보면 거부한다 :::evidence key="a14-f018-webplatformstartupvalidator-run" alt="JVM 프로브 출력 14줄. 통제 셋 중 하나가 배선되지 않은 스냅숏을 넣었을 때 스냅숏이 보고하는 미설치 통제 이름과 검증기가 던진 예외 이름과 메시지, 셋 다 배선된 스냅숏을 넣었을 때 통과한 결과, 그리고 출하 루트의 기본 패키지를 제외 필터 없이 admin 에 걸었을 때 올라온 두 타입의 빈 수가 각각 0 이라는 결과가 보인다." caption="검증기를 두 스냅숏에 직접 넣고, 같은 스캔에서 빈 수를 센 결과 — 14줄 · exit 0" zoom="true" ::: 통제 하나가 빠진 스냅숏을 넣으면 그 이름을 들어 거부한다. 거부 메시지가 런북과 같은 말을 한다. 설정만 되고 설치되지 않은 통제는 필요해지는 순간까지 동작하는 것과 구별되지 않으므로 여기서 기동을 실패시킨다는 것이다. 셋 다 배선된 스냅숏은 통과한다. 같은 프로브가 출하 루트의 기본 패키지를 제외 필터 없이 `admin` 에 걸어 보면 두 타입 다 빈이 0 개다. ## 확인하지 못한 것 애플리케이션을 통째로 띄워 검증기가 돌지 않는 것을 확인하지 않았다. 확인한 것은 main 에 만드는 곳이 없고 스캔으로도 올라오지 않는다는 것까지다. 런북이 첫 단계로 지시하는 스냅숏 조회를 실제로 해 보지 않았다. 확인한 것은 main 에서 스냅숏을 쓰는 곳이 검증기 하나뿐이라는 것까지다. 프로브가 넣은 통제 이름 셋은 지어낸 것이다. 실제 배포가 어떤 이름을 필수로 두는지는 확인하지 않았다.