check_evidence --repo 이 clean-architecture-backend-template 에서 141건을 세고 있었다.
124건 전부를 고정 리비전 21234e38 에 대조했더니 날조는 0 이었다 — 인용은 맞고 없던
쪽이 SSOT 였다. 그래서 SSOT 를 보강한다.
넣는 것은 기록이 옮겨 적은 문장이 아니라 저장소 원문이다. 기록을 복사해 넣으면
검사기는 초록이 되지만 옮겨 적기가 어긋나도 더는 못 잡는다. 원문을 넣으면 어긋난
기록은 계속 걸린다.
- 코드 124건 → 리프 절 일곱 곳에 원문 30조각(300줄). 자리는 기록의 source 앵커와
소스 파일의 모듈을 교차시켜 정했고, 둘이 갈린 다섯은 모듈을 따르고 그 사실을 적었다
- 식별자 13건 → spring.factories · MethodSecurityConfig 원문과 줄여 적힌 경로의 전체 경로
- source 앵커 4건 → 계약이 이미 적고 있던 SSOT 앵커를 기록 frontmatter 에 맞췄다.
맨 앵커를 한 줄씩 앞에 둔다 — `_record_sources` 가 `- <공백 없는 한 덩어리>` 만 잇달아 읽는다
- 인용 4건 → concept-signed-cursor-structure 의 `{ ... }` 생략을 저장소 원문 형태로 고쳤다.
생략한 자리는 `…` 로 남기고 무엇을 줄였는지 본문에 적는다
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4vKjQo9KKBBokzxqXLCfk
10 KiB
kind, slug, title, topic, project, status, sourceRevision, rootTreeNode, evidenceCapturedOn, assets, evidence, source
| kind | slug | title | topic | project | status | sourceRevision | rootTreeNode | evidenceCapturedOn | assets | evidence | source | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| CASE | a-flag-that-validates-an-unwired-subsystem | 하위 시스템 전체가 미배선인데 그것을 켜는 플래그는 시작 검사를 수행한다 | assembly-ownership | clean-architecture-backend-template | 게시 전 | 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 | case:a-flag-that-validates-an-unwired-subsystem | 2026-09-02 |
|
|
|
하위 시스템 전체가 미배선인데 그것을 켜는 플래그는 시작 검사를 수행한다
mongo 트랜잭션 타입은 어느 것도 빈이 아니고 다른 프로덕션 코드가 부르지도 않는다. 그런데 그것을 켜는 설정 플래그는 살아 있어서, 토폴로지 프로브를 함께 공급한 배포에서는 트랜잭션 지원 여부를 검증받게 만든다. 능력 요구만 만들고 능력을 제공하지 않는다.
관계
- @Bean이 있다는 것은 조립 증거가 아니다 플래그 값이 바인딩된다는 것도 그 하위 시스템이 조립됐다는 증거가 아니다.
- "꺼짐"은 조건의 반복이 아니라 구조여야 한다 같은 설정 클래스가 스위치 둘을 서로 반대 방향으로 하위 시스템과 떼어 놓았다.
문제
mongo 플랫폼 자동설정에서 트랜잭션 타입과 인과 세션 타입을 찾으면 일치가 각각 0 이다. 트랜잭션 패키지 밖의 프로덕션 참조도 0 이다.
즉 실행체와 세션 팩토리와 재시도 코디네이터와 인과 세션 실행기 어느 것도 빈이 아니고, 이 어댑터의 다른 프로덕션 코드가 부르지도 않는다.
그런데 설정의 transactions 플래그는 다르다. 값이 그대로 바인딩되고, 플랫폼 자동설정이 그 값을 시작 검증기에 넘기며, 검증기는 트랜잭션이 켜져 있는데 능력이 Stable 이 아니면 시작을 거부한다.
결론
프로브를 공급한 배포는 토폴로지가 트랜잭션을 지원하는지 검증받고, 그다음 트랜잭션을 실행할 빈은 하나도 받지 못한다. 플래그가 능력 요구만 만들고 능력을 제공하지 않는다.
같은 설정 클래스의 change stream 플래그는 반대쪽으로 어긋나 있다. 실행체 쪽 드라이버 코드는 출하되어 빈으로 조립되는데, 플래그는 생성자에서 조용히 거짓이 된다.
데이터 위험은 없다. 없는 것을 쓸 수는 없기 때문이다. 이 플래그가 무엇을 켜는지 적힌 곳이 없고, 시작 검증이 통과한 것과 실행체가 조립된 것을 구분해 주는 신호도 없다.
수정은 셋 중 하나다. 트랜잭션 실행체를 조건부 빈으로 조립하거나, 플래그가 무엇을 켜는지를 문서에 적거나, 같은 생성자의 required-secondaries 처럼 값을 예외로 거부하는 것이다.
검증 환경
OpenJDK : 21.0.12 Gradle : 9.0.0 Spring Boot : 4.0.8 확인 방식 : 정적 도달성 전수 확인 소스 수정 : x
재현 조건
- 트랜잭션 패키지의 타입을 하위 디렉터리까지 나열하고, mongo 플랫폼 자동설정에서 그 타입 참조를 센다.
- 트랜잭션 패키지 밖의 프로덕션 참조를 센다.
- 설정 플래그가 선언되는 지점과 그 값이 시작 검증기로 전달되는 지점, 검증기의 판정을 확인한다.
- 그 검사를 여는 조건과, 이 저장소가 그 조건을 만족시키는지 확인한다.
- 같은 생성자가 형제 입력을 각각 어떻게 처리하는지 확인한다.
- change stream 실행체의 조립 조건과 드라이버 호출을 확인한다.
본문
ca-skeleton.persistence-mongo.platform.transactions 를 참으로 둔 배포가 토폴로지 프로브를 함께 공급하면, 시작할 때 트랜잭션 능력 검사가 돈다. 검사가 통과해도 트랜잭션을 실행할 빈은 하나도 조립되지 않는다.
타입은 스물인데 그것을 부르는 코드가 없다
:::evidence key="a-flag-that-validates-an-unwired-subsystem" alt="코드베이스에서 트랜잭션 패키지의 타입 스물과 자동설정의 참조 매치 수, 패키지 밖 참조 수, 플래그가 선언되어 검증기 판정에 쓰이는 지점, 그 검사를 여는 프로브 조건과 이 저장소가 프로브를 내지 않는다는 기술, 설정 생성자가 입력마다 다르게 처리하는 세 줄, 반대쪽 실행체가 플래그와 무관하게 조립되는 조건과 드라이버 호출, 그리고 소비자가 요구하는 다섯 포트를 뽑은 출력 70줄. 타입은 스물인데 참조가 0 이고, 검사는 프로브가 있어야 열린다는 것이 그 출력에 그대로 보인다." caption="타입 20 · 참조 0 · 플래그 → 검증기 → 거부 · 검사를 여는 프로브 조건 · 반대쪽의 무조건 조립 — 70줄 · exit 0" zoom="true" :::
트랜잭션 패키지에 타입이 스무 개 있다. 블로킹과 리액티브 양쪽의 실행체와 세션 팩토리, 스코프와 프로파일, 커밋 조정과 재시도 예산을 갖춘 재시도 코디네이터, 그리고 인과 세션 실행기 넷이다. 절반은 계약이고 나머지가 구체 클래스다.
플랫폼 자동설정에서 Transaction 을 세면 0, CausalSession 을 세도 0 이다. 트랜잭션 패키지 밖의 main 참조도 0 이다.
플래그는 그 사실과 무관하게 검증까지 간다
transactions 는 설정 record 의 성분이라 값이 그대로 바인딩된다. 자동설정 362행이 그 값을 시작 검증기에 넘기고, 검증기 97행이 그것으로 판정한다 — 트랜잭션이 켜져 있는데 능력이 Stable 이 아니면 시작을 거부한다.
그 검사 자체가 조건부다
검증기를 돌리는 빈은 MongoTopologyProbe 에 조건되어 있다. 그리고 이 저장소는 프로브를 출하하지 않는다 — 그 자리 javadoc 이 직접 적는다. 프로브는 연결을 소유한 조립 루트가 살아 있는 데이터 평면 클라이언트로 만드는 것이고, 그것은 fork 의 결정이라는 것이다.
프로브 없이 플랫폼 프로파일만 설정한 배포는 별도의 빈이 시작을 거부한다. 그 예외 문구가 이유를 적는다 — 검사가 하필 자기 부재를 보고해야 할 바로 그 빈에 조건되어 있어서, 프로브 없이 시작하면 토폴로지도 Stable API 수준도 자격의 실제 능력도 아무것도 검사하지 않은 채 조용히 지나간다는 것이다.
그래서 이 플래그가 시작 요구를 만드는 것은 fork 가 프로브와 보안 프로파일과 관리 자격 참조와 스키마 버전 범위를 모두 공급했을 때다. 셰이프 그대로의 이 저장소에서는 검사가 열리지 않는다.
같은 생성자가 입력마다 다르게 처리한다
설정 record 의 컴팩트 생성자를 보면 처리가 갈린다.
profiles 의 널은 빈 맵으로 흡수한다. change-streams 는 무엇이 오든 거짓으로 덮어쓴다. required-secondaries 에 음수가 오면 예외를 던져 거부한다.
transactions 는 이 생성자에 아예 등장하지 않는다. 값이 그대로 보존되는 이유가 그것이다.
덮어쓰기 쪽만 참으로 설정해도 예외도 로그도 발생하지 않는다. 그 줄에 붙은 주석은 값을 무시하면 적용된 것처럼 보이게 되니 저장하지 않고 거부한다고 적는데, 예외로 거부하는 것은 required-secondaries 가 하는 일이고 여기서 일어나는 것은 조용한 덮어쓰기다.
강등의 근거로 적힌 사실이 코드와 맞지 않는다
그 주석에는 드라이버 쪽 소스가 — watch, resumeAfter/startAfter, 커서 수명, 재접속이 — 출하되지 않았다고 이유까지 적혀 있다.
같은 모듈의 리액티브 소스가 .changeStream(...) 과 .watchCollection(...) 을 호출하고, 재개 위치에 따라 startAfter 와 resumeAfter 를 나눠 건다. 자동설정이 그것을 빈으로 조립하는데, 그 경로에서 플래그를 보는 조건은 0 이다. 리액티브 템플릿이 있으면 조립된다.
그 위의 소비자는 다섯 포트를 요구한다. 무엇을 투영할지, 중복을 어떻게 걸러 낼지, 어디에 투영했다고 기록할지, 저장한 토큰을 어떻게 보호할지, 어느 컬렉션을 볼지다. 다섯 모두 main 에서 빈으로 등록되는 곳이 0 인데, 이쪽은 어긋난 것이 아니다. 플랫폼이 투영기를 지어낼 수 없으니 배포가 주기 전까지 소비자가 서 있는 것이 맞다.
두 스위치가 반대 방향으로 같은 곳에서 끊겼다
트랜잭션은 스위치가 살아서 요구를 만드는데 그 요구를 갚을 코드가 조립되지 않는다. change stream 은 코드가 조립되는데 스위치가 죽어 있다. 방향은 반대이고 끊긴 자리는 같다.
남는 것은 데이터 위험이 아니다
없는 것을 쓸 수는 없으니 트랜잭션이 깨질 일은 없다.
남는 것은 이 플래그가 무엇을 켜는지 적힌 곳이 없다는 것이다. 시작 검증이 통과한 것과 실행체가 조립된 것을 구분해 주는 신호도 없다.
확인하지 못한 것
애플리케이션을 부팅해 플래그를 켠 상태의 시작 동작과 빈 목록을 확인하지 않았다. 도달성은 이름 기반 정적 검색으로 판정했으므로, 리플렉션이나 설정으로 조립되는 경로는 배제하지 못했다.