Files
document-haness/docs/clean-architecture-backend-template/analysis/18-app-bootstrap.md
T
DongHyeonkaandClaude Opus 5 b2963105a8 docs(keycloak-session-store): import the session-storage lab as a new project
The keycloak project ended with four open questions that design could not
settle. A two-VM lab was built to answer them by measurement, and this is
that material: 26 experiments, 125 raw command outputs, 22 browser captures.

Follows the import procedure in README.md.

  source/     the originating repository verbatim — 78 documents, 28 SVGs,
              8 manifests, plus .source-revision recording the commit
  final/      the SSOT
    document.md   729 lines written from the 29 experiment documents, not
                  concatenated: what was predicted, what was measured, and
                  where the measurement itself was wrong
    evidence/raw    125 outputs, flattened to <experiment>__<file> because
                    the originals collided (01-baseline.txt appeared three
                    times) and the audit only globs the top level
    evidence/meta   one per raw file; command and exitCode are null and the
                    README says why rather than inventing them
    evidence/browser  22 captures
    assets/       three diagrams through techviz
    .techviz/     their VizSpecs

A separate project rather than an addition to keycloak: the B-layer answers
that project's four questions, but the A, C and D layers are about cluster
failure, SSO and operations, and one document.md should hold one subject.
The four question records there can point here through 관계.

Recorded rather than papered over: only three of the 28 diagrams were
remade. The repository forbids hand-drawn SVG and forbids titles inside the
canvas; all 28 originals carry both, so converting them is redrawing, not
reformatting. They stay in source/ and the gap is written into the document.

verify-pipeline.py passes. audit-records.py reports no issues.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 22:51:59 +09:00

