76 lines
3.7 KiB
Markdown
76 lines
3.7 KiB
Markdown
---
|
|
title: "Spring @ConditionalOnBean ordering trap — user-defined @Configuration vs autoconfiguration"
|
|
source_type: error-note
|
|
status: raw
|
|
tags: [spring-boot, conditional, autoconfiguration, ordering, tracing, TDD]
|
|
created: 2026-06-17
|
|
---
|
|
|
|
# Spring @ConditionalOnBean ordering trap
|
|
|
|
## Parent
|
|
|
|
[[raw/project-notes/ca-skeleton-operational-contract]]
|
|
|
|
## 현상 (Symptom)
|
|
|
|
`TracingConfig`(`@Configuration` — user-defined)에 `@ConditionalOnBean(Tracer.class)` + `@ConditionalOnMissingBean(SpanErrorRecorder.class)` 빈 메서드를 추가했다. `Tracer` bean은 `@AutoConfigureObservability(tracing=true)`로 autoconfiguration에서 공급된다.
|
|
|
|
테스트에서 `MicrometerSpanErrorRecorder`(SpanErrorRecorder 구현) 빈을 `context.getBean(SpanErrorRecorder.class)`로 조회하면:
|
|
|
|
```
|
|
NoSuchBeanDefinitionException: No qualifying bean of type 'dev.caskeleton.shared.tracing.SpanErrorRecorder' available
|
|
```
|
|
|
|
→ `Tracer` 빈은 존재(`context.getBean(Tracer.class)` 성공)하나, `@ConditionalOnBean(Tracer.class)` 조건은 false로 평가됨.
|
|
|
|
## 원인 (Root Cause)
|
|
|
|
Spring Boot 문서 주의사항: **`@ConditionalOnBean` / `@ConditionalOnMissingBean`은 bean definition ordering에 민감하다.**
|
|
|
|
- user-defined `@Configuration` 클래스(예: `@Import(TracingConfig.class)`)는 Spring이 autoconfiguration보다 먼저 처리한다.
|
|
- 조건 평가 시점(bean definition 등록 단계)에 `Tracer`는 아직 정의되지 않음 → `@ConditionalOnBean(Tracer.class)` = false.
|
|
- 결과적으로 `micrometerSpanErrorRecorder` 빈 메서드 자체가 스킵됨.
|
|
|
|
Spring 공식 문서 인용:
|
|
> "When using `@ConditionalOnBean` and `@ConditionalOnMissingBean` in component scan configurations, the condition evaluation is not predictable because of the order in which beans are created."
|
|
|
|
## 해결 (Fix)
|
|
|
|
`@ConditionalOnBean(Tracer.class)` 제거 → `ObjectProvider<Tracer>` 런타임 조회로 대체:
|
|
|
|
```java
|
|
@Bean
|
|
@ConditionalOnMissingBean(SpanErrorRecorder.class)
|
|
SpanErrorRecorder micrometerSpanErrorRecorder(ObjectProvider<Tracer> tracerProvider) {
|
|
Tracer tracer = tracerProvider.getIfAvailable();
|
|
if (tracer == null) {
|
|
return SpanErrorRecorder.NOOP;
|
|
}
|
|
return new MicrometerSpanErrorRecorder(tracer);
|
|
}
|
|
```
|
|
|
|
`ObjectProvider.getIfAvailable()`은 bean instantiation 시점(모든 bean definition이 등록된 후)에 호출되므로 `Tracer` autoconfiguration bean을 정확히 조회한다.
|
|
|
|
## 정리 (Lessons)
|
|
|
|
1. **`@ConditionalOnBean`은 `@AutoConfiguration`에서만 안전**하게 autoconfiguration bean을 조건으로 쓸 수 있다.
|
|
2. **user-defined `@Configuration` + `@ConditionalOnBean(autoconfig-provided-bean)`** = 순서 문제로 항상 false.
|
|
3. **해결 패턴 2가지**:
|
|
- `ObjectProvider<T>` 런타임 lazy resolution (이번 선택).
|
|
- user-defined config를 `@AutoConfiguration`으로 전환 + `AutoConfiguration.imports` 등록.
|
|
4. `@ConditionalOnMissingBean`은 **여전히 유효** — 이미 등록된 bean을 체크하는 것이므로 상대적으로 ordering에 덜 민감하다(단, user-defined bean이 autoconfiguration보다 먼저 등록될 것이 보장되어야 함).
|
|
|
|
## 재현 환경
|
|
|
|
- Spring Boot 3.5.x / Micrometer Tracing 1.5.12
|
|
- `TracingConfig` (`@Configuration`, `@EnableConfigurationProperties(TracingSettings.class)`)
|
|
- Test: `@SpringBootTest` + `@AutoConfigureObservability(tracing=true)` + `@EnableAutoConfiguration(exclude=[data-layer])`
|
|
- TDD red: `TracingActivationContextTest.realSpanErrorRecorderBeanReplacesNoop` → `NoSuchBeanDefinitionException`
|
|
|
|
## Evidence
|
|
|
|
- `actually-implemented`: ObjectProvider 패턴으로 교체 후 `TracingActivationContextTest` BUILD SUCCESSFUL.
|
|
- `locally-verified`: `:app-bootstrap:test` 전체 green.
|