docs(clean-architecture-backend-template): 제1부가 채택한 것만 글감으로 남기고 다시 고른다

글감 1,001개 중 제1부(§3~§11) 앵커를 하나라도 가진 것은 112개뿐이었다. 나머지 889개는
제2부 모듈 분석 65편의 절 제목에서 나온 것이고, 그것이 재판정이 필요했던 이유다.

  주제      44 → 16   (43개가 독자 질문 없이 있었다. 지금은 전부 있다)
  글감   1,001 → 123  (제1부 앵커 112 + 제1부가 채택했는데 비어 있던 자리 11)
  후보      965 → 1,088 · PENDING 905 → 0
  error   3,042 → 0

내려온 889개는 후보 대장에 KEEP_IN_SSOT 로 남는다 — 버린 것이 아니라 분석에 남기고 독립
기록으로 만들지 않기로 한 것이다. 그 글감을 받치던 기록 파일 828개는 지웠다. 계약이 정본이고,
파일이 남아 있다는 이유로 계약에서 뺀 주제가 되살아나면 안 된다. 이력에는 그대로 있다 —
git checkout a0ca2bb -- <경로>.

제1부가 채택했는데 글감이 없던 자리 열하나를 채웠다: mongo high-water mark 가 재전달 이벤트를
삼킨 P1, admin plane 이 가드만 켜고 서비스는 켜지 않은 것과 그 짝인 결정, 실패 어휘 세 층과
SQLState 매트릭스 병합 규칙, 부하 아래에서만 새는 admission 경계, 발행 증거와 완료 판정의
분리, keyset·JSONB 결정 둘.