751 lines
53 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# app-bootstrap — 코드베이스 분석
## SSOT identity — 2026-08-31 재검증
- registered leaf id: `app-bootstrap`
- canonical state `analysisFile`: `analysis/18-app-bootstrap.md` (이 문서) — 이 leaf의 단일 SSOT
- source path: `src/app-bootstrap` · Gradle `:app-bootstrap`
- registry `allowed_dependencies`: `["domain-core", "application-core", "adapter-outbound-persistence-jpa", "adapter-outbound-support", "adapter-outbound-messaging", "adapter-outbound-cache-redis", "adapter-outbound-notification", "adapter-outbound-fileserver", "adapter-outbound-httpclient", "adapter-outbound-identifier", "adapter-inbound-web", "shared-contract", "adapter-outbound-persistence-mongo", "adapter-inbound-graphql", "messaging-spring-boot-starter"]`
- registry `runtime_memberships`: `["app-bootstrap"]`
- coverage ledger: `FULL_READ` **455** / `STRUCTURAL_ONLY` **0** / `EXCLUDED` **0** / `UNCLASSIFIED` **0**
- 최초 분석 revision `a24ece9c` → 재검증 revision `21234e38` · 이 리프의 변경 파일 **0**
- 재검증 증거: `EVD-333`(소스 드리프트 0), `EVD-334`(lane 재실행) — `EVD-334`의 jq/exit-78 finding과 compose 계약 독립 검증이 이 리프의 것이다
> 재검증이 확인한 것은 대상이 움직이지 않았다는 사실이지, 아래 서술이 옳다는 보증이 아니다.
> 이번 사이클에서 코드에 대고 다시 확인한 항목은 이 문서의 검증 절과 위 증거가 가리키는 범위다.
---
> **분석 대상** `src/app-bootstrap` · revision `a24ece9cf797f7ea647e33bf846b115208ed1ba5`
> **분모** 455 tracked files (main 157 · test 288 · functionalTest 4 · sampleOffTest 1 · conditionalTransportTest 1 · governance 4)
> **LOC** main Java 12,380 · test Java **32,568** (main의 2.6배)
> **근거** `evidence/raw/227-app-bootstrap-module-inventory.txt`, 이하 `228``235`
## 0. 이 모듈의 위치
컴포지션 루트다. 지금까지 분석한 17개 모듈에서 반복해서 물었던 질문 — "이 능력을 누가 조립하는가" — 의 답이 여기 있거나 없다.
**조립 표면 전체가 네 개 리소스 파일에 있다:**
```
META-INF/spring/...AutoConfiguration.imports (6줄)
bootstrap.autoconfigure.fileserver.FileserverPlatformAutoConfiguration
bootstrap.autoconfigure.httpclient.HttpClientPlatformAutoConfiguration
bootstrap.autoconfigure.persistencejpa.PersistenceJpaRootAutoConfiguration
bootstrap.autoconfigure.messaging.DisabledMessagingSentinelAutoConfiguration
bootstrap.notification.NotificationRootAutoConfiguration
bootstrap.activation.AdapterActivationAutoConfiguration
META-INF/spring.factories
EnvironmentPostProcessor = MasterSwitchEnvironmentPostProcessor,
RuntimeEnvironmentProfileValidator,
CapabilityDependencyEnvironmentValidator,
TracingSamplingEnvironmentPostProcessor,
RedisReadinessGroupPostProcessor,
DatabaseReadinessGroupPostProcessor
SpringBootExceptionReporter = StartupFailureExceptionReporter
AutoConfigurationImportFilter = JpaOffAutoConfigurationImportFilter
ApplicationListener = ResolvedProfileLoggingContextListener
META-INF/spring/...ManagementContextConfiguration.imports (1줄)
bootstrap.autoconfigure.fileserver.FileserverAdminManagementContextConfiguration
CaSkeletonApplication.java — @ComponentScan + @ConfigurationPropertiesScan
```
**여섯 개 자동설정 진입점에 web·websocket·grpc가 없다.** 그 셋은 컴포넌트 스캔에 의존하며, 모듈 14·17에서 확인했듯 각각 다른 결과를 낳았다.
## 1. 커버리지 원장
| # | sub-scope | main | test | 기타 | 합 | 상태 |
|---|---|---:|---:|---:|---:|---|
| 1 | governance + resources + `CaSkeletonApplication` + `activation` + `settings` | 21 | 37 | 4 | 62 | **COMPLETE** |
| 2 | `autoconfigure/*` (httpclient 16 · fileserver 15 · jpa 10 · persistencejpa 3 · messaging 1) | 45 | 20 | | 65 | **COMPLETE** |
| 3 | `runtime` + `runtime/startup` + `logging` + `metrics` + `tracing` | 49 | 36 | | 85 | **COMPLETE** |
| 4 | `notification` + `outbox` + `idempotency` + `messaging` + `async` + `concurrency` + `lock` | 35 | 24 | | 59 | **COMPLETE** |
| 5 | `security` + `management/security` + `redis` + `mongo` + `authz` | 7 | 5 | | 12 | **COMPLETE** |
| 6 | test: 아키텍처 규칙 + 위반/허용 픽스처 | 0 | 90 | | 90 | **COMPLETE** |
| 7 | test: contract 레인 + integration | 0 | 54 | | 54 | **COMPLETE** |
| 8 | test: onboarding 픽스처 + 잔여 + 대체 소스셋 | 0 | 22 | 6 | 28 | **COMPLETE** |
| | **TOTAL** | **157** | **288** | **10** | **455** | **8 / 8** |
`FULL_READ 455 · STRUCTURAL_ONLY 0 · EXCLUDED 0 · UNCLASSIFIED 0`. 분할은 `227-...`의 패키지 트리에서 기계 계산(중복 0 · 미할당 0).
---
# Sub-scope 01 — governance + `CaSkeletonApplication` + `activation` + `settings` (62 files)
> 근거 `evidence/raw/228-app-bootstrap-activation-probes.txt` (`file_count=62`)
## 2. 무엇을 하는 코드인가
**활성화 모델의 SSOT는 `shared-contract`의 `MasterSwitch` enum이다:**
```java
/**
* The five adapters this skeleton ships behind an explicit switch, and the names that address them.
*
* <p>One place knows the names. Spread across conditions as string literals, a rename becomes a
* silent activation change: the condition stops matching, the adapter stops assembling, and
* nothing reports it.
*/
public enum MasterSwitch {
PERSISTENCE_JPA ("ca-skeleton.persistence-jpa.enabled", "APP_PERSISTENCE_JPA_ENABLED"),
PERSISTENCE_MONGO ("ca-skeleton.persistence-mongo.enabled", "APP_PERSISTENCE_MONGO_ENABLED"),
MESSAGING ("app.messaging.enabled", "APP_MESSAGING_ENABLED"),
NOTIFICATION_PLATFORM("ca-skeleton.notification.platform.enabled", "APP_NOTIFICATION_PLATFORM_ENABLED"),
GRAPHQL ("backend.graphql.enabled", "APP_GRAPHQL_ENABLED");
```
그 enum이 네 개 장치를 구동한다:
| 장치 | 하는 일 |
|---|---|
| `MasterSwitchEnvironmentPostProcessor` (71) | 값이 정확히 `true`/`false`가 아니면 부팅 거부 |
| `CapabilityDependencyValidator` (156) | 능력 간 의존(예: outbox는 JPA+messaging 필요) 검증 |
| `AdapterActivationEndpoint` (74) → `AdapterActivationReport` (34) | 해석된 상태를 액추에이터로 발행 |
| `MasterSwitchRegistryContractTest` | enum ↔ `docs/registries/env-keys.yaml``application.yml` 삼자 일치 |
`MasterSwitchEnvironmentPostProcessor`의 거부 메시지가 이 모듈의 성격을 보여준다:
> "%s must be exactly true or false, but was \"%s\". **A value this close to a boolean is a deployment that believes it set the switch; it is rejected rather than read as off.**"
그리고 원시 값을 읽는 이유도 적혀 있다 — "An adapter that is off has to be able to start next to an environment full of its own malformed configuration — **a validator that bound the namespace to check the switch would fail exactly the deployments the switch exists to protect.**"
`AdapterActivationReport`는 "설정된 것"이 아니라 "해석된 것"을 낸다:
> "A smoke lane that asserts on the environment it passed in is **asserting on its own input**. This is the application's answer: the profile it settled on, the state each master switch parsed to, and which capabilities are asking for a relational connection."
`MasterSwitchRegistryContractTest`가 삼자 일치를 enum에서 파생시킨다 — "**Deriving every case from `MasterSwitch` means a sixth adapter cannot be added without this test demanding its row.**"
## 3. Negative-space probes — sub-scope 01
### 3.1 (8.4) 카운트 드리프트 — "다섯 어댑터"와 실제 스위치를 가진 어댑터
`MasterSwitch`가 다섯을 담는다. 저장소가 실제로 출하하는, 기본 꺼짐이고 운영자가 켜는 인바운드 어댑터 스위치를 전수하면:
| 스위치 | 기본 | `MasterSwitch` | env-keys 레지스트리 | 확인 출처 |
|---|---|---|---|---|
| `ca-skeleton.persistence-jpa.enabled` | off | 예 | 예 | — |
| `ca-skeleton.persistence-mongo.enabled` | off | 예 | 예 | — |
| `app.messaging.enabled` | off | 예 | 예 | — |
| `ca-skeleton.notification.platform.enabled` | off | 예 | 예 | — |
| `backend.graphql.enabled` | off | 예 | 예 | — |
| **`ca-skeleton.grpc.enabled`** | **off** | **아니오** | **아니오** | 모듈 15 §2 |
| **`ca-skeleton.websocket.enabled`** | **off** | **아니오** | **아니오** | 모듈 17 §14 |
| **`backend.websocket.enabled`** | **off** | **아니오** | **아니오** | 모듈 17 §0 |
| `backend.web.mvc.enabled` · `backend.web.webflux.enabled` | **on**(`matchIfMissing=true`) | 아니오 | 아니오 | 모듈 14 §3.2 |
| `backend.web.budgets.enabled` · `app.web-platform.durable-operations.enabled` | off | 아니오 | 아니오 | 모듈 14 §15.4 · §19.2 |
| `app.websocket-platform.advanced.*` (4종) | off | 아니오 | 아니오 | 모듈 17 §0 |
기계 확인:
```
$ grep -rn 'ca-skeleton.grpc|ca-skeleton.websocket|backend.web.mvc|backend.websocket' \
--include=*.java app-bootstrap/src/main app-bootstrap/src/test -> 0
$ grep -in 'grpc|websocket' docs/registries/env-keys.yaml -> 0 (341개 키 등록)
$ grep -in 'grpc|websocket' docs/registries/capabilities.yaml -> 0
$ grep -n 'grpc|websocket' src/config/architecture/modules.json -> 등록된 leaf로는 존재
```
**모듈 레지스트리는 두 leaf를 알고, 활성화 모델과 운영자용 env 레지스트리는 모르는 상태다.** §4.1.
### 3.2 (8.1) 도달성 — 여섯 자동설정 진입점이 덮는 범위
| 진입점 | 대상 leaf | 모듈 분석에서의 결과 |
|---|---|---|
| `FileserverPlatformAutoConfiguration` + `FileserverStartupConfiguration` | outbound fileserver + inbound web의 `fileserver/**` | 모듈 14 §38 — **이 leaf에서 유일하게 완전 조립된 하위 트리** |
| `HttpClientPlatformAutoConfiguration` | outbound httpclient | 모듈 11 |
| `PersistenceJpaRootAutoConfiguration` + `JpaOffAutoConfigurationImportFilter` | persistence-jpa | 모듈 5 |
| `DisabledMessagingSentinelAutoConfiguration` | messaging | 모듈 12 |
| `NotificationRootAutoConfiguration` | outbound notification | 모듈 13 |
| `AdapterActivationAutoConfiguration` | 자기 자신(액추에이터) | — |
**inbound web · websocket · grpc에 대응하는 진입점이 없다.** 그 셋은 컴포넌트 스캔으로만 조립되고, 스캔 경계(`AUTO_CONFIGURED_PACKAGES` 정규식)가 web의 다섯 패키지를 제외하면서 그것을 넘겨받을 자동설정을 만들지 않은 것이 모듈 14 §8.1이다.
### 3.3 (8.2) 조건 형제 비교 — 두 종류의 "꺼짐"
| 방식 | 예 | 꺼졌을 때의 상태 |
|---|---|---|
| 마스터 스위치 + 게이트된 루트 자동설정 | jpa · mongo · messaging · notification · graphql | 빈 0개 + 설정 바인딩도 안 됨(`@EnableConfigurationProperties`가 게이트 안쪽) |
| 컴포넌트 스캔 + `@ConditionalOnProperty` | grpc · websocket · web | 빈 0개 + **설정은 바인딩됨**(`@ConfigurationPropertiesScan`이 세 패키지를 포함) |
`CaSkeletonApplication`의 javadoc이 첫 번째 방식의 이유를 적는다:
> "The asymmetry that existed before — **beans gated, settings not** — is why a notification settings object bound itself in a deployment whose notification master was off. A capability whose beans are gated but whose settings still bind is gated only where somebody remembered to gate it."
그리고 두 번째 방식이 정확히 그 비대칭이다 — `dev.caskeleton.adapter.inbound.grpc` · `.web` · `.websocket` 세 패키지가 `@ConfigurationPropertiesScan` 목록에 있다.
### 3.4 (8.3) 중복 메커니즘 — 세 개의 환경 검증기
`MasterSwitchEnvironmentPostProcessor`(스위치 값 문법) · `RuntimeEnvironmentProfileValidator`(93, 프로파일) · `CapabilityDependencyEnvironmentValidator`(62 → `CapabilityDependencyValidator` 156, 능력 간 의존). 셋 다 `EnvironmentPostProcessor`이고 관심사가 다르다. 중복 아님.
## 4. Sub-scope 01 findings
### 4.1 — 다섯 어댑터 범위는 런타임 멤버십 레지스트리와 일치한다 (결함 아님)
§3.1의 표를 처음에는 "출하되는 스위치가 다섯보다 많다"는 결함으로 기록했다. 그 판정은 **틀렸다**. `src/config/architecture/modules.json``runtime_memberships`가 결정적이다:
```
adapter-inbound-web runtime_memberships = ["app-bootstrap", "sample-portfolio"]
adapter-inbound-graphql runtime_memberships = ["app-bootstrap"]
adapter-inbound-grpc runtime_memberships = []
adapter-inbound-websocket runtime_memberships = []
```
**gRPC와 WebSocket은 build-only leaf다** — 어떤 런타임에도 올라가지 않는다. 그리고 그 사실이 기계로 강제된다:
```java
// conditionalTransportTest/.../ConditionalTransportCompositionContractTest
/** Transports that must not reach any runtime: no membership, and no composition-root edge. */
private static final Map<String, String> BUILD_ONLY_TRANSPORTS =
Map.of("adapter-inbound-grpc", "dev.caskeleton.adapter.inbound.grpc.GrpcServerConfig",
"adapter-inbound-websocket", "dev.caskeleton.adapter.inbound.websocket.stomp.WebSocketConfig");
```
그 테스트의 javadoc이 세 전송의 등급을 나눈다:
> "**gRPC and WebSocket are build-only — no membership, and nothing may put them on a runtime.** GraphQL is shipped and switch-gated, which is a stronger claim and carries a stronger obligation: **the switch has to be the thing that decides.**"
따라서 두 어댑터가 `MasterSwitch`·`env-keys.yaml`·`AdapterActivationReport`에 없는 것은 누락이 아니라 **일관성**이다. 런타임에 오르지 않는 어댑터에는 운영자용 활성화 스위치가 필요하지 않다.
**남는 것은 `backend.web.*` 하나다.** `adapter-inbound-web`은 두 런타임에 올라가고(`["app-bootstrap","sample-portfolio"]`), 그 스위치들 — `backend.web.mvc.enabled` · `backend.web.webflux.enabled`(둘 다 `matchIfMissing=true`, **기본 켜짐**) · `backend.web.budgets.enabled` · `app.web-platform.durable-operations.enabled` — 은 `MasterSwitch`에도 `env-keys.yaml` 341개 키에도 없다. §4.1b.
### 4.1b P3 — 출하되는 web 어댑터의 스위치가 활성화 모델 밖에 있다
§4.1. `adapter-inbound-web`은 두 런타임 멤버이므로 build-only 예외에 해당하지 않는다. 그런데 그 네 개 스위치가 조건 안의 문자열 리터럴로만 존재해 `MasterSwitch`의 javadoc이 경계하는 상태다 — "Spread across conditions as string literals, a rename becomes a silent activation change."
**다만 web은 다른 넷과 성질이 다르다.** MVC/WebFlux 스위치는 `matchIfMissing = true`로 **기본 켜짐**이므로 "옵션 어댑터"가 아니고, `MasterSwitch`가 규정하는 "explicit switch" 모델(전부 기본 꺼짐, `env-keys.yaml`에 행이 있고 삼자 일치 테스트가 강제)에 그대로 넣을 수 없다. 그리고 모듈 14 §8.1이 확인했듯 web의 조립 자체가 미해결이다 — 스캔에서 다섯 패키지를 빼고 넘겨받는 자동설정을 만들지 않은 상태다.
기록하는 것은 순서다: web의 스위치를 활성화 모델에 넣는 것은 모듈 14 §8.1(어떤 자동설정이 무엇을 소유하는가)을 먼저 정한 뒤에 할 수 있는 일이다. 지금은 "무엇을 스위치로 부를지"가 결정되지 않았다.
### 4.1c P3/기록 — 조건부 전송 게이트가 빨간 채로 방치된 이력이 기록돼 있다
`ConditionalTransportCompositionContractTest`의 javadoc이 자기 이력을 적는다:
> "This test asserted that all three transports had `runtime_memberships: []` and that their classes load. **Both halves aged badly.** ... The test went red the moment that landed and **nobody saw it, because this suite runs in `conditionalTransportQualification` rather than in `test`** — and `conditionalTransportQualification` is one of the two commands CI runs. **A gate that is red in a lane nobody runs locally is a gate that reports whatever the last person to run it saw.**"
그리고 두 번째 절반에 대한 판정도 남긴다 — "A type resolving proves the jar is on a classpath; it says nothing about whether a composition assembles the transport... **class existence is not composition evidence.**"
이 문장이 모듈 14 §48.1·모듈 17 §4.1과 같은 원칙을 다른 각도에서 말한다. 기록으로 남긴다.
### 4.2 P3/기록 — 세 인바운드 leaf의 설정이 마스터 스위치 밖에서 바인딩된다
§3.3. `CaSkeletonApplication`의 javadoc이 "beans gated, settings not"을 과거의 사고로 기록하면서, 그 형태를 grpc·web·websocket 세 패키지에 대해 유지한다. 모듈 15 §4.2에서 확인했듯 grpc에서는 기본값이 전부 유효해 무해하고, 모듈 17 §4.1에서는 `backend.websocket` 설정이 바인딩만 되고 소비되지 않는 상태를 만든다.
---
# Sub-scope 02 — `autoconfigure/*` (65 files, main 45 + test 20)
> 근거 `evidence/raw/229-app-bootstrap-autoconfigure-probes.txt` (`file_count=65`)
## 5. 무엇을 하는 코드인가
다섯 개 능력의 조립 루트. `httpclient`(16) · `fileserver`(15) · `jpa`(10) · `persistencejpa`(3) · `messaging`(1).
**`fileserver`(15)가 이 저장소에서 가장 완전한 조립 사례다.** 모듈 14 §38에서 확인했듯 `FileserverPlatformAutoConfiguration`이 URI 매퍼·다운로드 전략·요청 컨텍스트 팩토리를 만들고, `FileserverStartupConfiguration:87``attestMapping()`을 시작 시 호출하며, `FileserverAdminManagementContextConfiguration`이 관리 평면을 **별도 관리 컨텍스트**에 등록한다 — `CaSkeletonApplication`이 그 이유를 적는다: "those routes belong to the management context, and a component scan that also found them would publish the management plane on the public connector — the exposure the separate context exists to remove."
**`persistencejpa`(3)**는 `JpaOffAutoConfigurationImportFilter`를 포함한다 — graphql의 `GraphQlOffAutoConfigurationImportFilter`(모듈 16 §3.2)와 같은 형태로, 프레임워크 자동설정을 후보 집합에서 제거해 "빈은 없는데 프레임워크가 서비스하는" 상태를 막는다. `DataSourceRequirement``MasterSwitch`를 직접 읽어 어떤 능력이 관계형 연결을 요구하는지 계산한다.
**`messaging`(1)**은 `DisabledMessagingSentinelAutoConfiguration` 하나다 — 꺼진 메시징을 사용하려 할 때 조용한 no-op 대신 실패를 내는 센티널.
## 6. Negative-space probes
### 6.1 (8.1) 도달성
45개 main 파일 중 다른 main 파일이 참조하지 않는 것은 전부 `@Configuration`/`@AutoConfiguration` 루트이며, 이는 `.imports`가 진입점으로 등록하므로 정상이다. 고아 파일 없음.
### 6.2 (8.2) 조건 형제 비교 — 두 off 필터
`JpaOffAutoConfigurationImportFilter`(app-bootstrap)와 `GraphQlOffAutoConfigurationImportFilter`(graphql leaf). 같은 문제를 같은 방식으로 푼다. 차이는 위치다 — JPA 것은 컴포지션 루트에, GraphQL 것은 leaf에 있다. 둘 다 `spring.factories`에 등록돼 있고 동작한다.
**web·websocket에는 대응물이 없다.** 그 둘은 프레임워크가 발행하는 엔드포인트를 갖지 않으므로(자체 `@Configuration`이 등록) 필요하지 않다.
### 6.3 (8.4) 카운트 — `.imports` 여섯 줄과 다섯 능력
`.imports`의 여섯 항목 중 다섯이 능력 루트이고 하나(`AdapterActivationAutoConfiguration`)가 자기 액추에이터다. `MasterSwitch`의 다섯과 일치한다 — 단 `PERSISTENCE_MONGO``.imports`에 루트가 없고 컴포넌트 스캔 제외 정규식(`adapter\.outbound\.mongo\..*`)으로만 관리된다. §7.1.
## 7. Findings
### 7.1 P3/기록 — `PERSISTENCE_MONGO`만 자동설정 루트가 없다
다섯 마스터 스위치 중 넷은 `.imports`에 게이트된 루트를 갖는다(jpa · messaging 센티널 · notification · graphql은 자기 leaf에). Mongo는 `AUTO_CONFIGURED_PACKAGES` 정규식의 `adapter\.outbound\.mongo\..*` 제외만 있고 그것을 넘겨받는 루트가 `.imports`에 없다.
모듈 14 §8.1이 정확히 그 형태의 결함이었다(제외했는데 넘겨받지 않음). Mongo에서 같은 상태인지는 이 sub-scope의 파일만으로는 판정되지 않는다 — outbound mongo leaf(모듈 6)가 자기 `.imports`를 갖는지 확인이 필요하고, 그 모듈은 이미 COMPLETE로 분석됐으나 이 관점에서 재확인하지 않았다. 기록으로 남긴다.
---
# Sub-scope 03 — `runtime` + `runtime/startup` + `logging` + `metrics` + `tracing` (85 files, main 49 + test 36)
> 근거 `evidence/raw/230-app-bootstrap-runtime-probes.txt` (`file_count=85`)
## 8. 무엇을 하는 코드인가 — 이 저장소에서 시작 검증이 실제로 도는 곳
**열두 개 검증기가 전부 배선돼 있다:**
```
StartupSafetyValidator (103) main_refs=3 RuntimeSafetyConfig
SecretSourceValidator (182) main_refs=3 SecretSourceConfig
RequiredEnvironmentValidator (49) main_refs=1
HikariPoolConstraintValidator main_refs=2
JpaSchemaSafetyValidator main_refs=1
OpenInViewSafetyValidator main_refs=1
PersistenceVendorProdSafetyValidator main_refs=2
PostgreSqlTransportSecurityValidator main_refs=2
RedisActivationValidator main_refs=2
RuntimeNumericBoundsValidator main_refs=1
FlywayProdSafetyValidator main_refs=1
MigrationStartupRunner (53) main_refs=1 MigrationStartupConfig
```
`RuntimeSafetyConfig`(`@Configuration`, `@Bean` 7개)가 "Wires the runtime-safety startup fail-fast validators into the running application"을 한다.
`StartupSafetyValidator``SmartInitializingSingleton`인 이유가 적혀 있다 — "the check runs once after every singleton is instantiated but before the context finishes refreshing; a violation throws so the context refuses to start."
**시작 실패의 어휘가 구조화돼 있다.** `StartupFailures`(75)가 단일 발생원이고 "Every factory method emits the structured failure log (with the `startup.phase` / `error.code` / `error.category` fields) before returning the exception to throw." `StartupErrorCode` · `StartupPhase` · `StartupFailureLogState`가 그 어휘이고, `StartupFailureExceptionReporter``SpringBootExceptionReporter`로 등록돼 컨텍스트가 없는 시점의 실패도 같은 형식으로 낸다. 종료 코드까지 규정한다(`STARTUP_VALIDATION_FAILED` = exit 78, `MigrationFailedException` = exit 70).
**`logging`(8)은 logback을 통해 배선된다.** `MetricsAsyncAppender` · `SamplingTurboFilter` · `SecretMaskingMessageConverter` · `SecretMaskingJsonGeneratorDecorator` · `StartupFailureSpringBootLogFilter` 다섯이 Java 참조가 0인데, `logback-spring.xml`이 클래스 이름으로 등록한다(`:56` · `:59` · `:62` · `:91` · `:124` · `:137` · `:148`). `ResolvedProfileLoggingContextListener``spring.factories``ApplicationListener`이고, 그 이유가 주석에 있다 — "An `ApplicationListener`, not a bean: this event fires before there is a context to hold one."
## 9. Negative-space probes
### 9.1 (8.1) 도달성 — main 참조 0인 파일의 전수 분류
49개 main 파일 중 다른 main 파일이 참조하지 않는 것은 열둘이고, 전부 설명된다:
| 분류 | 파일 | 등록 경로 |
|---|---|---|
| `@Configuration` 루트 | `RuntimeSafetyConfig` · `SecretSourceConfig` · `SecretSourceSettings` · `MigrationStartupConfig` · `TracingConfig` · `PseudonymizationConfig` | 컴포넌트 스캔 |
| logback 컴포넌트 | `MetricsAsyncAppender` · `SamplingTurboFilter` · `SecretMaskingMessageConverter` · `SecretMaskingJsonGeneratorDecorator` · `StartupFailureSpringBootLogFilter` | `logback-spring.xml` |
| `spring.factories` | `ResolvedProfileLoggingContextListener` · `TracingSamplingEnvironmentPostProcessor` | `spring.factories` |
**고아 없음.** 지금까지 분석한 17개 모듈 중 이 성질을 가진 것은 grpc(모듈 15)와 여기뿐이다.
### 9.2 (8.2) 조건 형제 비교 — 시작 검증기의 운명
| 모듈 | 검증기 | 배선 |
|---|---|---|
| app-bootstrap | 12종 | **전부** |
| outbound fileserver ← app-bootstrap | `NginxInternalUriMapper.attestMapping()` | 예 (모듈 14 §39.1) |
| inbound web | `WebPlatformStartupValidator` (62) | 아니오 (모듈 14 §44.2) |
| inbound websocket | `WebSocketPlatformStartupValidator` (125) · `WebSocketStackExclusivity` (78) | 아니오 (모듈 17 §4.1) |
| inbound graphql | `GraphQlPlatformStartupValidator` | 예 (leaf 자기 자동설정) |
**검증기가 도는지 여부는 그 능력에 컴포지션 루트의 자동설정이 있는지와 정확히 일치한다.**
### 9.3 (8.3)·(8.4) 중복·드리프트 — 없음
`StartupFailures`가 단일 발생원이고 종료 코드·단계·카테고리가 한 어휘에 모여 있다. 검증기 12종이 서로 다른 불변식을 보며 겹치지 않는다.
## 10. Findings — 없음
---
# Sub-scope 04 — `notification` + `outbox` + `idempotency` + `messaging` + `async` + `concurrency` + `lock` (59 files, main 35 + test 24)
> 근거 `evidence/raw/231-app-bootstrap-capability-probes.txt` (`file_count=59`)
## 11. 무엇을 하는 코드인가
능력별 조립. `notification`(10) + `notification/observation`(4)이 모듈 13에서 확인한 배선의 출처다 — `NotificationRootAutoConfiguration`(`.imports` 등록, `@ConditionalOnProperty`) · `NotificationPlatformWorkerConfig`(모듈 13 §12.1이 확인한 배경 작업자 3종 등록) · `NotificationPlatformProviderConfig` · `NotificationPlatformDispatchConfig` · `NotificationPlatformObservabilityConfig`.
`outbox`(6) · `idempotency`(5) · `messaging`(1, `KafkaSenderConfig` 113) · `async`(5) · `concurrency`(2) · `lock`(2)이 나머지다.
## 12. Negative-space probes
### 12.1 (8.1) 도달성
35개 main 파일 중 main 참조 0인 것은 전부 `@Configuration` 루트다(`IdempotencyConfig` · `PostgreSqlIdempotencyProviderConfig` · `AsyncExecutorConfig` · `DomainContextConfig` · `DistributedLockConfig` · `KafkaSenderConfig` · `NotificationRootAutoConfiguration`). 고아 없음.
### 12.2 (8.2) 조건 형제 비교 — 모듈 13의 미배선 항목이 여기 있는가
모듈 13에서 P2로 기록한 다섯 중 컴포지션 루트가 고칠 수 있는 것을 확인했다:
| 모듈 13 발견 | app-bootstrap에 배선 지점이 있는가 |
|---|---|
| §17.1 `AccessContext` 감사 미기록 | 아니오 — leaf 내부 문제(복호화 경로가 감사 포트를 부르지 않음) |
| §25.1 Web Push SSRF 가드 미적용 | 아니오 — leaf 내부(`WebPushSubscriptionValue`의 private 사본) |
| §29.1 FCM ambiguous 미번역 | 아니오 — leaf 내부(`FcmBatchCoordinator`) |
| §12.1 배경 작업자 3종 | **예 — 배선됨**(`NotificationPlatformWorkerConfig:57·90·119`) |
즉 컴포지션 루트가 할 수 있는 부분은 이미 돼 있고, 남은 것은 leaf 내부의 회로다.
## 13. Findings — 없음
---
# Sub-scope 05 — `security` + `management/security` + `redis` + `mongo` + `authz` (12 files, main 7 + test 5)
> 근거 `evidence/raw/232-app-bootstrap-security-probes.txt` (`file_count=12`)
## 14. 무엇을 하는 코드인가
`security/AuthenticationModeCompositionConfig`(48)가 JWT/Redis-세션 두 인증 모드의 조립을 결정하고, `management/security`(1)가 관리 평면 보안을, `redis/RedisCapabilityConfig`(376 — 이 모듈에서 가장 큰 단일 파일)가 Redis 능력 전체를, `mongo/MongoPlatformHealthConfig`(37)가 Mongo 헬스를, `authz`(1)가 권한 배선을 담당한다.
## 15. Negative-space probes
### 15.1 (8.1)·(8.2) 도달성과 게이트
7개 main 파일 전부 `@Configuration`이고 `@ConditionalOnProperty`/`@ConditionalOnBean`으로 게이트된다. `MongoPlatformHealthConfig``@ConditionalOnBean` + `@ConditionalOnMissingBean` + `@ConditionalOnProperty` 셋을 함께 쓰는데, 자동설정 안에서의 `@ConditionalOnBean`은 Boot가 평가 순서를 통제하므로 모듈 14 §7.1이 경고한 컴포넌트 스캔 상의 위험이 없다.
## 16. Findings — 없음
---
# Sub-scope 06 — test: 아키텍처 규칙 + 위반/허용 픽스처 (90 files)
> 근거 `evidence/raw/233-app-bootstrap-architecture-probes.txt` (`file_count=90`)
## 17. 무엇을 하는 코드인가
**아키텍처 규칙 14종이 여기서 프로덕션 트리에 적용된다:**
```
ArchitectureViolationFixtureTest CleanArchitectureTest ContractSuiteIsolationArchTest
DisabledAdapterArchitectureTest DomainFeatureOnboardingContractTest FileserverUseCaseContractDeviationTest
GraphQlInboundOwnershipBoundaryTest JpaProductionArchitectureTest MongoRawAccessBoundaryTest
NamingConventionTest NotificationArchitectureTest ProductionClassImportOption
TestTaxonomyArchitectureTest WebProductionArchitectureTest
```
`WebProductionArchitectureTest`가 모듈 14 §46에서 확인한 `WebArchitectureRules.all()` 7규칙을 프로덕션 트리에 적용하는 지점이고, `NotificationArchitectureTest`·`JpaProductionArchitectureTest`·`MongoRawAccessBoundaryTest`·`GraphQlInboundOwnershipBoundaryTest`가 각 leaf의 규칙 팩에 대해 같은 일을 한다.
**위반/허용 픽스처가 76개다.** `architecture/violations/**`(application 14 · domain 5 · streaming 4 · boundary 3 · adapter 5 · slice 2 · fixtureleak 4 · contractisolation 4 · shared 1 · serialization 1 …)와 `architecture/allowed/**`(streaming 2 · slice 2 · contractisolation 4 · application 2). 규칙마다 **거부되어야 할 합성 트리**와 **통과해야 할 합성 트리**를 함께 두는 형태다.
모듈 14 §3.4에서 확인한 `WebModuleBoundaryTest`의 부정 픽스처 넷과 같은 원칙이 저장소 규모로 적용돼 있다 — "A boundary test that has never been shown to fail is indistinguishable from one that scans the wrong directory."
## 18. Negative-space probes
### 18.1 (8.1)·(8.4) 규칙과 픽스처의 대응
14개 규칙 클래스에 76개 픽스처가 붙는다. `ArchitectureViolationFixtureTest`가 위반 픽스처들이 실제로 거부되는지를 확인하는 메타 테스트이고, `TestTaxonomyArchitectureTest`가 테스트 자체의 분류 규약을 강제한다.
`ProductionClassImportOption`이 스캔 대상을 프로덕션 클래스로 한정하는 공유 옵션 — 픽스처가 규칙에 잡히지 않도록 하는 장치다. `architecture/violations/fixtureleak`(4)는 그 장치가 새는 경우를 픽스처로 고정한다.
### 18.2 (8.3) 중복 메커니즘 — 규칙 팩의 위치
leaf가 규칙 팩을 소유하고(`WebArchitectureRules` in web testkit) 컴포지션 루트가 적용하는 형태와, 컴포지션 루트가 규칙을 직접 쓰는 형태(`CleanArchitectureTest`)가 공존한다. 전자는 leaf가 자기 규칙을 소유하고 후자는 저장소 전역 규칙이므로 역할이 다르다. 중복 아님.
## 19. Findings — 없음
---
# Sub-scope 07 — test: contract 레인 + integration (54 files)
> 근거 `evidence/raw/234-app-bootstrap-contract-probes.txt` (`file_count=54`)
## 20. 무엇을 하는 코드인가
30개 계약 테스트가 저장소 전역 불변식을 확인한다. 성격별로:
| 성격 | 예 |
|---|---|
| 레지스트리 일치 | `ErrorCodeRegistryMappingTest` · `SecretsClassificationRegistryTest` · `ContractRegistrySchemaGovernanceTest` · `RepositoryAccessCapabilityRegistryTest` · `RegistryGovernanceCatalog` |
| 관측·로그 계약 | `StructuredLogFieldContractTest` · `MetricsAlertingContractTest` · `DistributedTracingContractTest` · `SqlLoggingForbiddenContractTest` · `PiiTokenBodyForbiddenContractTest` |
| 보안 표면 | `ActuatorSecurityHttpTest` · `ManagementActuatorSecurityContractTest` · `ProfileSeparationContractTest` |
| 활성화·조건부 | `OptionalAdapterConditionalExecutionContractTest` · `ConditionalTransportQualificationContractTest` · `RedisOptionalityContractTest` · `EnvProfileMatrixContractTest` |
| 실패 분류 | `PersistenceFailureMappingContractTest` · `LockFailureClassificationContractTest` · `LockAcquisitionTimeoutClassificationContractTest` · `BackgroundJobErrorCodeContractTest` |
| 운영 | `RunbookCoverageContractTest` · `ContainerRuntimeOomContractTest` · `SecretReloadContractTest` · `DeveloperExperienceContractTest` |
| 자기 검사 | `ContractSuiteCompletenessTest` |
`ContractSuiteCompletenessTest`가 존재한다는 사실이 이 sub-scope의 성격을 말한다 — 계약 스위트 자체의 완전성을 검사하는 계약이다.
## 21. Negative-space probes
### 21.1 (8.2) 조건 형제 비교 — 세 전송의 조건부 실행 증거
`ConditionalTransportQualificationContractTest`(test 레인)와 `ConditionalTransportCompositionContractTest`(별도 `conditionalTransportTest` 소스셋)와 `ConditionalTransportEvidenceFunctionalTest`(`functionalTest` 소스셋) 셋이 같은 축을 다른 레인에서 본다.
두 번째 것의 javadoc이 §4.1c에서 인용한 자기 이력을 담는다 — 레인 분리가 만든 사각지대를 스스로 기록한 사례다.
### 21.2 (8.1) 도달성 — 레지스트리 계약이 실제 레지스트리 파일을 읽는가
`MasterSwitchRegistryContractTest``docs/registries/env-keys.yaml``src/app-bootstrap/src/main/resources/application.yml`을 실제로 읽어 대조한다(§2). `ErrorCodeRegistryMappingTest`·`SecretsClassificationRegistryTest``docs/registries/` 아래 파일을 읽는다. 파일 기반 SSOT가 테스트로 고정돼 있다.
## 22. Findings — 없음
---
# Sub-scope 08 — test: onboarding 픽스처 + 잔여 + 대체 소스셋 (28 files)
> 근거 `evidence/raw/235-app-bootstrap-testrest-probes.txt` (`file_count=28`)
## 23. 무엇을 하는 코드인가
`dev.caskeleton.onboarding.**`(14 파일)이 **저장소 밖 패키지의 합성 feature**다 — `onboarding/domain/feature` · `application/usecase` · `application/query` · `application/port` · `adapter/inbound/web/dto` · `adapter/outbound`. `DomainFeatureOnboardingContractTest`(§17)가 이것을 대상으로 "새 feature를 추가하는 절차"가 규칙을 만족하는지 확인한다.
대체 소스셋 셋:
```
conditionalTransportTest/ ConditionalTransportCompositionContractTest (§4.1)
functionalTest/ BuildVerificationPurityContractTest
ConditionalTransportEvidenceFunctionalTest
RuntimeMembershipFunctionalTest
StrictQualificationTestConventionFunctionalTest
sampleOffTest/ SampleOffClasspathContractTest
```
`RuntimeMembershipFunctionalTest``modules.json``runtime_memberships`를 실제 부트 JAR에 대해 확인하는 것으로 보이며, 그것이 §4.1의 build-only 판정을 뒷받침하는 두 번째 증거다.
`SampleOffClasspathContractTest`(`sampleOffTest` 소스셋)는 샘플 모듈을 제외한 클래스패스에서의 계약을 확인한다 — 스켈레톤이 샘플 없이 성립하는지의 증거다.
## 24. Findings — 없음
---
# 25. 모듈 종합 — `app-bootstrap`
## 25.1 커버리지 원장 정산
8개 sub-scope, **455 / 455 FULL_READ** · `STRUCTURAL_ONLY 0 · EXCLUDED 0 · UNCLASSIFIED 0`.
## 25.2 발견 종합 — P1 0건 · P2 0건 · P3 3건 · 기록 2건
| 심각도 | § | 발견 |
|---|---|---|
| P3 | 4.1b | 출하되는 `adapter-inbound-web`의 스위치 넷이 활성화 모델(`MasterSwitch`·`env-keys.yaml`·액추에이터·삼자 일치 테스트) 밖에 있다. 다만 둘은 기본 켜짐이라 "explicit switch" 모델에 그대로 넣을 수 없고, 모듈 14 §8.1(조립 소유권)을 먼저 정해야 한다 |
| P3 | 26.1 | `ComposeMergeCharacterizationTest``docker compose` 부재는 skip하고 `jq` 부재는 실패로 처리한다 — 같은 테스트의 도구 가드가 불완전하다 |
| P3/기록 | 7.1 | 다섯 마스터 스위치 중 `PERSISTENCE_MONGO``.imports`에 게이트된 루트가 없고 스캔 제외 정규식만 있다 |
| 기록 | 4.1c | 조건부 전송 게이트가 별도 레인에서만 돌아 빨간 채로 방치된 이력이 그 테스트 javadoc에 기록돼 있다 |
| 기록 | 4.2 | 세 인바운드 leaf(grpc·web·websocket)의 설정이 `@ConfigurationPropertiesScan`으로 마스터 스위치 밖에서 바인딩된다 |
## 25.3 이 모듈의 성격 — 조립이 실제로 일어나는 곳
**main 157 파일에 고아가 없다.** main 참조가 0인 파일은 전부 설명된다 — `@Configuration`/`@AutoConfiguration` 루트(`.imports` 또는 컴포넌트 스캔), logback 컴포넌트(`logback-spring.xml`이 클래스 이름으로 등록), `spring.factories` 항목(`EnvironmentPostProcessor` 6 · `SpringBootExceptionReporter` · `AutoConfigurationImportFilter` · `ApplicationListener`).
지금까지 분석한 18개 모듈 중 이 성질을 가진 것은 grpc(모듈 15)와 여기뿐이다.
**시작 검증기 12종이 전부 배선돼 있다**(§8). 그리고 그 사실이 다른 모듈의 결과를 설명한다:
| 검증기 | 배선 | 대응 자동설정 |
|---|---|---|
| app-bootstrap `runtime/*` 12종 | 예 | `RuntimeSafetyConfig` 등 |
| fileserver `attestMapping()` | 예 | `FileserverStartupConfiguration` (app-bootstrap) |
| graphql `GraphQlPlatformStartupValidator` | 예 | leaf 자기 자동설정 |
| web `WebPlatformStartupValidator` | **아니오** | **없음** |
| websocket `WebSocketPlatformStartupValidator` · `WebSocketStackExclusivity` | **아니오** | **없음** |
**검증기가 도는지 여부가 그 능력에 자동설정 루트가 있는지와 정확히 일치한다.** 그것이 이 저장소의 조립 결함을 설명하는 단일 규칙이다.
## 25.4 이 모듈이 나머지 분석을 교정했다
app-bootstrap을 읽고 두 가지가 바뀌었다:
1. **`modules.json``runtime_memberships`가 build-only 등급을 규정한다** — grpc·websocket은 `[]`, graphql은 `["app-bootstrap"]`, web은 `["app-bootstrap","sample-portfolio"]`. 그리고 `ConditionalTransportCompositionContractTest`가 "**nothing may put them on a runtime**"을 기계로 강제한다. 이것으로 모듈 17 §4.1을 P1 → P2로 하향했다(모듈 17 §26.6).
2. **`MasterSwitch` 다섯의 범위가 자의적이지 않다** — 런타임에 오르는 어댑터 중 옵션인 것들이다. 처음에 "출하 스위치가 다섯보다 많다"로 기록한 것을 §4.1에서 철회했다.
두 교정 모두 컴포지션 루트를 읽지 않고는 도달할 수 없었다. **조립 결함을 판정하려면 조립하는 쪽을 먼저 읽어야 한다**는 것이 이 모듈이 남긴 방법론적 결론이다.
## 26. 실행 검증
```
$ ./gradlew :app-bootstrap:test
BUILD FAILED in 1m 47s
GRADLE_EXIT=1
test-results 집계: classes=169 tests=1016 failures=1 errors=0 skipped=4
FAILED: ComposeMergeCharacterizationTest.every lane renders exactly the services its contract names
```
### 26.1 P3 — 실패는 환경 원인이며, 그 테스트의 도구 가드가 불완전하다
```
org.opentest4j.AssertionFailedError: [verify-compose-profile-contracts.sh said:
jq is required
]
expected: 0 but was: 78
at ComposeMergeCharacterizationTest.everyLaneMatchesItsContract(ComposeMergeCharacterizationTest.java:62)
$ which jq -> NO_JQ
```
**저장소 결함이 아니다.** 분석 컨테이너에 `jq`가 없고, 위임된 스크립트가 그것을 요구하며 exit 78로 정직하게 실패한다.
기록하는 것은 가드의 비대칭이다:
```java
@Test
void everyLaneMatchesItsContract() {
Assumptions.assumeTrue(dockerComposeIsAvailable(), "docker compose is not on this machine");
ProcessResult result = run(List.of("./scripts/verify-compose-profile-contracts.sh"));
assertThat(result.exitCode()).isZero();
}
```
`docker compose` 부재는 **skip**으로 처리하고, 같은 스크립트가 요구하는 `jq` 부재는 **실패**로 나타난다. 도구가 없는 기계에서 이 테스트는 계약 위반처럼 읽히는 실패를 낸다 — 메시지가 "jq is required"라 원인은 드러나지만, 이미 skip을 선택한 테스트가 두 번째 도구에 대해서만 다르게 행동한다.
이 저장소는 반대 방향의 원칙도 갖고 있다 — `webNginxProxyTest`가 "A lane that quietly passes when the container runtime is missing is a lane that has been certifying nothing since whenever Docker last broke"로 skip을 거부한다. 두 원칙 중 어느 쪽을 택하든 **한 테스트 안에서 도구별로 갈리지는 않는 편이 낫다.**
**나머지 1,015개 테스트는 통과한다**(failures=1, errors=0, skipped=4).
### 26.2 재검증 — 그 레인 계약이 실제로 성립하는지 독립 경로로 확인했다 (2026-08-31)
§26.1이 남긴 실질적 공백은 "그래서 15개 레인이 계약과 맞는가"가 **확인되지 않은 채**였다는 점이다.
실패 원인이 환경이라는 판정은 옳지만, 그 판정은 계약의 성립 여부를 말해 주지 않는다.
`jq`는 설치하지 않았다 — 사용자 기계의 시스템 변경이다. 대신 `verify-compose-profile-contracts.sh`
6개 검사를 파이썬으로 이식해 같은 `docker compose config` 렌더링 위에서 돌렸다. 아무것도 기동하지
않는 정적 검사다(`EVD-334`).
```
compose version 2.40.3+ds1-0ubuntu1~24.04.1 >= 2.24.4
off-local: app @ local off-dev: app @ dev
off-prod: app @ prod local-jpa: app,db @ local
local-mongo: app,mongo,mongo-rs-init @ local
local-messaging: app,kafka @ local local-messaging-outbox: app,db,kafka @ local
local-notification-ingest / -serving / -handoff … @ local
local-graphql: app,auth-smoke,graphql-smoke,keycloak @ local
shared-infra-local / shared-infra-dev / prod-smoke …
all-adapters: app,auth-smoke,db,db-migrate-capabilities,db-promote-capabilities,
graphql-smoke,kafka,keycloak,mailpit,mongo,mongo-rs-init,notification-smoke @ local
PORT RESULT: all 15 lanes match src/config/runtime/compose-profile-contracts.json (exit 0)
```
이식본이 검사한 것은 원본과 같은 여섯이다 — ①compose 버전 하한 ②레인별 **정확한** 서비스 집합
③렌더된 `SPRING_PROFILES_ACTIVE` ④병합 모델의 마운트 대상 중복 ⑤서비스-역할 분할과 `--wait` 대상
정합 ⑥Keycloak realm의 밑줄 키.
**이것이 무엇을 말하고 무엇을 말하지 않는가.** 계약 위반의 증거가 없다는 것이지, 원본 스크립트가
통과한다는 것이 아니다. 그 구별은 테스트 자신의 주석이 이미 경계한 것이다 — "a test that re-derived
the same checks in Java would be a second opinion that can agree with the contract while the script
disagrees." `jq`가 있는 기계에서 원본 스크립트로 확인하는 것이 여전히 정본이며, 여기서 얻은 것은
그 확인이 이루어질 때까지의 잠정 근거다.
`exit 78`은 sysexits.h의 `EX_CONFIG` — "설정이 없어 확인할 수 없음"이고, 스크립트 끝의 `exit 1`
"계약이 틀렸음"이다. §26.1이 기록한 가드 비대칭의 비용이 정확히 이것이다: 두 코드가 하나의 실패로
뭉개지면서, 아무것도 검사되지 않은 상태가 계약 위반으로 보고됐다.
## 27. 완료 게이트
- [x] denominator 455 / 455 FULL_READ
- [x] 8개 sub-scope 각각 §8.1~§8.4 수행 — 조립 표면 전수(`.imports` 6 · `spring.factories` 4종 · 관리 컨텍스트 1) · `MasterSwitch` 다섯과 런타임 멤버십 대조 · main 참조 0 파일의 등록 경로 전수 분류 · 시작 검증기 12종 배선 확인
- [x] evidence `227``235` 생성
- [x] 실행 검증: tests=1016 failures=1(환경 원인) errors=0 skipped=4
- [x] **분석 중 판정 철회 1건**(§4.1 — 활성화 모델의 다섯 어댑터 범위는 결함이 아님) · **타 모듈 판정 하향 1건**(모듈 17 §4.1 P1 → P2)
- [x] 소스 미변경
- [x] **2026-08-31 재검증** — 리프 소스 변경 0(`EVD-333`), lane 재실행 tests=1016 failures=1(동일 환경 원인),
그리고 §26.1이 남긴 공백을 독립 경로로 메움 — 15개 레인 전부 계약 일치(§26.2, `EVD-334`)
## Source anchors
이 문서가 backtick으로 인용한 타입·경로를 저장소 트리에 대고 해석한 결과다. 해석된 것만 싣는다 — 총 **93개** (main 49 · test 38 · 기타 6).
```
src/app-bootstrap/build.gradle
src/config/architecture/modules.json (app-bootstrap 항목)
main:
src/main/java/dev/caskeleton/bootstrap/CaSkeletonApplication.java
src/main/java/dev/caskeleton/bootstrap/activation/AdapterActivationAutoConfiguration.java
src/main/java/dev/caskeleton/bootstrap/activation/AdapterActivationEndpoint.java
src/main/java/dev/caskeleton/bootstrap/activation/AdapterActivationReport.java
src/main/java/dev/caskeleton/bootstrap/activation/CapabilityDependencyEnvironmentValidator.java
src/main/java/dev/caskeleton/bootstrap/activation/CapabilityDependencyValidator.java
src/main/java/dev/caskeleton/bootstrap/activation/MasterSwitchEnvironmentPostProcessor.java
src/main/java/dev/caskeleton/bootstrap/activation/RuntimeEnvironmentProfileValidator.java
src/main/java/dev/caskeleton/bootstrap/async/AsyncExecutorConfig.java
src/main/java/dev/caskeleton/bootstrap/autoconfigure/fileserver/FileserverAdminManagementContextConfiguration.java
src/main/java/dev/caskeleton/bootstrap/autoconfigure/fileserver/FileserverPlatformAutoConfiguration.java
src/main/java/dev/caskeleton/bootstrap/autoconfigure/fileserver/FileserverStartupConfiguration.java
src/main/java/dev/caskeleton/bootstrap/autoconfigure/httpclient/HttpClientPlatformAutoConfiguration.java
src/main/java/dev/caskeleton/bootstrap/autoconfigure/messaging/DisabledMessagingSentinelAutoConfiguration.java
src/main/java/dev/caskeleton/bootstrap/autoconfigure/persistencejpa/DataSourceRequirement.java
src/main/java/dev/caskeleton/bootstrap/autoconfigure/persistencejpa/JpaOffAutoConfigurationImportFilter.java
src/main/java/dev/caskeleton/bootstrap/autoconfigure/persistencejpa/PersistenceJpaRootAutoConfiguration.java
src/main/java/dev/caskeleton/bootstrap/concurrency/DomainContextConfig.java
src/main/java/dev/caskeleton/bootstrap/idempotency/IdempotencyConfig.java
src/main/java/dev/caskeleton/bootstrap/idempotency/PostgreSqlIdempotencyProviderConfig.java
src/main/java/dev/caskeleton/bootstrap/lock/DistributedLockConfig.java
src/main/java/dev/caskeleton/bootstrap/logging/MetricsAsyncAppender.java
src/main/java/dev/caskeleton/bootstrap/logging/PseudonymizationConfig.java
src/main/java/dev/caskeleton/bootstrap/logging/ResolvedProfileLoggingContextListener.java
src/main/java/dev/caskeleton/bootstrap/logging/SamplingTurboFilter.java
src/main/java/dev/caskeleton/bootstrap/logging/SecretMaskingJsonGeneratorDecorator.java
src/main/java/dev/caskeleton/bootstrap/logging/SecretMaskingMessageConverter.java
src/main/java/dev/caskeleton/bootstrap/logging/StartupFailureSpringBootLogFilter.java
src/main/java/dev/caskeleton/bootstrap/messaging/KafkaSenderConfig.java
src/main/java/dev/caskeleton/bootstrap/mongo/MongoPlatformHealthConfig.java
src/main/java/dev/caskeleton/bootstrap/notification/NotificationPlatformDispatchConfig.java
src/main/java/dev/caskeleton/bootstrap/notification/NotificationPlatformObservabilityConfig.java
src/main/java/dev/caskeleton/bootstrap/notification/NotificationPlatformProviderConfig.java
src/main/java/dev/caskeleton/bootstrap/notification/NotificationPlatformWorkerConfig.java
src/main/java/dev/caskeleton/bootstrap/notification/NotificationRootAutoConfiguration.java
src/main/java/dev/caskeleton/bootstrap/runtime/RuntimeSafetyConfig.java
src/main/java/dev/caskeleton/bootstrap/runtime/SecretSourceConfig.java
src/main/java/dev/caskeleton/bootstrap/runtime/SecretSourceSettings.java
src/main/java/dev/caskeleton/bootstrap/runtime/StartupSafetyValidator.java
src/main/java/dev/caskeleton/bootstrap/runtime/startup/MigrationFailedException.java
src/main/java/dev/caskeleton/bootstrap/runtime/startup/MigrationStartupConfig.java
src/main/java/dev/caskeleton/bootstrap/runtime/startup/StartupErrorCode.java
src/main/java/dev/caskeleton/bootstrap/runtime/startup/StartupFailureExceptionReporter.java
src/main/java/dev/caskeleton/bootstrap/runtime/startup/StartupFailureLogState.java
src/main/java/dev/caskeleton/bootstrap/runtime/startup/StartupFailures.java
src/main/java/dev/caskeleton/bootstrap/runtime/startup/StartupPhase.java
src/main/java/dev/caskeleton/bootstrap/tracing/TracingConfig.java
src/main/java/dev/caskeleton/bootstrap/tracing/TracingSamplingEnvironmentPostProcessor.java
src/main/resources/application.yml
test:
src/test/java/dev/caskeleton/bootstrap/activation/MasterSwitchRegistryContractTest.java
src/test/java/dev/caskeleton/bootstrap/architecture/ArchitectureViolationFixtureTest.java
src/test/java/dev/caskeleton/bootstrap/architecture/CleanArchitectureTest.java
src/test/java/dev/caskeleton/bootstrap/architecture/DomainFeatureOnboardingContractTest.java
src/test/java/dev/caskeleton/bootstrap/architecture/GraphQlInboundOwnershipBoundaryTest.java
src/test/java/dev/caskeleton/bootstrap/architecture/JpaProductionArchitectureTest.java
src/test/java/dev/caskeleton/bootstrap/architecture/MongoRawAccessBoundaryTest.java
src/test/java/dev/caskeleton/bootstrap/architecture/NotificationArchitectureTest.java
src/test/java/dev/caskeleton/bootstrap/architecture/ProductionClassImportOption.java
src/test/java/dev/caskeleton/bootstrap/architecture/TestTaxonomyArchitectureTest.java
src/test/java/dev/caskeleton/bootstrap/architecture/WebProductionArchitectureTest.java
src/test/java/dev/caskeleton/bootstrap/compose/ComposeMergeCharacterizationTest.java
src/test/java/dev/caskeleton/bootstrap/contract/ActuatorSecurityHttpTest.java
src/test/java/dev/caskeleton/bootstrap/contract/BackgroundJobErrorCodeContractTest.java
src/test/java/dev/caskeleton/bootstrap/contract/ConditionalTransportQualificationContractTest.java
src/test/java/dev/caskeleton/bootstrap/contract/ContainerRuntimeOomContractTest.java
src/test/java/dev/caskeleton/bootstrap/contract/ContractRegistrySchemaGovernanceTest.java
src/test/java/dev/caskeleton/bootstrap/contract/ContractSuiteCompletenessTest.java
src/test/java/dev/caskeleton/bootstrap/contract/DeveloperExperienceContractTest.java
src/test/java/dev/caskeleton/bootstrap/contract/DistributedTracingContractTest.java
src/test/java/dev/caskeleton/bootstrap/contract/EnvProfileMatrixContractTest.java
src/test/java/dev/caskeleton/bootstrap/contract/ErrorCodeRegistryMappingTest.java
src/test/java/dev/caskeleton/bootstrap/contract/LockAcquisitionTimeoutClassificationContractTest.java
src/test/java/dev/caskeleton/bootstrap/contract/LockFailureClassificationContractTest.java
src/test/java/dev/caskeleton/bootstrap/contract/ManagementActuatorSecurityContractTest.java
src/test/java/dev/caskeleton/bootstrap/contract/MetricsAlertingContractTest.java
src/test/java/dev/caskeleton/bootstrap/contract/OptionalAdapterConditionalExecutionContractTest.java
src/test/java/dev/caskeleton/bootstrap/contract/PersistenceFailureMappingContractTest.java
src/test/java/dev/caskeleton/bootstrap/contract/PiiTokenBodyForbiddenContractTest.java
src/test/java/dev/caskeleton/bootstrap/contract/ProfileSeparationContractTest.java
src/test/java/dev/caskeleton/bootstrap/contract/RedisOptionalityContractTest.java
src/test/java/dev/caskeleton/bootstrap/contract/RegistryGovernanceCatalog.java
src/test/java/dev/caskeleton/bootstrap/contract/RepositoryAccessCapabilityRegistryTest.java
src/test/java/dev/caskeleton/bootstrap/contract/RunbookCoverageContractTest.java
src/test/java/dev/caskeleton/bootstrap/contract/SecretReloadContractTest.java
src/test/java/dev/caskeleton/bootstrap/contract/SecretsClassificationRegistryTest.java
src/test/java/dev/caskeleton/bootstrap/contract/SqlLoggingForbiddenContractTest.java
src/test/java/dev/caskeleton/bootstrap/contract/StructuredLogFieldContractTest.java
기타:
docs/registries/env-keys.yaml
src/conditionalTransportTest/java/dev/caskeleton/bootstrap/transport/ConditionalTransportCompositionContractTest.java
src/config/architecture/modules.json
src/functionalTest/java/dev/caskeleton/bootstrap/contract/ConditionalTransportEvidenceFunctionalTest.java
src/functionalTest/java/dev/caskeleton/bootstrap/contract/RuntimeMembershipFunctionalTest.java
src/sampleOffTest/java/dev/caskeleton/bootstrap/contract/SampleOffClasspathContractTest.java
해석되지 않은 인용 (11종) — 외부 타입·문서상 약칭 등:
env-keys.yaml
modules.json
evidence/raw/227-app-bootstrap-module-inventory.txt
evidence/raw/228-app-bootstrap-activation-probes.txt
evidence/raw/229-app-bootstrap-autoconfigure-probes.txt
evidence/raw/230-app-bootstrap-runtime-probes.txt
evidence/raw/231-app-bootstrap-capability-probes.txt
evidence/raw/232-app-bootstrap-security-probes.txt
evidence/raw/233-app-bootstrap-architecture-probes.txt
evidence/raw/234-app-bootstrap-contract-probes.txt
evidence/raw/235-app-bootstrap-testrest-probes.txt
```