Files
DongHyeonkaandClaude Opus 5 ab59130196 chore: 이전 세션이 남긴 변경을 커밋한다
이번 파이프라인 작업과 무관하게 작업 트리에 남아 있던 것을 그대로 올린다.
사용자가 「전부 커밋」으로 정했고, 이번 작업과 섞이지 않게 커밋만 나눴다.

대부분은 clean-architecture-backend-template 의 그림 정본 재배치다 —
final/assets/diagrams/<이름>/ 에 있던 것이 CLAUDE.md 가 적은 배치인
final/assets/<이름>/ 로 옮겨졌고 .techviz/<이름>/ 이 함께 들어왔다.
삽입 줄의 대부분(3.15M)이 그 .techviz context.json 이다.

그 밖에 ca-tmpl·document-haness 의 정리, .claude/agents/ 열한 개,
writing-practitioner-guides 스킬, .playwright-mcp 세션 산출물,
scripts/check-ssot-facts.py 와 그 시험이 들어 있다.

이 커밋의 내용은 내가 만든 것이 아니라 이전 세션이 남긴 것이고 검증하지 않았다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 11:02:02 +09:00

149 lines
6.6 KiB
Markdown

---
kind: CONCEPT
slug: three-assembly-paths
title: Spring 조립의 세 경로와 각각이 결정하는 것
topic: assembly-ownership
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
basisVersion: Spring Boot 4.0.8 · Java 21 · 리비전 21234e38
rootTreeNode: concept:three-assembly-paths
evidenceCapturedOn: 2026-09-01
assets:
- key: three-assembly-paths
file: ../../../final/evidence/rendered/three-assembly-paths.svg
- key: three-assembly-paths-diagram
file: ../../../final/assets/diagrams/three-assembly-paths/three-assembly-paths.svg
evidence:
- ../../../final/evidence/raw/three-assembly-paths.txt
- ../../../final/evidence/raw/tl-web-six-unowned-components.txt
source:
- 원본 분석 절은 final/document.md#1-4, #7-1 · final/document.md#a18 · final/document.md#a14 §7.1 이다.
---
# Spring 조립의 세 경로와 각각이 결정하는 것
이 저장소의 빈은 컴포넌트 스캔, 자동설정 등록, 그리고 컨텍스트 이전 확장의 세 경로로 들어온다. 어떤 컴포넌트가 실제로 존재하는지는 그 셋 중 어느 것이 그것을 소유하는가로 정해진다.
## 관계
- **스캔에서 뺀 다섯 패키지의 컴포넌트 여섯을 두 자동설정 어느 쪽도 소유하지 않았다**
경로가 바뀌는 지점에서 소유권이 끊긴 사례다.
- **넓은 스캔을 좁히자 여덟 컴포넌트에 아무것도 도달하지 않았다**
같은 형태가 퍼시스턴스 리프에서 나타난 사례다.
- **@Bean이 있다는 것은 조립 증거가 아니다**
이 개념을 확인 절차로 옮긴 규칙이다.
## 본문
<!-- body:start -->
이 저장소에서 빈이 컨텍스트에 들어오는 경로가 셋이고 각각 다른 질문에 답한다.
## 조립의 세 경로
:::evidence key="three-assembly-paths-diagram" alt="빈이 들어오는 경로에서 컴포넌트 스캔과 imports 와 spring.factories 세 갈래가 나온다" caption="조립의 세 경로" zoom="false"
:::
**컴포넌트 스캔**(`@ComponentScan` + `AUTO_CONFIGURED_PACKAGES` 제외 정규식) · **`.imports`**(8개 파일 / 13개 클래스가 전부) · **`spring.factories`**(EnvironmentPostProcessor 6 · SpringBootExceptionReporter · AutoConfigurationImportFilter · ApplicationListener).
## 세 경로가 각각 답하는 질문
:::evidence key="three-assembly-paths" alt="분석 문서 final/document.md 에서 이 기록의 근거 절을 그대로 잘라낸 18줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md 발췌 — 18줄" zoom="true"
:::
## 조립 표면이 좁다
조립 결함을 판정할 때 세 경로를 다 읽어야 한다. main Java 4,614개 중 스테레오타입 보유가 206개(4.5%)뿐이다.
:::note
없음 — 세 경로의 내용을 전수 판독했다
:::
## 경로 1 — 컴포넌트 스캔
컴포지션 루트는 `@SpringBootApplication` 대신 `@SpringBootConfiguration` + `@EnableAutoConfiguration` + `@ComponentScan`을 직접 쓴다. javadoc이 이유를 적는다 — 스캔에 `excludeFilters`가 필요한데 그 애너테이션은 속성을 노출하지 않으므로, 스캔할 패키지를 대신 명시한다.
```java
@ComponentScan(
basePackages = {
"dev.caskeleton.bootstrap",
"dev.caskeleton.adapter",
"dev.caskeleton.application",
"dev.caskeleton.domain",
"dev.caskeleton.shared"
},
excludeFilters = {
@ComponentScan.Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@ComponentScan.Filter(
type = FilterType.CUSTOM,
classes = AutoConfigurationExcludeFilter.class),
@ComponentScan.Filter(
type = FilterType.REGEX,
pattern = CaSkeletonApplication.AUTO_CONFIGURED_PACKAGES)
})
```
정규식은 13개 패키지 접두사를 제외한다. 클래스 목록이 아니라 접두사인 이유도 적혀 있다 — 선택적 능력에 새 설정이 추가됐을 때, 아무도 제외를 기억하지 못했다는 이유로 활성화되면 안 되기 때문이다.
## 경로 2 — 자동설정 등록
제외된 패키지는 `META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports`로 들어온다. 이 저장소에는 그런 파일이 8개 있다.
```text
adapter/inbound/graphql
adapter/inbound/web
adapter/outbound/cache-redis
adapter/outbound/messaging
adapter/outbound/persistence-mongo
app-bootstrap
grpc/grpc-spring-boot-starter
messaging/messaging-spring-boot-starter
```
여기에 관리 컨텍스트용 `ManagementContextConfiguration.imports`가 하나 더 있다. Fileserver admin 라우트가 공개 커넥터가 아니라 관리 평면에 실리는 것이 이 파일의 역할이다.
## 경로 3 — 컨텍스트가 생기기 전
`META-INF/spring.factories`는 컨텍스트가 존재하기 전에 동작하는 확장을 등록한다.
```text
org.springframework.boot.EnvironmentPostProcessor=\
dev.caskeleton.bootstrap.activation.MasterSwitchEnvironmentPostProcessor,\
dev.caskeleton.bootstrap.activation.RuntimeEnvironmentProfileValidator,\
dev.caskeleton.bootstrap.activation.CapabilityDependencyEnvironmentValidator,\
...
org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\
dev.caskeleton.bootstrap.autoconfigure.persistencejpa.JpaOffAutoConfigurationImportFilter
```
`AutoConfigurationImportFilter`가 특히 중요하다. 이것은 후보 집합 자체를 좁히므로, 프로젝트의 어떤 조건보다 먼저 동작한다. 프레임워크 자신의 자동설정을 후보에서 빼는 것이 그 일이다.
## 세 경로가 만드는 함정
| 상황 | 증상 |
|---|---|
| 스캔에서 뺐는데 자동설정이 소유하지 않음 | 컴포넌트가 아무 데도 없다. 기동은 성공한다 |
| 자동설정 파일에 없는데 이름만 AutoConfiguration | 등록되지 않는다. 능력 리포트는 있다고 말한다 |
| `@Import`로 들어오는 클래스에 빈 조건 | 파싱 시점 평가라 빈 정의가 아직 없다 |
:::warning
세 경로의 공통점은 실패가 조용하다는 것이다. Spring은 "등록되지 않은 컴포넌트"를 오류로 보지 않는다. 그것이 정상 동작이기 때문이다.
:::
## 확인 순서
어떤 컴포넌트가 실제로 존재하는지 물을 때는 순서가 있다.
1. 컴포지션 루트의 스캔 범위와 제외 정규식을 읽는다
2. 제외됐다면 `.imports` 파일에서 그 패키지의 소유자를 찾는다
3. 소유자가 있다면 그것이 `@Bean`이나 `@Import`로 대상을 실제로 만드는지 확인한다
4. 조건이 붙어 있다면 그 조건이 언제 평가되는지 확인한다
<!-- body:end -->