Concept 17개에 basis-version 을 채우고, 계약 제목과 기록 제목이 갈라져 있던 23건을 기록 쪽에
맞췄다. candidateScope 에 excludedAnchorPattern 을 적어 제2부 앵커만 가진 글감이 다시 올라올
수 없게 한다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
DongHyeonka
2026-09-07 15:02:25 +09:00
co-authored by Claude Opus 5
parent a0ca2bb72a
commit 1f04117bbf
851 changed files with 5498 additions and 90638 deletions
@@ -1,204 +0,0 @@
---
kind: CASE
slug: a06-f018-changestreams-false
title: 플래그는 고정 거짓이라 능력 검사를 끄지만, 조립 조건이 아니라서 소비자 빈은 그대로 생성된다
topic: assembly-ownership
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: case:a06-f018-changestreams-false
evidenceCapturedOn: 2026-09-02
body: case-a06-f018-changestreams-false.body.md
assets:
- key: a06-f018-changestreams-false
file: ../../../final/evidence/rendered/a06-f018-changestreams-false.svg
- key: a06-f018-changestreams-false-wiring
file: ../../../final/evidence/rendered/a06-f018-changestreams-false-wiring.svg
evidence:
- ../../../final/evidence/raw/a06-f018-changestreams-false.txt
- ../../../final/evidence/raw/a06-f018-changestreams-false-wiring.txt
source:
- 원본 분석 절은 final/document.md#a06#L1069 이다. 등급은 P2 다. 주석의 전제가 더 이상 사실이 아니라는 판정, 형제 불리언과의 대비표, 검증기 분기가 도달 불가라는 사실, 그리고 실패가 장애 조치 런북으로 분류된다는 서술이 그 절에 있다.
- 같은 문서 `#L1015` 는 소비자가 도는 조건을 포크가 다섯을 공급하는 경우로 한정한다. 이 저장소 안에서는 그 다섯의 구현이 전부 시험 픽스처다.
- 같은 문서 `#L156` 은 같은 코드를 P3 으로 판정하면서 변경 스트림 실행체가 애초에 출하되지 않는다는 것을 근거로 든다. 이 리비전에서 그 근거가 성립하지 않으므로 그 절에 붙은 "현재 잘못된 동작을 만들지는 않는다"는 유지될 수 없고, 실질 등급은 이 절의 P2 로 흡수된다.
- 설정 빈이 속성을 받고도 거짓을 보고한다는 것, 그 두 경우의 빈 집합이 같다는 것, 검사 빈 자체에 조건이 있다는 것, 그리고 런북 전체에 단독 서버·오플로그·해당 오류 코드가 없다는 것은 이 기록에서 확인했다.
---
# 플래그는 고정 거짓이라 능력 검사를 끄지만, 조립 조건이 아니라서 소비자 빈은 그대로 생성된다
설정 주석은 드라이버 쪽 구현이 출하되지 않아 빈이 0 이라는 것을 근거로 플래그를 고정 거짓으로 만든다. 이 리비전에서 그 구현에는 빈 선언이 있고, 배포가 다섯을 공급하면 소비자도 선다. 조립 조건 어디에도 그 플래그는 없다.
## 관계
- **거부라고 적힌 처리가 폐기이고, 그 값을 읽는 시작 검사는 켤 방법이 없다**
같은 플래그를 다른 절에서 다룬 기록이다. 거부와 폐기의 차이는 그 기록이 다룬다.
- **하위 시스템 전체가 미배선인데 그것을 켜는 플래그는 시작 검사를 수행한다**
형제 불리언 쪽 기록이다. 트랜잭션 계수와 그 결과는 그 기록이 다룬다.
- **@Bean이 있다는 것은 조립 증거가 아니다**
플래그와 조립의 관계를 확인하는 규칙이다.
## 문제
시작 검증기에는 인접한 두 분기가 있다. 하나는 트랜잭션이 켜져 있는데 토폴로지가 지원하지 않으면 던지고, 다른 하나는 변경 스트림에 대해 같은 일을 한다. 두 좌항은 같은 설정 타입의 형제 불리언이고 자동 구성의 인접한 두 줄이 넘긴다.
두 불리언은 같은 검증기에서 서로 다르게 끝난다. 앞의 값은 배포가 넣은 대로 도착하고, 뒤의 값은 컴팩트 생성자가 이미 거짓으로 바꾼 뒤다.
## 결론
플래그가 고정 거짓이므로 검증기의 변경 스트림 분기는 실행되지 않는다. 조립은 그와 무관하게 진행된다. 소비자 빈의 조건은 배포가 공급해야 하는 타입 다섯이고, 그 목록에 이 플래그는 없다. 자동 구성 파일 전체에서 그 이름이 나오는 줄은 검증기 인자 하나뿐이다.
전체 자동 구성을 올린 스프링 컨텍스트로 확인했다. 설정 빈은 change-streams=true 를 받고도 거짓을 보고하고, 그 두 경우의 빈 집합이 같다. 모듈 opt-in 을 켜고 리액티브 템플릿이 있으면 드라이버 쪽 구현 빈은 만들어지고 소비자 빈은 만들어지지 않는다. 다섯을 함께 넣으면 소비자 빈도 만들어진다. opt-in 을 켜지 않으면 셋 다 없다.
주석은 이 코드가 있기 전 상태를 서술한다. 빈이 0 이고 스레드가 0 이라는 근거는 이 리비전에서 성립하지 않는다.
남는 것은 능력 검사만 꺼진 상태다. 다만 그 검사가 열리는 조건이 따로 있다. 검사 빈은 토폴로지 프로브를 조건으로 걸고, 보안 프로파일과 관리 자격 참조와 스키마 버전 범위 중 하나라도 없으면 부분 검증 대신 예외로 닫는다. 그리고 검증기는 선언 토폴로지와 실제를 능력 검사보다 먼저 대조한다. 변경 스트림 분기는 그 둘을 통과한 배포에서만 차례를 얻는데, 그 차례가 와도 좌항이 거짓이다.
그 다음 실패는 커서를 여는 시점의 드라이버 오류다. 복구 정책은 서버 코드가 이력 소실이 아니고 재개 가능 라벨도 아니면 실패로 확정하며 장애 조치 런북을 붙인다. 런북 어디를 봐도 그 세 낱말이 없다. 이 연쇄는 형제 기록이 오플로그 없는 서버에서 실행으로 확인했다.
수정은 셋 중 하나다. 플래그를 되살려 조립 조건으로 쓰거나, 소비자 빈이 설 때 능력을 기동에서 확인하거나, 최소한 주석을 현재 사실로 고치는 것이다.
## 검증 환경
OpenJDK : 21.0.12
확인 방식 : 전체 자동 구성을 올린 스프링 컨텍스트에서 빈 집합과 바인딩 값 비교, 코드베이스 정적 검색
소스 수정 : x
## 재현 조건
1. 시작 검증기의 인접한 두 분기와 그 좌항을 넘기는 두 줄을 읽는다.
2. 그 검사를 만드는 빈의 조건과 입력이 빠졌을 때의 처리를 읽는다.
3. 컴팩트 생성자의 플래그 강제와 그 주석을 읽는다.
4. 소비자 빈에 붙은 조건과 자동 구성 파일에서 그 플래그가 나오는 줄 수를 확인한다.
5. MongoPlatformAutoConfiguration 전체와 블로킹·리액티브 템플릿 빈을 등록한 컨텍스트를 ca-skeleton.persistence-mongo.enabled=true 로 띄우고 두 빈과 설정 빈의 유무, 그리고 바인딩된 플래그 값을 읽는다.
6. 조건 다섯을 함께 넣고 같은 것을 읽는다.
7. 두 경우를 change-streams=true 를 넣은 상태에서 반복한다.
8. opt-in 을 켜지 않은 경우와 리액티브 템플릿이 없는 경우를 각각 읽는다.
9. 복구 정책의 분류와 그것이 붙이는 런북 전체를 읽는다.
## 본문
<!-- body:start -->
시작 검증기에는 능력을 요구하는 분기가 둘 있고, 두 좌항은 같은 설정 타입의 형제 불리언이다.
## 두 분기가 읽는 값이 오는 자리
:::evidence key="a06-f018-changestreams-false" alt="시작 검증기의 인접한 두 능력 분기와 그 좌항을 넘기는 자동 구성의 두 줄, 그 검사를 만드는 빈의 조건과 입력이 빠졌을 때 닫는 처리, 컴팩트 생성자의 플래그 강제와 그 주석, 자동 구성 파일에서 그 플래그가 나오는 줄 수, 소비자 빈에 붙은 조건 전체, 복구 정책의 분류 메서드, 그리고 그것이 붙이는 런북의 증상 절 전체와 그 런북에서 단독 서버·오플로그·해당 오류 코드가 나오는 줄 수를 출력한 터미널 기록." caption="두 분기의 좌항은 형제 불리언이고 인접한 두 줄이 넘김 · 검사 빈은 토폴로지 프로브 조건이고 입력이 빠지면 닫음 · 변경 스트림은 생성자에서 고정 거짓이고 자동 구성 파일에 그 이름이 나오는 줄은 1 · 소비자 조건은 @ConditionalOnMissingBean 과 타입 다섯 · 실패는 FAILED 와 장애 조치 런북 · 그 런북에 단독 서버·오플로그·40573 은 0줄 — 91줄 · exit 0" zoom="true"
:::
```java
if (transactionsEnabled && !capabilities.isStable(MongoCapability.TRANSACTION)) {
...
if (changeStreamsEnabled && !capabilities.isStable(MongoCapability.CHANGE_STREAM)) {
```
자동 구성의 인접한 두 줄이 그 좌항을 넘긴다.
```java
properties.transactions(),
properties.changeStreams(),
```
앞의 값은 배포가 넣은 대로 도착한다. 그 값에서 무슨 일이 벌어지는지는 형제 기록이 다룬다. 여기서는 뒤의 값이 이미 거짓이라는 것과, 그런데도 조립은 진행된다는 것만 본다.
## 조립 조건
주석이 근거로 든 것은 빈이 0 이라는 사실이다.
```java
// Experimental, and therefore not a switch (MNG-INT-003). The driver-side source — watch,
// resumeAfter/startAfter, cursor lifetime, reconnection — is not shipped; what exists is policy
// and value objects that do not add up to a running consumer. Accepting the flag and ignoring …
...
changeStreams = false;
```
소비자 빈에 붙은 조건은 `@ConditionalOnMissingBean` 과 타입 다섯의 `@ConditionalOnBean` 이다.
```java
@org.springframework.boot.autoconfigure.condition.ConditionalOnBean({
dev.caskeleton.adapter.outbound.mongo.changestream.MongoChangeStreamSubscription.class,
dev.caskeleton.adapter.outbound.mongo.changestream.MongoResumeCheckpointStore.class,
dev.caskeleton.adapter.outbound.mongo.changestream.MongoResumeTokenCodec.class,
dev.caskeleton.adapter.outbound.mongo.changestream.projector.MongoChangeProjector.class,
dev.caskeleton.adapter.outbound.mongo.changestream.projector.MongoChangeDeduplicationStore
.class
})
```
다섯 다 배포가 공급해야 하는 타입이다. 이 플래그는 목록에 없고, 자동 구성 파일 전체에서 그 이름이 나오는 줄은 검증기 인자 하나뿐이다.
## 컨텍스트를 띄운 결과
:::evidence key="a06-f018-changestreams-false-wiring" alt="전체 자동 구성을 등록한 스프링 컨텍스트를 모듈 opt-in 없이, opt-in 과 리액티브 템플릿만으로, opt-in 과 배포가 공급해야 하는 다섯을 함께, 그리고 opt-in 과 리액티브 템플릿 없이 각각 띄워 드라이버 쪽 구현 빈과 소비자 빈과 설정 빈의 유무, 그리고 바인딩된 플래그 값을 change-streams 를 넣지 않은 경우와 넣은 경우에 대해 읽은 터미널 기록." caption="opt-in 없으면 셋 다 없음 · opt-in 과 템플릿이면 드라이버 쪽만 섬 · 다섯을 넣으면 소비자도 섬 · 설정 빈은 change-streams=true 를 받고도 changeStreams()=false · 두 경우의 빈 집합이 같음 — 13줄 · exit 0" zoom="true"
:::
`MongoPlatformAutoConfiguration` 전체를 등록하고 블로킹·리액티브 템플릿을 넣어 띄웠다.
```text
[opt-in, 리액티브 템플릿만]
기본 드라이버쪽=있음 소비자=없음 설정빈=있음 changeStreams()=false
change-streams=true 드라이버쪽=있음 소비자=없음 설정빈=있음 changeStreams()=false
[opt-in, 배포가 공급해야 하는 다섯을 함께]
기본 드라이버쪽=있음 소비자=있음 설정빈=있음 changeStreams()=false
change-streams=true 드라이버쪽=있음 소비자=있음 설정빈=있음 changeStreams()=false
```
설정 빈은 컨텍스트에 있고 속성을 받는다. 받고도 거짓을 보고하므로 조립 조건에 닿기 전에 이미 값이 정해져 있다. 소비자가 서는 조건은 다섯을 공급했는지 하나다.
모듈 opt-in 이 없으면 세 빈이 모두 만들어지지 않고, opt-in 이 있어도 리액티브 템플릿이 없으면 두 빈이 만들어지지 않는다.
```text
[모듈 opt-in 없이]
기본 드라이버쪽=없음 소비자=없음 설정빈=없음
[opt-in, 리액티브 템플릿 없이]
기본 드라이버쪽=없음 소비자=없음 설정빈=있음 changeStreams()=false
```
## 능력 검사가 열리는 조건
검사가 꺼진 것과 검사가 애초에 만들어지지 않는 것은 다르다. 검사 빈부터 조건이 있다.
```java
@Bean
@ConditionalOnBean(MongoTopologyProbe.class)
public InitializingBean mongoPlatformStartupCheck(
...
if (security == null || admin == null || versions == null) {
// Fail closed rather than validate a subset. A partial startup check reports success for
// the parts nobody supplied, which is the shape the missing wiring already had.
```
토폴로지 프로브가 있어야 만들어지고, 보안 프로파일과 관리 자격 참조와 스키마 버전 범위가 다 있어야 돈다. 그리고 검증기는 능력 검사보다 먼저 선언 토폴로지와 실제를 대조한다. 그 둘을 통과한 배포에서만 변경 스트림 분기가 자기 차례를 얻고, 그 차례에서 좌항이 거짓이다.
## 그 다음 실패가 가는 곳
```java
if (failure.hasLabel("ResumableChangeStreamError")) {
return MongoChangeStreamRecoveryDecision.resume();
}
return MongoChangeStreamRecoveryDecision.halt(MongoChangeStreamState.FAILED, FAILURE_RUNBOOK);
...
private static boolean isHistoryLost(int serverCode) {
return serverCode == 286 || serverCode == 280;
```
토폴로지가 복제 셋이 아니라는 오류는 286 도 280 도 아니고 재개 가능 라벨도 없으므로 셋째 갈래다. 붙는 런북의 증상 절은 네 항목이고 전부 프라이머리 선출과 서버 선택 지연이다.
```text
- `MongoServerSelectionException` / `MongoConnectionException` spike, then recovery within seconds.
- `MongoSdamObservationListener` reports a topology change (primary removed, new primary elected).
- `MongoPoolObservationListener` shows checkout wait times rising while server-side command duration
stays flat — the wait is topology, not query cost.
- Latency spike on writes with no corresponding rise in read latency.
```
증상 절뿐 아니라 그 런북 전체에서 단독 서버도 오플로그도 해당 오류 코드도 나오지 않는다. 이 연쇄를 실제 서버에서 이은 것은 형제 기록이다.
## 확인하지 못한 것
다섯을 공급한 포크의 배포를 오플로그 없는 토폴로지에 올려 기동 통과와 커서 열기 실패를 이어서 재현하지는 않았다. 그 연쇄는 형제 기록이 단독 서버에서 실행으로 확인했다. 여기서는 조립 조건과 검사가 열리는 조건까지 확인했다.
<!-- body:end -->
@@ -1,90 +0,0 @@
---
kind: CASE
slug: autoconfiguration-in-name-only
title: 이름만 AutoConfiguration이던 세 클래스가 capability 리포트에 Stable로 올라 있었다
topic: assembly-ownership
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: case:autoconfiguration-in-name-only
evidenceCapturedOn: 2026-09-01
assets:
- key: autoconfiguration-in-name-only
file: ../../../final/evidence/rendered/autoconfiguration-in-name-only.svg
evidence:
- ../../../final/evidence/raw/autoconfiguration-in-name-only.txt
source:
- 원본 분석 절은 final/document.md#a05 §14.4 이다.
---
# 이름만 AutoConfiguration이던 세 클래스가 capability 리포트에 Stable로 올라 있었다
이름이 AutoConfiguration 으로 끝나는 세 클래스가 실제로는 평범한 팩토리였다. 컴포지션 루트는 그 패키지를 스캔에서 뺐고, 자동설정으로 등록되지도 않았다. 능력 리포트는 세 능력을 Stable 로 보고했고 실행 컨텍스트에는 그중 아무것도 없었다.
## 관계
- **@Bean이 있다는 것은 조립 증거가 아니다**
이름과 위치가 조립을 보장하지 않는다는 사례다.
- **조건부 빈의 평가 시점 — 파싱 시점과 등록 시점**
같은 클래스에서 이어진 두 번째 결함이 그 개념을 설명한다.
- **시작 검증기가 도는지는 그 능력에 자동설정 루트가 있는지와 일치한다**
같은 형태의 확인 절차다.
## 문제
세 클래스가 이름을 AutoConfiguration 으로 끝냈다. 그러나 셋 다 다음을 갖고 있지 않았다.
@AutoConfiguration 애너테이션
@Bean 메서드
AutoConfiguration.imports 항목
동시에 컴포지션 루트는 이 패키지를 컴포넌트 스캔에서 의도적으로 제외한다. 자동설정이 이 패키지에 들어가는 유일한 경로이기 때문이다.
세 조건이 겹치면 결과는 하나다. 아무도 이 클래스들을 등록하지 않는다.
## 결론
능력 리포트는 트랜잭션 재시도와 완료 증거와 관측성을 Stable 로 올려 두었고, 실행 컨텍스트에는 그중 아무것도 없었다.
이 격차의 위험은 리포트를 읽는 사람에게 있다. 재시도에 의존하는 코드를 배포할 수 있고, 그 재시도는 한 번도 일어나지 않는다. 리포트가 그것을 Stable 이라고 말했기 때문이다.
수정은 등록을 추가하는 것이었다. 팩토리는 그대로 남았다. 팩토리가 조립 결정을 담고 있고, 자기 컴포지션 루트를 직접 배선하는 애플리케이션은 여전히 그것을 직접 호출할 수 있기 때문이다. 달라진 것은 기본 애플리케이션이 이제 빈을 받는다는 점이다.
## 검증 환경
OpenJDK : 21.0.12
Gradle : 9.0.0
Spring Boot : 4.0.8
근거 : 저장소의 javadoc 이 사후 기록으로 남긴 회귀
소스 수정 : x
## 재현 조건
수정된 형태를 확인하는 절차다.
1. JpaPlatformRuntimeAutoConfiguration 의 클래스 javadoc 두 번째 문단을 읽는다. 세 클래스가 무엇을 갖고 있지 않았는지 열거되어 있다.
2. CaSkeletonApplication 의 AUTO_CONFIGURED_PACKAGES 에서 이 패키지가 제외되는지 확인한다.
3. 현재 클래스에 Configuration 애너테이션과 조건들이 붙어 있고 실제 @Bean 을 갖는지 확인한다.
## 본문
<!-- body:start -->
세 클래스가 `...AutoConfiguration`으로 이름 붙었고 plain factory였다 — `@AutoConfiguration`도, `@Bean`도, `.imports` 엔트리도 없었고 합성 루트는 그 패키지를 스캔에서 제외한다.
## 세 클래스가 갖지 않은 것
:::evidence key="autoconfiguration-in-name-only" alt="분석 문서 final/document.md#a05 에서 이 기록의 근거 절을 그대로 잘라낸 16줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a05 발췌 — 16줄" zoom="true"
:::
## 리포트와 컨텍스트가 어긋났다
capability 리포트는 transaction retry·completion evidence·observability를 Stable로 나열했고 **돌고 있는 컨텍스트에는 그중 아무것도 없었다.** 개발자가 재시도되지 않는 재시도에 의존하는 코드를 배포할 수 있었다.
## 확인하지 못한 것
당시 능력 리포트의 출력을 직접 보지 않았다. 이 기록은 저장소가 javadoc 에 남긴 사후 기록에 근거한다.
없음 — 수정 후 형태를 코드로 확인했다
<!-- body:end -->
@@ -1,90 +0,0 @@
---
kind: CASE
slug: conditionalonbean-evaluated-at-parse-time
title: '@ConditionalOnBean(DataSource.class)가 클래스 파싱 시점에 평가되어 여덟 빈이 사라졌다'
topic: assembly-ownership
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: case:conditionalonbean-evaluated-at-parse-time
evidenceCapturedOn: 2026-09-01
assets:
- key: conditionalonbean-evaluated-at-parse-time
file: ../../../final/evidence/rendered/conditionalonbean-evaluated-at-parse-time.svg
evidence:
- ../../../final/evidence/raw/conditionalonbean-evaluated-at-parse-time.txt
source:
- 원본 분석 절은 final/document.md#a05 §14.4 이다.
---
# @ConditionalOnBean(DataSource.class)가 클래스 파싱 시점에 평가되어 여덟 빈이 사라졌다
@Import 로 들어오는 설정 클래스에 붙은 @ConditionalOnBean 이 데이터소스 빈 정의가 생기기 전에 평가되어 항상 거짓이었다. 여덟 빈이 조용히 사라졌고, 아무것도 그것을 보고하지 않았다.
## 관계
- **조건부 빈의 평가 시점 — 파싱 시점과 등록 시점**
이 사례가 설명하는 메커니즘이다.
- **ConditionalOnBean 사슬의 실제 평가 순서를 확인하지 않았다**
이 함정이 현재 리비전에도 남아 있는지에 대한 미해결 질문이다.
- **이름만 AutoConfiguration이던 세 클래스가 capability 리포트에 Stable로 올라 있었다**
같은 클래스에서 앞서 일어난 결함이다.
## 문제
이 클래스는 예전에 @ConditionalOnBean(DataSource.class) 를 갖고 있었다.
문제는 이 클래스가 자동설정으로 등록되는 것이 아니라 PersistenceJpaRootAutoConfiguration 이 @Import 로 끌어온다는 점이다. 그래서 조건이 클래스 파싱 시점에 평가된다. 데이터소스 빈 정의가 아직 존재하지 않는 시점이다.
따라서 조건은 실제 배포 전부에서 거짓이었다.
## 결론
여덟 빈이 조용히 사라졌다.
아무것도 그것을 보고하지 않았다. 그 여덟에 의존하는 것이 없었기 때문이다. 결함이 드러난 것은 데이터소스 검증기가 마침내 호출자에 연결되고 JPA Compose 레인이 적격 빈 없음이라고 답했을 때다.
수정은 조건의 순서를 바꾸는 것이 아니라 조건을 제거하는 것이었다. 근거는 이렇다. 이 클래스는 JPA 루트를 통해서만 도달하고 그 루트가 이미 마스터 스위치를 갖고 있으므로, 파싱 시점에는 데이터소스가 있느냐는 질문에 이미 예라고 답한 상태다. 데이터소스가 필요한 빈은 그것을 파라미터로 받고, 스위치가 켜진 채 데이터소스가 없으면 시끄러운 실패가 된다. 계층이 사라지는 것보다 그쪽이 원하던 결과다.
## 검증 환경
OpenJDK : 21.0.12
Gradle : 9.0.0
Spring Boot : 4.0.8
근거 : 저장소의 javadoc 이 사후 기록으로 남긴 회귀
소스 수정 : x
## 재현 조건
수정된 형태를 확인하는 절차다.
1. JpaPlatformRuntimeAutoConfiguration 의 클래스 javadoc 다섯 번째와 여섯 번째 문단을 읽는다.
2. 현재 클래스 애너테이션에 ConditionalOnBean 이 없고 ConditionalOnClass 와 ConditionalOnProperty 만 있는지 확인한다.
3. PersistenceJpaRootAutoConfiguration 이 이 클래스를 Import 하는지 확인한다.
## 본문
<!-- body:start -->
이 클래스는 루트가 **import**하지 auto-configure하지 않으므로, 그 조건이 클래스 파싱 중 — datasource 빈 정의가 존재하기 전에 — 평가됐고 따라서 **모든 실제 배포에서 false**였다.
## 조건이 평가된 시점
:::evidence key="conditionalonbean-evaluated-at-parse-time" alt="분석 문서 final/document.md#a05 에서 이 기록의 근거 절을 그대로 잘라낸 16줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a05 발췌 — 16줄" zoom="true"
:::
## 여덟 빈이 조용히 사라졌다
아무것도 그중 어느 것에도 의존하지 않아 아무것도 보고하지 않았다.
## 드러난 시점
datasource validator가 caller에 배선되고 Compose 레인이 "No qualifying bean"이라고 답했을 때다. 같은 함정을 피하려고 루트의 검사가 validator를 주입받지 않고 직접 생성한다.
## 확인하지 못한 것
현재 리비전의 다른 조건부 빈들이 각각 어느 시점에 평가되는지 런타임에서 확인하지 않았다. debug 부팅의 조건 평가 리포트가 그것을 답한다.
현재 리비전에서 재발하지 않는지 ConditionEvaluationReport로 확인하지 않았다
<!-- body:end -->
@@ -1,104 +0,0 @@
---
kind: CASE
slug: narrowing-the-scan-orphaned-eight-components
title: 넓은 스캔을 좁히자 여덟 컴포넌트에 아무것도 도달하지 않았다
topic: assembly-ownership
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: case:narrowing-the-scan-orphaned-eight-components
evidenceCapturedOn: 2026-09-01
assets:
- key: narrowing-the-scan-orphaned-eight-components
file: ../../../final/evidence/rendered/narrowing-the-scan-orphaned-eight-components.svg
evidence:
- ../../../final/evidence/raw/narrowing-the-scan-orphaned-eight-components.txt
source:
- 원본 분석 절은 final/document.md#a05 §14.2 이다.
---
# 넓은 스캔을 좁히자 여덟 컴포넌트에 아무것도 도달하지 않았다
컴포지션 루트가 퍼시스턴스 패키지를 스캔에서 뺐다. 그 제외는 옳았지만 나머지 절반이 빠져 있었다. 스캔 컴포넌트로 작성된 여덟 클래스에 아무도 도달하지 않았고, 그중 하나는 트랜잭션 포트의 유일한 구현이었다.
## 관계
- **스캔에서 뺀 다섯 패키지의 컴포넌트 여섯을 두 자동설정 어느 쪽도 소유하지 않았다**
같은 형태가 웹 리프에서 반복된 사례다.
- **꺼짐은 조건의 반복이 아니라 구조여야 한다**
스캔 제외가 그 규칙을 따른 조치라는 점이 이 사례의 전제다.
- **@Bean이 있다는 것은 조립 증거가 아니다**
스테레오타입이 붙어 있다는 것도 조립 증거가 아니다.
## 문제
컴포지션 루트의 컴포넌트 스캔은 퍼시스턴스 어댑터 패키지 전체를 정규식으로 제외한다. javadoc 은 그 제외가 옳다고 명시한다. 선택적 능력을 선택적으로 만드는 것이 그 제외이며, JPA 가 꺼진 배포는 퍼시스턴스 빈을 조립하지 않는다.
빠진 것은 나머지 절반이다. 이 리프의 여덟 클래스가 스캔 컴포넌트로 작성되어 있었다.
SpringTransactionPort
PersistenceExceptionTranslator
StandardSqlStateErrorMapping
DomainContextAuditContextPort
멱등성 저장소와 그 리퍼
outbox 저장소와 그 리퍼
넓은 스캔이 이들에게 닿지 않게 되자 다른 어떤 것도 닿지 않았다. @Component@Repository 가 붙어 있었지만 실행 중인 어떤 애플리케이션에서도 빈이 아니었다.
특히 TransactionPort 는 구현이 아예 없는 상태가 됐다. 트랜잭션을 여는 모든 유스케이스가 그것을 열 포트를 갖지 못했다.
## 결론
단위 테스트로는 보이지 않았다. 이 클래스들은 각자의 테스트에서 직접 생성되기 때문이다.
드러난 것은 트랜잭션이 필요한 능력이 실제로 조립됐을 때다. 알림 오케스트레이터가 local-notification-ingest 레인에서 미충족 의존성으로 실패했다.
수정은 스캔을 복원하되 원래 덮었어야 할 패키지로 좁히고, PersistenceJpaRootAutoConfiguration 을 통해서만 도달하게 만드는 것이었다. 그 루트가 JPA 마스터 스위치를 갖는다. 꺼짐은 여전히 구조적이다.
두 패키지는 의도적으로 빠져 있다. fileserver 는 자기 능력 스위치로 게이트되고 자기 설정 클래스가 스캔한다. notification 은 전용 파사드가 빈 단위로 명시적으로 조립한다.
이 패키지들 아래 컴포넌트는 각자의 ConditionalOnProperty 가드를 유지한다. 스캔 대상이 된다는 것은 후보가 된다는 뜻이지 무조건 빈이 된다는 뜻이 아니다.
## 검증 환경
OpenJDK : 21.0.12
Gradle : 9.0.0
Spring Boot : 4.0.8
근거 : 저장소의 javadoc 이 사후 기록으로 남긴 회귀
소스 수정 : x
## 재현 조건
수정된 형태를 확인하는 절차다.
1. JpaAdapterComponentsConfig 의 클래스 javadoc 을 읽는다. 여덟 클래스가 이름으로 열거되어 있다.
2. CaSkeletonApplication 의 제외 정규식에 퍼시스턴스 패키지가 있는지 확인한다.
3. 이 설정 클래스가 PersistenceJpaRootAutoConfiguration 을 통해서만 도달하는지 확인한다.
4. 의도적으로 빠진 두 패키지의 대체 조립 경로를 확인한다.
## 본문
<!-- body:start -->
합성 루트의 스캔이 persistence 트리를 정규식으로 제외했고 **그 제외는 옳다** — 그것이 optional capability를 optional하게 만든다. 빠진 것은 나머지 절반이다.
## SpringTransactionPort 참조 위치
:::evidence key="narrowing-the-scan-orphaned-eight-components" alt="코드베이스에서 SpringTransactionPort 를 검색한 출력 11줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="SpringTransactionPort 코드베이스 검색 — 11줄 · exit 0" zoom="true"
:::
## 여덟 클래스에 아무것도 도달하지 않았다
`SpringTransactionPort`·`PersistenceExceptionTranslator`·`StandardSqlStateErrorMapping`·`DomainContextAuditContextPort`·idempotency store와 reaper·outbox store와 reaper가 scanned component로 쓰여 있는데, 넓은 스캔이 멈추자 아무것도 도달하지 않았다.
## 특히 TransactionPort 는 구현이 전혀 없었다
트랜잭션을 여는 모든 유스케이스가 열 포트를 갖지 못했고, 단위 테스트는 각 클래스를 직접 생성하므로 볼 수 있는 것이 없었다.
## 확인하지 못한 것
당시 실패했던 local-notification-ingest 레인을 이 리비전에서 재실행하지 않았다.
없음 — 수정된 @ComponentScan 대상 6개를 코드로 확인했다
<!-- body:end -->
@@ -1,114 +0,0 @@
---
kind: CONCEPT
slug: when-conditions-are-evaluated
title: 조건부 빈의 평가 시점 — 파싱 시점과 등록 시점
topic: assembly-ownership
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: concept:when-conditions-are-evaluated
evidenceCapturedOn: 2026-09-01
assets:
- key: when-conditions-are-evaluated
file: ../../../final/evidence/rendered/when-conditions-are-evaluated.svg
evidence:
- ../../../final/evidence/raw/when-conditions-are-evaluated.txt
source:
- 원본 분석 절은 final/document.md#a05 §14.4 이다.
---
# 조건부 빈의 평가 시점 — 파싱 시점과 등록 시점
같은 `@ConditionalOnBean`이라도 그 클래스가 자동설정으로 등록되는지 `@Import`로 들어오는지에 따라 평가 시점이 다르다. 그 차이가 조건을 항상 거짓으로 만들 수 있다.
## 관계
- **ConditionalOnBean(DataSource)이 클래스 파싱 시점에 평가되어 여덟 빈이 사라졌다**
이 개념이 실제로 문제가 된 사례다.
- **ConditionalOnBean은 조건이 만족될 수 있는지까지 확인해야 한다**
이 개념에서 나온 확인 규칙이다.
- **ConditionalOnBean 사슬의 실제 평가 순서를 확인하지 않았다**
현재 리비전에 대한 미해결 질문이다.
## 본문
<!-- body:start -->
`@ConditionalOnBean`은 그 클래스가 **언제 평가되는가**에 따라 답이 달라진다. `@AutoConfiguration`으로 등록되면 다른 자동설정 이후에 평가되지만, plain `@Configuration`이 `@Import`로 들어오면 **클래스가 파싱되는 동안 — 대상 빈 정의가 존재하기 전에** 평가된다.
## 등록 방식이 평가 시점을 정한다
:::evidence key="when-conditions-are-evaluated" alt="분석 문서 final/document.md#a05 에서 이 기록의 근거 절을 그대로 잘라낸 16줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md#a05 발췌 — 16줄" zoom="true"
:::
## 이 저장소가 그 함정을 밟았다
여덟 빈이 조용히 사라졌다.
## 남은 회피 방법 둘
검증기를 주입받지 않고 직접 생성하기, 그리고 조건을 루트로 올리기.
:::note
현재 리비전의 각 조건부 빈이 어느 시점에 평가되는지는 ConditionEvaluationReport로 확인하지 않았다
:::
## 두 시점
`@ConditionalOnBean`은 "이 타입의 빈 정의가 이미 등록되어 있는가"를 묻는다. 그 질문의 답은 언제 묻느냐에 달라진다.
| 클래스가 들어오는 경로 | 조건이 평가되는 시점 |
|---|---|
| 자동설정 (`.imports`) | 자동설정 순서에 따라 등록 단계에서 |
| `@Import` | 그것을 import 하는 클래스가 파싱될 때 |
| 컴포넌트 스캔 | 스캔 단계에서 |
`@Import`로 들어오는 클래스가 문제다. 부모 설정이 파싱되는 시점에 자식 클래스의 클래스 수준 조건이 함께 평가되는데, 그 시점에는 자동설정이 만들 빈 정의가 아직 존재하지 않는다.
## 이 저장소가 겪은 형태
```java
/**
* <p>This class used to carry {@code @ConditionalOnBean(DataSource.class)}. It is imported by
* {@code PersistenceJpaRootAutoConfiguration} rather than auto-configured, so that condition was
* evaluated while the class was parsed — before the datasource bean definition existed — and was
* therefore false in every real deployment. All eight beans below silently disappeared, and nothing
* reported it because nothing depended on any of them. It surfaced only when the datasource
* validator was finally wired to a caller and the JPA Compose lane answered "No qualifying bean".
*/
```
두 문장이 이 개념의 핵심이다. 조건이 모든 실제 배포에서 거짓이었다는 것, 그리고 아무것도 그것을 보고하지 않았다는 것.
보고되지 않은 이유가 특히 중요하다. 사라진 여덟 빈에 의존하는 것이 없었기 때문이다. 의존이 있었다면 미충족 의존성으로 시끄럽게 실패했을 것이다.
## 해결 방향은 순서가 아니라 제거였다
```java
/**
* <p>The condition is removed rather than reordered: this class is reached only through the JPA
* root, which already carries the master switch, so "is there a datasource" has been answered yes
* by the time it is parsed. A bean here that needs one takes it as a parameter, and a missing
* datasource with the switch on is then a loud failure — which is the outcome that was wanted,
* rather than the layer vanishing.
*/
```
:::tip
조건을 옮기거나 순서를 바꾸는 대신, 그 조건이 이미 답해진 지점으로 도달 경로를 제한하고 조건 자체를 없앴다. 그리고 필요한 의존은 파라미터로 받게 해서, 없을 때 조용히 사라지는 대신 시끄럽게 실패하게 만들었다.
:::
## 확인 방법
정적으로는 두 가지를 본다.
1. 그 클래스가 `.imports`에 있는가, 아니면 다른 클래스가 `@Import` 하는가
2. 조건이 요구하는 빈을 누가 언제 등록하는가
런타임으로는 `debug=true` 부팅의 조건 평가 리포트가 답한다. `Negative matches` 항목의 사유 문자열이 "빈 정의 없음"인지 "타입 자체가 없음"인지를 구별해 준다.
<!-- body:end -->
@@ -1,7 +1,7 @@
---
kind: QUESTION
slug: conditional-evaluation-order-unverified
title: '@ConditionalOnBean 사슬의 실제 평가 순서를 확인하지 않았다'
title: @ConditionalOnBean 사슬의 실제 평가 순서를 확인하지 않았다
topic: assembly-ownership
project: clean-architecture-backend-template
status: 게시 전
@@ -1,7 +1,7 @@
---
kind: REFERENCE
slug: a-bean-is-not-composition-evidence
title: '@Bean이 있다는 것은 조립 증거가 아니다'
title: @Bean이 있다는 것은 조립 증거가 아니다
topic: assembly-ownership
project: clean-architecture-backend-template
status: 게시 전
@@ -1,7 +1,7 @@
---
kind: REFERENCE
slug: conditionalonbean-must-be-satisfiable
title: '@ConditionalOnBean은 조건이 만족될 수 있는지까지 확인해야 한다'
title: @ConditionalOnBean은 조건이 만족될 수 있는지까지 확인해야 한다
topic: assembly-ownership
project: clean-architecture-backend-template
status: 게시 전
@@ -1,53 +0,0 @@
---
kind: REFERENCE
slug: count-the-frameworks-own-autoconfigurations
title: 프레임워크가 기여하는 자동설정까지 세지 않으면 스위치가 아니다
topic: assembly-ownership
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: reference:count-the-frameworks-own-autoconfigurations
verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다
---
# 프레임워크가 기여하는 자동설정까지 세지 않으면 스위치가 아니다
## 목적
프로젝트 코드만 게이트하고 프레임워크가 기여하는 자동설정을 남겨 두어, 꺼진 능력이 여전히 자원을 잡는 것을 막는다.
## 규칙
1. 프로젝트 조건은 프레임워크 자동설정을 막지 못한다
후보 집합에 들어온 자동설정은 자기 조건으로 판단한다. 프로젝트의 마스터 스위치는 그 판단에 참여하지 않는다.
2. 후보 집합을 좁히는 필터가 따로 필요하다
AutoConfigurationImportFilter 는 어떤 프로젝트 조건보다 먼저 동작하므로 이 일을 할 수 있는 유일한 자리다.
3. 그 필터는 권한이 아니라 도구다
후보를 빼는 일과 능력이 켜졌는지 판정하는 일은 다르다. 판정 권한은 루트 하나가 갖는다.
4. 자원 필요 여부는 능력 질문으로 묻는다
커넥션 풀 같은 공유 자원은 한 능력의 사유물이 아니다. 그것을 필요로 하는 능력이 하나라도 활성인지를 묻는다.
## 적용 조건
프레임워크가 같은 기술에 대해 자기 자동설정을 갖는 모든 능력. 데이터소스, 메시징, 캐시가 대표적이다.
## 예외
프레임워크 자동설정이 이미 프로젝트 조건과 같은 속성을 보도록 설계되어 있으면 필터가 필요 없다.
## 예시
JPA 가 꺼진 배포에서 프레임워크의 데이터소스 자동설정을 후보에서 빼는 필터가 spring.factories 에 등록되어 있다.
풀이 필요한지는 JPA 가 켜졌는지가 아니라 커넥션을 필요로 하는 능력이 하나라도 활성인지로 묻는다. 이 저장소는 여섯 조건의 논리합으로 판정한다.
## 관계
- **풀이 필요한지는 JPA가 켜졌나가 아니라 커넥션이 필요한 capability가 있나로 묻는다**
이 규칙을 채택한 결정이다.
- **꺼짐은 조건의 반복이 아니라 구조여야 한다**
같은 목표의 짝 규칙이다.
@@ -1,7 +1,7 @@
---
kind: REFERENCE
slug: off-must-be-structural
title: '"꺼짐"은 조건의 반복이 아니라 구조여야 한다'
title: "꺼짐"은 조건의 반복이 아니라 구조여야 한다
topic: assembly-ownership
project: clean-architecture-backend-template
status: 게시 전