읽는 방법
이 글은 Spring Boot 2.7.18의 구현을 기준으로 삼아 ConfigDataEnvironment.processAndApply()의 실제 실행 경로를 따라간다.
코드에서는 주로 메서드명, 조건, 데이터 구조를 그대로 두고, 로그, 예외 래핑, 본래 흐름과 무관한 세부는 생략해 제어 흐름을 쉽게 확인할 수 있게 했다.
전체 글은 다음 다섯 가지 동작을 엄격히 구분한다.
- resolve:
ConfigDataLocation을ConfigDataResource로 해석한다. - load:
Loader가 Resource를 읽어ConfigData와PropertySource로 만든다. - bind: 단일
PropertySource에서spring.config.import와spring.config.activate를 바인딩한다. - active:
Contributor가 현재ActivationContext에서 활성화 조건을 만족한다. - apply: 유효한
PropertySource를 최종적으로 실제Environment에 추가한다.
ConfigData는 “application.yml을 다 읽으면 끝”인 파일 로더가 아니라, 설정 의존성 그래프를 단계적으로 해결하는 과정이다. 각 단계에서는 현재 확정할 수 있는 import를 계속 확장해 Contributor Tree가 안정 상태에 이를 때까지 진행한다. Profile이 확정된 뒤 프로필별 리소스를 보충하고, 마지막에 Environment에 일괄 적용한다.
2. 시작 진입점과 초기 Contributor Tree
ConfigData의 실행 진입점은 Environment 준비 단계에 있다.
SpringApplication은 ApplicationContext를 생성하고刷新하기 전에 먼저 ConfigurableEnvironment를 준비한다. EnvironmentPostProcessorApplicationListener가 ConfigDataEnvironmentPostProcessor를 호출하고, 후자는 ConfigDataEnvironment를 생성해 processAndApply()를 실행한다.
이 시점에는 다음 작업이 아직 시작되지 않았다.
- BeanDefinition 스캔
- 자동 구성 클래스 분석
- 싱글턴 Bean 생성
ApplicationContextrefresh
진입점 호출 체인은 다음과 같다.
2.1 초기 Contributor의 출처
ConfigDataEnvironment를 생성할 때는 현재 Environment에 이미 존재하는 PropertySource를 읽어 Kind.EXISTING Contributor로 래핑한다.
이 출처는 일반적으로 다음과 같다.
commandLineArgssystemPropertiessystemEnvironmentdefaultProperties
이 PropertySource들은 초기의 Binder 조회와 placeholder 해석에는 참여하지만, ConfigData 메커니즘이 다시 import하는 대상은 아니다.
이후 프레임워크는 다음 설정을 바탕으로 INITIAL_IMPORT 노드를 생성한다.
spring.config.importspring.config.additional-locationspring.config.location
spring.config.location을 명시적으로 설정하지 않았다면 다음과 같은 기본 검색 위치를 사용한다.
classpath:/
classpath:/config/
file:./
file:./config/
file:./config/*/
초기 위치를 즉시 Resolver로 처리하지 않고 Contributor로 표현하는 이유는 초기 위치와 이후의 spring.config.import 위치가 모두 동일한 처리 흐름을 거치게 하기 위해서다.
选择待处理节点
↓
resolveAndLoad
↓
生成子 Contributor
↓
替换不可变树
초기 트리의 논리 구조는 다음과 같다.
INITIAL_IMPORT는 이미 로드된 특정 파일에 대응하지 않으며 PropertySource도 제공하지 않는다. 여러 ConfigDataLocation을 보유할 뿐이며, withProcessedImports()에서 첫 번째 resolveAndLoad()를 실행하도록 할 뿐이다.
4. 전체 글에 걸쳐 사용하는 설정 예제
아래 설정에는 다음 항목이 모두 포함되어 있다.
- 조건 없는 import
- CloudPlatform 조건
- Profile 조건
- 재귀 import
- 프로필별 파일
런타임 환경이 최종적으로 Kubernetes로 식별되고, 기본 문서가 dev Profile을 활성화한다고 가정하자.
application.yml의 문서 0:
spring:
profiles:
active: dev
config:
import: classpath:common.yml
app:
source: base
application.yml의 문서 1:
spring:
config:
activate:
on-cloud-platform: kubernetes
import: classpath:k8s.yml
platform:
mode: kubernetes
application.yml의 문서 2:
spring:
config:
activate:
on-profile: dev
import: classpath:feature.yml
feature:
mode: dev-document
common.yml:
spring:
config:
import: classpath:db.yml
common:
enabled: true
db.yml:
db:
url: jdbc:mysql://localhost/base
k8s.yml:
platform:
service-discovery: dns
feature.yml:
feature:
enabled: true
application-dev.yml:
spring:
config:
import: classpath:dev-db.yml
app:
source: application-dev
dev-db.yml:
db:
pool-size: 20
여기서는 “파일”과 “문서”를 반드시 구분해야 한다.
application.yml은 Loader가 한 번에 읽지만, 그 안의 세 YAML 문서는 각각 독립적인 PropertySource가 된 뒤 세 개의 독립적인 Contributor로 변환된다.
각 Contributor에는 imports, activate, active 상태, children가 각각 있다.
따라서 파일을 이미 읽었다는 사실은 파일에 있는 모든 문서가 현재 단계에서 활성 상태라는 뜻은 아니다.
6. processAndApply의 세 번의 import 처리
processAndApply()는 withProcessedImports()를 세 번 호출하지만, ImportPhase는 두 종류뿐이다.
BEFORE_PROFILE_ACTIVATIONAFTER_PROFILE_ACTIVATION
처음 두 호출은 모두 BEFORE이며, 차이는 ActivationContext가 얼마나 완성되었는지뿐이다. 세 번째 호출은 Profiles가 생성된 후 AFTER로 진행된다.
주요 흐름은 다음과 같다.
6.1 첫 번째 호출: context가 null인 BEFORE
processInitial()은 null인 ActivationContext를 전달한다.
ImportPhase.get(null)은 BEFORE_PROFILE_ACTIVATION을 반환한다.
spring.config.activate가 없는 문서는 activate 필드가 null이므로 properties.isActive(null)에서도 활성 상태로 판정된다.
반면 activate 조건을 하나라도 선언한 문서는 Activate.isActive(null)에 들어가고 곧바로 false를 반환한다.
이 단계는 application.yml 한 겹만 읽는 단계가 아니다.
조건이 없는 문서의 imports는 계속 확장된다.
application.yml
↓ import
common.yml
↓ import
db.yml
바깥쪽 while은 현재 보이는 조건 없는 의존성 체인 전체가 바인딩과 import 처리를 마칠 때까지 계속 실행된다.
6.2 두 번째 호출: CloudPlatform은 알지만 Profiles는 여전히 null인 BEFORE
첫 번째 단계가 안정 상태에 도달하면 createActivationContext()가 현재 Environment와 Contributor 기반 Binder를 사용해 CloudPlatform을 추론하고, profiles는 명시적으로 null로 설정한다.
추론 순서는 설정에서 CloudPlatform을 강제로 지정했는지 먼저 확인한 다음 CloudPlatform.getActive(environment)를 호출하는 방식이다.
processWithoutProfiles()는 withProcessedImports()를 다시 호출한다.
현재 phase는 여전히 BEFORE지만, 이전에 on-cloud-platform 조건 때문에 비활성 상태였던 문서는 이제 다시 판정할 수 있다.
플랫폼이 일치하면 이 문서들의 import가 확장된다.
on-profile만 선언한 문서는 여전히 비활성 상태다.
플랫폼과 Profile을 모두 선언한 문서는 두 조건이 동시에 충족되어야 하므로 세 번째 단계를 기다린다.
6.3 Profile 추론과 세 번째 AFTER
withProfiles()는 현재 Contributor Tree로 Binder를 구성하고, IGNORE_PROFILES가 붙은 PropertySource를 걸러낸 다음 다음 항목을 읽는다.
spring.profiles.activespring.profiles.defaultspring.profiles.includespring.profiles.groupadditionalProfiles
Profiles 생성자는 Profile group도 확장하며, LinkedHashSet을 사용해 순서를 유지하고 반복을 종료한다.
이후 activationContext.withProfiles(profiles)를 호출하면 CloudPlatform과 Profiles를 모두 포함한 새 컨텍스트가 만들어진다.
processWithProfiles()는 withProcessedImports()를 세 번째로 호출한다.
이때는 activationContext.getProfiles() != null이므로 ImportPhase.get()이 AFTER_PROFILE_ACTIVATION을 반환한다.
이 단계에는 두 가지 새로운 기능이 생긴다.
on-profile문서를 활성 상태로 판정할 수 있다.- Resolver가 일반
resolve()뿐 아니라resolveProfileSpecific()도 실행한다.
따라서 표준 파일 Resolver는 application-dev.yml, common-dev.yml, application-prod.yml을 찾을 수 있다.
8. 실제 데이터 구조를 이용한 전체 실행 과정
8.1 초기 트리
EXISTING 노드를 무시하면 초기 트리는 다음과 같다.
ROOT
└─ INITIAL_IMPORT
imports = [classpath:/]
children = {}
8.2 첫 번째 BEFORE: 기본 문서 로드와 조건 없는 import 확장
INITIAL_IMPORT가 선택된다. 활성화 조건이 없고 imports가 비어 있지 않으며, children에 BEFORE key가 없다.
Resolver는 profiles == null일 때 일반 resolve만 실행해 application.yml을 찾는다.
Loader는 세 YAML 문서를 읽어 하나의 ConfigData를 생성한다.
asContributors()는 PropertySource의 역순으로 세 개의 UNBOUND_IMPORT를 생성하고, INITIAL_IMPORT.withChildren(BEFORE, children)를 통해 새 트리에 붙인다.
INITIAL_IMPORT
└─ BEFORE
├─ UNBOUND application.yml#2
├─ UNBOUND application.yml#1
└─ UNBOUND application.yml#0
이때 세 문서 모두 이미 물리적으로 읽혔지만 activate와 import는 아직 바인딩되지 않았다.
우선순위 순서에 따른 후위 순회는 먼저 application.yml#2를 찾는다. Kind가 UNBOUND_IMPORT이므로 활성 여부를 검사하지 않고 withBoundProperties()를 실행해 다음 결과를 얻는다.
BOUND application.yml#2
activate.on-profile = dev
imports = [feature.yml]
현재 context가 null이므로 isActive(null) == false이고, feature.yml은 아직 확장되지 않는다.
다음으로 application.yml#1을 바인딩한다.
BOUND application.yml#1
activate.on-cloud-platform = kubernetes
imports = [k8s.yml]
context가 여전히 null이므로 이 문서도 비활성 상태이며 k8s.yml은 아직 확장되지 않는다.
마지막으로 application.yml#0을 바인딩한다.
BOUND application.yml#0
activate = null
imports = [common.yml]
spring.profiles.active = dev
이 문서는 null context에서 활성 상태이므로 다음 라운드에 선택되어 common.yml을 로드하고 BEFORE children에 기록한다.
common.yml은 처음에는 UNBOUND common.yml로 추가된 뒤 다음과 같이 바인딩된다.
BOUND common.yml
imports = [db.yml]
활성화 조건이 없으므로 db.yml을 계속 로드한다. db.yml은 바인딩한 뒤 imports가 없으므로 더 이상 대기 노드가 되지 않는다.
첫 번째 BEFORE 단계가 안정 상태에 도달한 뒤의 트리는 다음과 같다.
이 시점의 application.yml#1과 application.yml#2는 load와 bind를 모두 마쳤지만 내부 import는 아직 확장되지 않았다.
첫 번째 단계가 끝나는 조건은 “모든 파일이 활성화되는 것”이 아니라, 현재 트리에 다음이 없는 것이다.
UNBOUND_IMPORT가 없고,- 활성 상태이면서 BEFORE 단계가 아직 처리되지 않은 Contributor도 없어야 한다.
8.3 두 번째 BEFORE: 플랫폼 조건 설정 추가
createActivationContext()가 CloudPlatform.KUBERNETES와 profiles = null을 추론한다.
두 번째 후위 순회에서 application.yml#1의 플랫폼 조건이 일치한다.
activate.on-cloud-platform = kubernetes
当前平台 = Kubernetes
따라서 isActive(context) == true이고, BEFORE key가 아직 없으므로 k8s.yml을 로드한다.
application.yml#1
activate.on-cloud-platform = kubernetes
isActive(context) = true
children[BEFORE] = [k8s.yml]
k8s.yml은 바인딩을 마친 뒤 imports가 없다.
application.yml#2는 여전히 dev Profile을 요구하지만 profiles == null이다.
application.yml#2
activate.on-profile = dev
isActive(context) = false
따라서 비활성 상태를 유지한다.
8.4 withProfiles: 활성 설정에서 dev 추론
withProfiles()가 만든 Binder는 IGNORE_PROFILES를 필터링하고, 핵심 바인딩에는 FAIL_ON_BIND_TO_INACTIVE_SOURCE를 활성화한다.
기본 문서인 application.yml#0은 현재 활성 상태이므로其中的 spring.profiles.active: dev가 읽힌다.
Profiles는 다음 항목을 함께 처리한다.
spring.profiles.activespring.profiles.defaultspring.profiles.includespring.profiles.groupadditionalProfiles
group을 모두 확장한 뒤의 최종 컨텍스트는 다음과 같다.
CloudPlatform = KUBERNETES
Profiles.active = [dev]
8.5 세 번째 AFTER: Profile 조건과 프로필별 파일 추가
application.yml#2를 다시 판정하면 다음과 같다.
on-profile = dev
Profiles 接受 dev
따라서 활성 상태가 된다. AFTER key가 아직 없으므로 feature.yml을 AFTER 단계에서 로드하고 바인딩한다.
BEFORE에서 imports를 이미 처리한 노드도 AFTER key는 여전히 없다. 따라서 같은 imports가 Profiles를携带着 다시 Resolver에 전달된다.
Resolver는 다음을 실행한다.
resolve(location)
resolveProfileSpecific(location, profiles)
일반 리소스는 이미 ConfigDataImporter.loaded에 있으므로 중복 제거된다. 새로 나타난 프로필별 Resource만 로드 결과에 포함된다.
INITIAL_IMPORT는 AFTER 단계에서 classpath:/를 다시 해석한다. 일반 application.yml은 이미 로드되어 중복 제거된다.
StandardConfigDataLocationResolver는 dev에 대해 application-dev.yml을 찾는다.
StandardConfigDataLoader는 해당 PropertySource에 PROFILE_SPECIFIC을 표시한다.
application-dev.yml은 먼저 UNBOUND_IMPORT로 AFTER children에 추가된 뒤, 다음과 같이 바인딩된다.
imports = [dev-db.yml]
현재 Profile을 알 수 있고 이 문서도 활성 상태이므로 dev-db.yml은 AFTER 단계에서 계속 확장된다.
최종 트리의 대략적인 구조는 다음과 같다.
application.yml의 조건 문서들은 첫 번째 일반 해석에서 만들어졌으므로 처음에는 모두 INITIAL_IMPORT의 BEFORE children에 위치한다. 내부 import는 이후 활성 상태가 되었을 때 해당 phase의 children에 기록된다.
application-dev.yml은 resolveProfileSpecific()에서 만들어진 프로필별 리소스이며, 트리 구조를 조정한 뒤 AFTER의 높은 우선순위 영역에 놓인다.
10. BEFORE와 AFTER의 실제 차이
ImportPhase는 현재 노드의 imports가 Profile 추론 전에 실행되는지, Profile 추론 후에 실행되는지를 나타낸다.
노드가 비활성 상태인지 활성 상태인지를 직접 나타내지는 않는다.
ImportPhase.get()의 로직에는 단 하나의 조건 확인만 있다.
phase = activationContext != null
&& activationContext.getProfiles() != null
? AFTER_PROFILE_ACTIVATION
: BEFORE_PROFILE_ACTIVATION;
따라서 BEFORE는 Profiles가 아직 확정되지 않았다는 뜻이고, AFTER는 Profiles가 확정되었다는 뜻이다.
10.1 Resolver 동작의 차이
ConfigDataImporter는 ActivationContext에서 Profiles를 가져와 ConfigDataLocationResolvers.resolve(...)에 전달한다.
Resolver는 항상 먼저 일반 해석인 resolver.resolve(context, location)을 호출한다.
Profiles가 null이면 일반 해석 결과를 그대로 반환한다.
Profiles가 null이 아니라면 resolver.resolveProfileSpecific(context, location, profiles)도 실행한다.
따라서 다음과 같이 나뉜다.
- 일반 해석:
resolve(location) - Profile을 알 수 있게 된 뒤의 해석:
resolve(location)+resolveProfileSpecific(location, profiles)
표준 파일 위치의 경우 resolveProfileSpecific()가 Profiles가 허용하는 프로필을 순회하고 Profile을 StandardConfigDataReference에 전달한다.
예를 들면 다음과 같다.
classpath:/디렉터리에서application-dev.yml후보가 생성된다.- 명시적인 파일
common.yml에서common-dev.yml후보가 생성된다.
StandardConfigDataLoader는 resource.getProfile() != null을 감지하면 해당 PropertySource에 PROFILE_SPECIFIC을 추가한다.
10.2 같은 imports를 두 번 처리하는 이유
BOUND Contributor의 imports는 BEFORE 처리가 끝나면 children[BEFORE]만 갖는다.
AFTER로 들어오면 hasUnprocessedImports(AFTER)가 여전히 true를 반환한다. 따라서 같은 Location은 두 가지 컨텍스트에서 각각 한 번씩 해석된다.
첫 번째 해석에서는 Profiles = null이므로 일반 리소스만 해석한다.
두 번째 해석에서는 Profiles = [dev]이므로 일반 리소스와 프로필별 리소스를 모두 해석한다.
ConfigDataImporter.loaded는 ConfigDataResource를 기준으로 중복을 제거하므로 일반 리소스에 Loader.load()가 다시 실행되지는 않는다.
두 번째 해석의 가치는 이전에 없던 프로필별 Resource를 찾아내는 데 있다.
“두 번 처리한다”는 것은 같은 설정 위치가 두 가지 컨텍스트에서 각각 한 번씩 resolve된다는 뜻이며, 같은 파일을 반드시 두 번 물리적으로 읽는다는 뜻은 아니다.
10.3 PROFILE_SPECIFIC에 따른 트리 구조 조정
withChildren(AFTER, children)는 moveProfileSpecific()를 호출한다.
이 메서드는 BEFORE 서브트리에서 PROFILE_SPECIFIC Option을 가진 자식 노드를 찾아 원래 위치에서 제거하고, 임시 Option을 없앤 뒤 현재 노드의 AFTER children에 추가한다.
이 조정으로 프로필별 출처는 Profile 활성화 이후의 높은 우선순위 영역으로 이동한다. 동시에 프로필별 import가 아닌 항목의 기존 부모·자식 관계는 그대로 유지된다.
다음 두 항목은 상태 관련 값이지만 의미가 다르다.
fromProfileSpecificImport:ConfigDataResolutionResult.isProfileSpecific()에서 가져온 값으로, Resource가resolveProfileSpecific()에 의해 생성되었음을 뜻한다.PROFILE_SPECIFICOption:PropertySource에 붙는 값으로, 이후 트리 구조와 우선순위를 조정하는 데 사용된다.
12. Options, 중복 제거, 예외 및 최종 우선순위
12.1 ConfigData.Option의 출처와 역할
Option은 ConfigDataLoader가 ConfigData를 생성할 때 제공하며, PropertySource별로 ofUnboundImport()에 전달된다.
Spring Boot은 “로컬 설정, 원격 설정, 설정 저장소” 같은 유형에 따라 이 표식들을 일률적으로 추론하지 않는다. 어떤 Option을 반환할지는 사용자 정의 Loader가 결정한다.
| Option | 설정 위치 | 사용 위치 | 역할 |
|---|---|---|---|
IGNORE_IMPORTS | Loader 또는 EMPTY_LOCATION의 고정 Option | withBoundProperties() | 바인딩 후 spring.config.import를 제거하지만 일반 속성은 계속 기여할 수 있음 |
IGNORE_PROFILES | Loader가 결정 | withProfiles()、include 스캔、잘못된 속성 검증 | 해당 PropertySource가 active、default、include、group 추론에 참여하지 않도록 함 |
PROFILE_SPECIFIC | StandardConfigDataLoader가 resource.profile != null일 때 설정 | moveProfileSpecific() | 프로필별 출처를 AFTER의 높은 우선순위 영역으로 이동 |
EMPTY_LOCATION은 IGNORE_IMPORTS를 사용한다. 해당 Location이 유효하지만 실제 PropertySource가 없으므로 빈 노드에서 import를 읽으면 안 된다는 뜻이다.
IGNORE_PROFILES의 정확한 정의는 해당 PropertySource에 있는 Profile 추론에 영향을 미치는 속성을 무시하는 것이다. “원격 설정은 항상 Profile을 무시한다”라는 뜻과는 다르다.
12.2 Resource 중복 제거와 순환 import
ConfigDataImporter는 Set<ConfigDataResource> loaded를 유지한다.
후보 Resource가 이미 존재하면 Loader를 다시 호출하지 않는다.
해당 Location은 loadedLocations에 기록된다.
A import B, B import A라고 가정하자.
중복 제거의 기준은 문자열 Location이 아니라 ConfigDataResource.equals()와 ConfigDataResource.hashCode()다.
서로 다른 Location이 같은 Resource로 해석되더라도 한 번만 로드된다.
같은 Location이 AFTER 단계에서 새로운 프로필별 Resource를 해석했다면 Resource가 서로 다르므로 이때는 다시 로드할 수 있다.
12.3 optional, 빈 위치 및 필수 위치 검사
optional: 접두사와 Resource 자체의 optional 상태는 ConfigDataImporter에 기록된다.
ConfigDataNotFoundException은 optional 위치에서 무시한다. optional이 아닌 위치는 spring.config.on-not-found 정책에 따라 처리한다.
Resolver는 유효한 빈 디렉터리 Resource를 반환할 수도 있다. Loader는 ConfigData.EMPTY를 반환하고, 이후 EMPTY_LOCATION Contributor가 생성된다.
최종 apply 전에 checkMandatoryLocations()는 활성 Contributor에 있는 optional이 아닌 imports를 모은 뒤 다음 항목들을 차감한다.
- 트리에 이미 등장한 Location
loadedLocationsoptionalLocations
남아 있는 Location은 성공적으로 해석되지 않은 필수 import로 간주된다.
12.4 최종 apply와 속성 우선순위
applyToEnvironment()는 먼저 다음 작업을 수행한다.
- 유효하지 않은 Profile 속성 검증
- 필수 위치 검사
이후 ContributorIterator로 전체 트리를 순회한다.
다음 조건을 모두 만족하는 노드만 Environment에 추가된다.
Kind == BOUND_IMPORT
PropertySource != null
在最终 ActivationContext 下 active
구조를 정리하면 다음과 같다.
비활성 노드도 Contributor Tree에는 남지만 Environment에는 들어가지 않는다.
명령줄 인수와 systemProperties 같은 EXISTING PropertySource는 이미 Environment에 있으며 ConfigData 결과보다 앞에 있으므로 더 높은 우선순위를 유지한다.
ConfigData 노드 내부의 순서는 AFTER → BEFORE → parent 순회 순서에 달려 있다.
마지막으로 DefaultPropertiesPropertySource는 맨 뒤로 이동하여 기본 속성이 가장 낮은 우선순위를 갖도록 한다.
14. 완전한 실행 모델
ConfigData의 전체 실행 과정은 다음 연속된 단계로 압축할 수 있다.
첫 번째 단계, 기존 Environment PropertySource를 래핑하고 INITIAL_IMPORT를 생성해 ROOT를 구성한다.
ROOT
├─ EXISTING
├─ INITIAL_IMPORT
└─ EXISTING
두 번째 단계, 첫 번째 BEFORE를 실행한다.
context = null
Profiles = null
CloudPlatform = null
조건 없는 문서를 계속 로드하고 바인딩하면서 조건 없는 imports를 재귀적으로 확장한다.
세 번째 단계, ActivationContext를 생성한다. Environment와 Contributor Binder에서 CloudPlatform을 추론하지만 Profiles는 여전히 null이다.
네 번째 단계, 두 번째 BEFORE를 실행한다. 플랫폼 조건을 다시 판정하고 on-cloud-platform 문서의 imports를 계속 확장한다.
다섯 번째 단계, Profiles를 계산한다.
IGNORE_PROFILES필터링- active 읽기
- default 읽기
- include 읽기
- additionalProfiles 읽기
- group 확장
여섯 번째 단계, 세 번째 AFTER를 실행한다.
- on-profile 조건을 다시 판정한다.
- 모든 imports를 Profiles와 함께 다시 해석한다.
application-dev.yml,common-dev.yml, 기타 프로필별 Resource를 찾는다.- loaded Resource 집합으로 중복을 제거하고 새 노드를 재귀적으로 계속 확장한다.
일곱 번째 단계, 각 단계 내부에서는 동일한 고정점 알고리즘을 실행한다.
여덟 번째 단계, 최종 apply를 실행한다.
- 필수 위치 검증
- 유효하지 않은 Profile 속성 검증
- 활성 BOUND PropertySource만 추가
AFTER → BEFORE → parent순서로 정렬- Environment의 defaultProfiles와 activeProfiles 설정
최종적인 멘탈 모델은 다음과 같다.
- Contributor Tree는 ConfigData의 중간 표현이다.
- ActivationContext는 현재 어떤 노드를 계속 확장할 수 있는지 결정한다.
- ImportPhase는 위치 해석 시 Profiles를 사용할 수 있는지 결정한다.
- ContributorIterator는 우선순위가 적용된 후위 순회를 제공한다.
- withProcessedImports의 while은 현재 풀 수 있는 설정 의존성을 안정 상태까지 계속 확장한다.
- applyToEnvironment만 최종 결과를 실제 Environment에 전달한다.