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:
co-authored by
Claude Opus 5
parent
a0ca2bb72a
commit
1f04117bbf
-204
@@ -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 -->
|
||||
-90
@@ -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 -->
|
||||
-90
@@ -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 -->
|
||||
-104
@@ -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 -->
|
||||
Reference in New Issue
Block a user