회사에서 배치 관련 일을 하다가 XML과 Java Config를 섞어서 쓸 일이 생겼다. 이렇게 설정해서 테스트를 돌려보니 심상치 않은 에러들이 있어서, 이 김에 아예 스프링 빈과 관련된 개념을 정리해보고자 한다.
아래 도식도를 따라 스프링 빈들이 초기화된다. 실제 구현체들은 수십 개가 있을 정도로 다양하기 때문에, 상황에 맞게 필요한 구현체를 사용하면 된다. 프레임워크가 내부적으로 구현체 선택까지 잘 지원해주기 때문에 웬만해선 직접 고를 일은 없지만, 테스트 설정처럼 직접 설정이 필요할 때를 대비해서 알아두는 것도 좋아보인다.
크게 다음과 같은 과정을 거친다.
@ComponentScan, @Configuration 같은 코드들은 아래처럼 처리된다고 생각하면 된다. 물론 내부적으로 더 복잡한 과정을 거치겠지만, 이정도만 이해해도 크게 문제 없을 것 같다.
// 메인 애플리케이션 코드
public class MyApp {
public static void main(String[] args) {
// AnnotationConfigApplicationContext 생성
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
// BeanDefinitionRegistry를 가져옵니다.
BeanDefinitionRegistry registry = (BeanDefinitionRegistry) context.getBeanFactory();
// ClassPathBeanDefinitionScanner를 생성하고, @Configuration 클래스를 찾을 수 있도록 설정합니다.
ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(registry);
// @Configuration 어노테이션이 붙은 클래스를 찾도록 필터를 추가합니다.
scanner.addIncludeFilter(new AnnotationTypeFilter(Configuration.class));
// 스캔할 패키지 지정 (예: com.example.myapp.config)
scanner.scan("com.example.myapp.config");
// 컨텍스트 리프레시 전에 ConfigurationClassPostProcessor를 등록하여 @Configuration 클래스를 처리합니다.
context.addBeanFactoryPostProcessor(new ConfigurationClassPostProcessor());
// 컨텍스트 리프레시
context.refresh();
// 이제 컨텍스트에서 빈을 가져와서 사용할 수 있습니다.
MyService myService = context.getBean(MyService.class);
myService.doSomething();
// 컨텍스트 종료
context.close();
}
}
이제 조금씩 깊이를 추가해보자. 이전 도식도를 살펴보면 BeanDefinition 등록 이라는 과정이 있다. 일단 BeanDefinition이 뭔지부터 알아보자.
BeanDefinition는 AttributeAccessor와 BeanMetadataElement를 상속하는 Interface다. 상속하는 Interface 명에서 유추할 수 있듯이, Bean에 대한 Metadata(Attribute)를 정의하고 이에 대해 접근할 수 있도록 명세해놓은 것이다. 추상 구현체인 AbstractBeanDefinition를 살펴보자.
public abstract class AbstractBeanDefinition extends BeanMetadataAttributeAccessor implements BeanDefinition, Cloneable {
// 빈의 기본 스코프를 나타내는 상수 (빈 문자열은 기본 스코프인 싱글톤을 의미)
public static final String SCOPE_DEFAULT = "";
// 자동 주입 모드를 나타내는 상수들
public static final int AUTOWIRE_NO = 0; // 자동 주입 안 함
public static final int AUTOWIRE_BY_NAME = 1; // 이름으로 자동 주입
public static final int AUTOWIRE_BY_TYPE = 2; // 타입으로 자동 주입
public static final int AUTOWIRE_CONSTRUCTOR = 3; // 생성자로 자동 주입
/** @deprecated */
@Deprecated
public static final int AUTOWIRE_AUTODETECT = 4; // 자동 감지 (더 이상 사용되지 않음)
// 의존성 검사 수준을 나타내는 상수들
public static final int DEPENDENCY_CHECK_NONE = 0; // 의존성 검사 안 함
public static final int DEPENDENCY_CHECK_OBJECTS = 1; // 객체 참조만 검사
public static final int DEPENDENCY_CHECK_SIMPLE = 2; // 단순 타입만 검사
public static final int DEPENDENCY_CHECK_ALL = 3; // 모든 의존성 검사
// 특수한 빈 정의 속성을 위한 상수들
public static final String PREFERRED_CONSTRUCTORS_ATTRIBUTE = "preferredConstructors"; // 선호하는 생성자 속성명
public static final String ORDER_ATTRIBUTE = "order"; // 빈의 순서를 지정하는 속성명
public static final String INFER_METHOD = "(inferred)"; // 추론된 팩토리 메서드를 나타내는 값
// 빈 클래스 정보 (클래스 객체 또는 클래스 이름 문자열)
@Nullable
private volatile Object beanClass; // 빈의 클래스 또는 아직 로드되지 않은 경우 클래스 이름
@Nullable
private String scope; // 빈의 스코프 (예: 싱글톤, 프로토타입)
private boolean abstractFlag; // 이 빈 정의가 추상 클래스인지 여부
@Nullable
private Boolean lazyInit; // 빈의 지연 초기화 여부
private int autowireMode; // 자동 주입 모드
private int dependencyCheck; // 의존성 검사 모드
@Nullable
private String[] dependsOn; // 이 빈이 의존하는 다른 빈들의 이름
private boolean autowireCandidate; // 자동 주입 후보로 고려되는지 여부
private boolean primary; // 동일한 타입의 여러 빈 중 우선순위가 높은지 여부
private final Map<String, AutowireCandidateQualifier> qualifiers; // 자동 주입을 위한 추가적인 한정자들
@Nullable
private Supplier<?> instanceSupplier; // 빈 인스턴스를 생성하기 위한 공급자 콜백
private boolean nonPublicAccessAllowed; // 비공개 접근이 허용되는지 여부
private boolean lenientConstructorResolution; // 느슨한 생성자 해결을 허용하는지 여부
@Nullable
private String factoryBeanName; // 팩토리 빈의 이름 (있을 경우)
@Nullable
private String factoryMethodName; // 빈을 생성하는 팩토리 메서드의 이름 (있을 경우)
@Nullable
private ConstructorArgumentValues constructorArgumentValues; // 생성자 인자 값들
@Nullable
private MutablePropertyValues propertyValues; // 빈에 설정할 프로퍼티 값들
private MethodOverrides methodOverrides; // 메서드 오버라이드 정보 (메서드 인젝션에 사용)
@Nullable
private String[] initMethodNames; // 초기화 메서드 이름들
@Nullable
private String[] destroyMethodNames; // 소멸 메서드 이름들
private boolean enforceInitMethod; // 초기화 메서드의 존재를 강제할지 여부
private boolean enforceDestroyMethod; // 소멸 메서드의 존재를 강제할지 여부
private boolean synthetic; // 프레임워크에 의해 생성된 합성 빈인지 여부
private int role; // 빈의 역할 (애플리케이션, 지원, 인프라 등)
@Nullable
private String description; // 빈에 대한 설명
@Nullable
private Resource resource; // 이 빈 정의가 정의된 리소스 (예: XML 파일)
}
정말 변수가 많다. 주요하게 볼 것은 dependsOn 이다. Bean은 아직 생성이 안 됐지만 해당 Bean이 의존하는 다른 Bean들의 이름을 가지고 있다. 이러한 정보들이 BeanFactory(ApplicationContext)에 등록되고, 해당 정보를 기반으로 BeanFactoryPostProcessor 단계에서 의존성 그래프에 따라 객체를 초기화하는 것이다.
이제 BeanFactory에 Bean 메타데이터가 모두 등록되었으니 Bean을 생성할 차례이다. 만약 메타데이터에 대한 추가 등록이 필요할 경우, 이 단계에서도 메타데이터를 추가 등록하거나 수정할 수도 있다. 이중 가장 먼저 처리되는 ConfigurationClassPostProcessor를 알아보자.
@Configuration을 사용하는 클래스에 등록된 Bean을 생성한다. 이때 @ComponentScan, @ImportResource 등도 붙어있으면 처리한다. 그래서 이런 Annotation 들은 @Configuration 관련 Annotation과 같이 사용해야하는 것이다.
public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPostProcessor, BeanRegistrationAotProcessor, BeanFactoryInitializationAotProcessor, PriorityOrdered, ResourceLoaderAware, ApplicationStartupAware, BeanClassLoaderAware, EnvironmentAware {
// 스프링에서 설정 클래스(@Configuration)를 처리하고 빈 정의를 등록하는 주요 클래스
public static final AnnotationBeanNameGenerator IMPORT_BEAN_NAME_GENERATOR;
private static final String IMPORT_REGISTRY_BEAN_NAME;
// Import된 빈들을 관리하기 위한 이름 생성기와 레지스트리 이름 정의
private final Log logger = LogFactory.getLog(this.getClass());
// 로깅을 위한 로거 객체
private SourceExtractor sourceExtractor = new PassThroughSourceExtractor();
// 소스 추출기를 초기화 (빈 정의의 소스 위치를 추적하는 데 사용)
private ProblemReporter problemReporter = new FailFastProblemReporter();
// 문제 발생 시 즉시 실패(fail-fast) 방식으로 리포팅하는 리포터
@Nullable
private Environment environment;
// 스프링의 환경 정보를 담고 있는 객체 (프로파일, 프로퍼티 등)
private ResourceLoader resourceLoader = new DefaultResourceLoader();
// 리소스 로더 초기화 (클래스패스 리소스 등을 로드하기 위해 사용)
@Nullable
private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader();
// 빈 클래스 로더를 초기화 (클래스 파일을 로드하기 위해 사용)
private MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory();
private boolean setMetadataReaderFactoryCalled = false;
// 메타데이터 리더 팩토리를 초기화 (클래스 어노테이션 정보를 읽기 위해 사용)
private final Set<Integer> registriesPostProcessed = new HashSet();
private final Set<Integer> factoriesPostProcessed = new HashSet();
// 이미 처리된 레지스트리와 팩토리의 ID를 저장하여 중복 처리를 방지
@Nullable
private ConfigurationClassBeanDefinitionReader reader;
// 설정 클래스를 읽어서 빈 정의를 생성하는 리더 객체
private boolean localBeanNameGeneratorSet = false;
private BeanNameGenerator componentScanBeanNameGenerator;
private BeanNameGenerator importBeanNameGenerator;
// 빈 이름 생성기를 설정 (컴포넌트 스캔과 임포트된 빈에 사용)
private ApplicationStartup applicationStartup;
@Nullable
private List<PropertySourceDescriptor> propertySourceDescriptors;
// 애플리케이션 스타트업 측정 및 프로퍼티 소스 설명자를 위한 필드
public ConfigurationClassPostProcessor() {
// 생성자: 빈 이름 생성기와 애플리케이션 스타트업을 초기화
this.componentScanBeanNameGenerator = AnnotationBeanNameGenerator.INSTANCE;
this.importBeanNameGenerator = IMPORT_BEAN_NAME_GENERATOR;
this.applicationStartup = ApplicationStartup.DEFAULT;
}
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
// BeanDefinitionRegistryPostProcessor 인터페이스 구현 메서드
int registryId = System.identityHashCode(registry);
// 현재 레지스트리의 ID를 가져옴
if (this.registriesPostProcessed.contains(registryId)) {
// 이미 처리된 레지스트리인지 확인
throw new IllegalStateException("postProcessBeanDefinitionRegistry already called on this post-processor against " + registry);
// 중복 처리 시 예외 발생
} else if (this.factoriesPostProcessed.contains(registryId)) {
// 팩토리 후처리가 이미 이루어졌는지 확인
throw new IllegalStateException("postProcessBeanFactory already called on this post-processor against " + registry);
// 중복 처리 시 예외 발생
} else {
this.registriesPostProcessed.add(registryId);
// 현재 레지스트리를 처리 목록에 추가
this.processConfigBeanDefinitions(registry);
// 설정 빈 정의를 처리하는 핵심 메서드 호출
}
}
public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
// 설정 클래스(@Configuration)를 처리하여 빈 정의를 등록하는 메서드
List<BeanDefinitionHolder> configCandidates = new ArrayList();
// 설정 클래스 후보들을 담을 리스트 초기화
String[] candidateNames = registry.getBeanDefinitionNames();
// 현재 레지스트리에 등록된 모든 빈 정의 이름을 가져옴
for (String beanName : candidateNames) {
BeanDefinition beanDef = registry.getBeanDefinition(beanName);
// 각 빈 정의를 가져옴
if (beanDef.getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE) != null) {
// 이미 설정 클래스로 처리된 빈 정의인지 확인
if (this.logger.isDebugEnabled()) {
this.logger.debug("Bean definition has already been processed as a configuration class: " + beanDef);
// 디버그 로그 출력
}
} else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) {
// 설정 클래스 후보인지 검사 (@Configuration, @Component 등 어노테이션이 있는지 확인)
configCandidates.add(new BeanDefinitionHolder(beanDef, beanName));
// 설정 클래스 후보 리스트에 추가
}
}
if (!configCandidates.isEmpty()) {
// 설정 클래스 후보가 존재하면
configCandidates.sort((bd1, bd2) -> {
int i1 = ConfigurationClassUtils.getOrder(bd1.getBeanDefinition());
int i2 = ConfigurationClassUtils.getOrder(bd2.getBeanDefinition());
return Integer.compare(i1, i2);
});
// @Order 어노테이션에 따라 설정 클래스의 순서를 정렬
SingletonBeanRegistry singletonRegistry = null;
if (registry instanceof SingletonBeanRegistry) {
SingletonBeanRegistry sbr = (SingletonBeanRegistry) registry;
singletonRegistry = sbr;
if (!this.localBeanNameGeneratorSet) {
BeanNameGenerator generator = (BeanNameGenerator) sbr.getSingleton("org.springframework.context.annotation.internalConfigurationBeanNameGenerator");
if (generator != null) {
this.componentScanBeanNameGenerator = generator;
this.importBeanNameGenerator = generator;
}
}
}
if (this.environment == null) {
this.environment = new StandardEnvironment();
// 환경 객체가 없을 경우 기본 환경을 설정
}
// 설정 클래스 파싱을 위한 파서 생성
ConfigurationClassParser parser = new ConfigurationClassParser(
this.metadataReaderFactory, this.problemReporter, this.environment,
this.resourceLoader, this.componentScanBeanNameGenerator, registry
);
Set<BeanDefinitionHolder> candidates = new LinkedHashSet(configCandidates);
// 중복을 허용하지 않는 LinkedHashSet으로 설정 클래스 후보들을 저장
Set<ConfigurationClass> alreadyParsed = new HashSet(configCandidates.size());
// 이미 파싱된 설정 클래스를 저장할 집합
do {
// 설정 클래스 파싱 및 빈 정의 로딩을 반복
StartupStep processConfig = this.applicationStartup.start("spring.context.config-classes.parse");
// 애플리케이션 스타트업 측정을 위한 스텝 시작
parser.parse(candidates);
// 설정 클래스 후보들을 파싱 (여기서 @ImportResource 등이 처리됨)
parser.validate();
// 파싱된 설정 클래스의 유효성 검사
Set<ConfigurationClass> configClasses = new LinkedHashSet(parser.getConfigurationClasses());
configClasses.removeAll(alreadyParsed);
// 이미 파싱된 클래스를 제외하고 새로운 설정 클래스만 남김
if (this.reader == null) {
this.reader = new ConfigurationClassBeanDefinitionReader(
registry, this.sourceExtractor, this.resourceLoader,
this.environment, this.importBeanNameGenerator, parser.getImportRegistry()
);
// 설정 클래스에서 빈 정의를 읽어올 리더를 초기화
}
this.reader.loadBeanDefinitions(configClasses);
// 파싱된 설정 클래스로부터 빈 정의를 로드하고 등록 (@Bean 메서드, @ImportResource 등 처리)
alreadyParsed.addAll(configClasses);
// 이미 파싱된 설정 클래스에 추가
processConfig.tag("classCount", () -> String.valueOf(configClasses.size())).end();
// 스타트업 측정 스텝 종료
candidates.clear();
// 다음 반복을 위해 후보 리스트 초기화
if (registry.getBeanDefinitionCount() > candidateNames.length) {
// 새로운 빈 정의가 추가되었는지 확인
String[] newCandidateNames = registry.getBeanDefinitionNames();
Set<String> oldCandidateNames = Set.of(candidateNames);
Set<String> alreadyParsedClasses = new HashSet();
for (ConfigurationClass configurationClass : alreadyParsed) {
alreadyParsedClasses.add(configurationClass.getMetadata().getClassName());
}
for (String candidateName : newCandidateNames) {
if (!oldCandidateNames.contains(candidateName)) {
BeanDefinition bd = registry.getBeanDefinition(candidateName);
if (ConfigurationClassUtils.checkConfigurationClassCandidate(bd, this.metadataReaderFactory)
&& !alreadyParsedClasses.contains(bd.getBeanClassName())) {
candidates.add(new BeanDefinitionHolder(bd, candidateName));
// 새로운 설정 클래스 후보를 추가
}
}
}
candidateNames = newCandidateNames;
// 후보 이름 목록을 업데이트
}
} while (!candidates.isEmpty());
// 새로운 설정 클래스 후보가 없을 때까지 반복
if (singletonRegistry != null && !singletonRegistry.containsSingleton(IMPORT_REGISTRY_BEAN_NAME)) {
singletonRegistry.registerSingleton(IMPORT_REGISTRY_BEAN_NAME, parser.getImportRegistry());
// Import된 리소스의 레지스트리를 싱글톤으로 등록
}
this.propertySourceDescriptors = parser.getPropertySourceDescriptors();
// 프로퍼티 소스 정보를 저장
MetadataReaderFactory var26 = this.metadataReaderFactory;
if (var26 instanceof CachingMetadataReaderFactory) {
CachingMetadataReaderFactory cachingMetadataReaderFactory = (CachingMetadataReaderFactory) var26;
cachingMetadataReaderFactory.clearCache();
// 메타데이터 리더 팩토리의 캐시를 정리
}
}
}
}
이제 회사에서 겪었던 문제를 풀어나가보자. 문제는 context-foo-job.xml 설정을 BatchFooJobConfig.java 로 옮기면서 Bean 등록 순서가 꼬인 것이었다. 일단 사용하고 있는 모든 설정은 다음과 같았다.
초기 코드는 아래와 같았다.
@ContextConfiguration(locations = {
"classpath:/spring/context-common.xml",
"classpath:/spring/context-datasource.xml",
"classpath:/spring/context-batch.xml",
"classpath:/spring/job/context-foo-job.xml",
})
public class FooJobMannualRunner {
@Autowired
private BatchJobRunner runner;
@Test
public void run() {
runner.run("FOO_JOB"):
}
}
일단 Job 설정을 xml에서 java로 변경했으므로, 코드를 아래처럼 변경해봤더니 locations랑 classes는 동시에 쓸 수 없다고 에러가 발생했다.
@ContextConfiguration(
locations = {
"classpath:/spring/context-common.xml",
"classpath:/spring/context-datasource.xml",
"classpath:/spring/context-batch.xml"
},
classes = BatchFooJobConfig.class
)
public class FooJobMannualRunner {
@Autowired
private BatchJobRunner runner;
@Test
public void run() {
runner.run("FOO_JOB"):
}
}
그래서 최종적으론 다음처럼 설정했더니 이번엔 context-batch.xml에 있는 JobRepository 라는 빈이 없다는 에러가 BatchFooJobConfig에서 발생했다. ContextConfiguration에 있는 클래스 순서를 바꾸면 또 되고...
@ContextConfiguration(classes = {
BatchFooJobConfig.class,
FooJobMannualConfig.class
})
public class FooJobMannualRunner {
@Autowired
private BatchJobRunner runner;
@Test
public void run() {
runner.run("FOO_JOB"):
}
}
@ImportResource(value = {
"classpath:/spring/context-common.xml",
"classpath:/spring/context-datasource.xml",
"classpath:/spring/context-batch.xml"
})
@Configuration
public class FooJobMannualConfig {
}
@Configuration
public class BatchFooJobConfig {
@Bean
public Job fooJob(
final JobRepository jobRepository,
@Qualifier("fooStep") final Step step
) {
return jobRepository.get("FOO_JOB")
.start(step)
.build():
}
}
아래처럼 AbstractTestContextBootstrapper에서 addAll로 처리하는 코드가 있기 때문이다. 그렇다보니 스프링 공식 문서에서도 등록 순서에 조심하라고 가이드하고 있다.
// 순서를 유지하면서 locations와 classes에 추가
locations.addAll(0, Arrays.asList(configAttributes.getLocations()));
classes.addAll(0, Arrays.asList(configAttributes.getClasses()));
상세 흐름은 다음과 같다.
1. 테스트 실행 시점에 AbstractTestContextBootstrapper가 동작한다.
• 테스트 클래스에서 @ContextConfiguration 어노테이션을 찾아 설정 정보를 수집한다.
• buildMergedContextConfiguration() 메서드를 통해 설정 파일 경로나 설정 클래스 정보를 수집하여 MergedContextConfiguration을 생성한다.
2. MergedContextConfiguration을 사용하여 테스트 컨텍스트를 구성한다.
• 테스트에서 사용할 애플리케이션 컨텍스트를 로드하기 위한 모든 설정 정보를 포함한다.
3. AbstractContextLoader를 사용하여 애플리케이션 컨텍스트를 로드한다.
• processContextConfiguration()을 통해 설정 파일 경로나 설정 클래스를 처리한다.
• prepareContext()를 통해 컨텍스트를 초기화하고, 필요한 설정을 적용한다.
4. 애플리케이션 컨텍스트 초기화 과정에서 설정 클래스가 처리된다.
• 설정 클래스(@Configuration이 붙은 클래스)는 ConfigurationClassPostProcessor에 의해 처리된다.
• 이 과정에서 @ImportResource 어노테이션이 설정 클래스의 파싱 시점에 가장 먼저 처리되어 XML 리소스의 빈들이 로드된다.
5. @ContextConfiguration은 테스트 컨텍스트를 구성하기 위한 설정 정보를 제공한다.
• 실제 애플리케이션 컨텍스트에서 빈을 로드하는 과정은 설정 클래스의 파싱 및 처리 과정에서 이루어진다.
• 따라서 @ImportResource를 통해 로드된 빈들은 애플리케이션 컨텍스트 초기화 과정에서 이미 로드된다.
상세 내용은 아래 부분을 참고해보면 된다.
1. AbstractTestContextBootstrapper 클래스의 buildMergedContextConfiguration() 메서드
2. MergedContextConfiguration 클래스
3. AbstractContextLoader 및 그 구현체들의 loadContext() 메서드
AbstractTestContextBootstrapper는 스프링의 테스트 컨텍스트 부트스트래핑 과정에서 핵심적인 역할을 하는 추상 클래스다. 이 클래스는 테스트 클래스에서 사용되는 설정 정보를 수집하고, 테스트 컨텍스트를 초기화한다.
public abstract class AbstractTestContextBootstrapper implements TestContextBootstrapper {
private final Log logger = LogFactory.getLog(this.getClass());
@Nullable
private BootstrapContext bootstrapContext;
public void setBootstrapContext(BootstrapContext bootstrapContext) {
// 부트스트랩 컨텍스트를 설정
this.bootstrapContext = bootstrapContext;
}
public BootstrapContext getBootstrapContext() {
// 부트스트랩 컨텍스트를 반환
Assert.state(this.bootstrapContext != null, "No BootstrapContext set");
return this.bootstrapContext;
}
public TestContext buildTestContext() {
// 테스트 컨텍스트를 생성
return new DefaultTestContext(
this.getBootstrapContext().getTestClass(),
this.buildMergedContextConfiguration(),
this.getCacheAwareContextLoaderDelegate()
);
}
public final MergedContextConfiguration buildMergedContextConfiguration() {
// 테스트 클래스 가져오기
Class<?> testClass = this.getBootstrapContext().getTestClass();
// ContextLoaderDelegate 가져오기
CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate = this.getCacheAwareContextLoaderDelegate();
// @ContextConfiguration 또는 @ContextHierarchy 어노테이션이 있는지 확인
if (TestContextAnnotationUtils.findAnnotationDescriptorForTypes(
testClass, new Class[]{ContextConfiguration.class, ContextHierarchy.class}) == null) {
// 어노테이션이 없으면 기본 MergedContextConfiguration 생성
return this.buildDefaultMergedContextConfiguration(testClass, cacheAwareContextLoaderDelegate);
} else if (TestContextAnnotationUtils.findAnnotationDescriptor(testClass, ContextHierarchy.class) == null) {
// @ContextHierarchy가 없으면 단일 컨텍스트 설정을 생성
return this.buildMergedContextConfiguration(
testClass,
ContextLoaderUtils.resolveContextConfigurationAttributes(testClass),
null,
cacheAwareContextLoaderDelegate,
true
);
} else {
// @ContextHierarchy가 있으면 계층 구조 처리
// ...
}
}
private MergedContextConfiguration buildMergedContextConfiguration(
Class<?> testClass,
List<ContextConfigurationAttributes> configAttributesList,
@Nullable MergedContextConfiguration parentConfig,
CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate,
boolean requireLocationsClassesOrInitializers) {
// ContextLoader를 결정
ContextLoader contextLoader = this.resolveContextLoader(testClass, configAttributesList);
List<String> locations = new ArrayList<>();
List<Class<?>> classes = new ArrayList<>();
// 설정 어트리뷰트 리스트를 순서대로 처리
for (ContextConfigurationAttributes configAttributes : configAttributesList) {
if (contextLoader instanceof SmartContextLoader smartContextLoader) {
// SmartContextLoader를 사용하여 설정 처리
smartContextLoader.processContextConfiguration(configAttributes);
// 순서를 유지하면서 locations와 classes에 추가
locations.addAll(0, Arrays.asList(configAttributes.getLocations()));
classes.addAll(0, Arrays.asList(configAttributes.getClasses()));
} else {
// ...
}
// ...
}
// MergedContextConfiguration 생성
MergedContextConfiguration mergedConfig = new MergedContextConfiguration(
testClass,
StringUtils.toStringArray(locations),
ClassUtils.toClassArray(classes),
// 기타 설정들...
contextLoader,
cacheAwareContextLoaderDelegate,
parentConfig
);
return this.processMergedContextConfiguration(mergedConfig);
}
}
MergedContextConfiguration 클래스는 테스트 컨텍스트를 로드하기 위한 설정 정보를 담는 클래스다.
public class MergedContextConfiguration implements Serializable {
private final Class<?> testClass; // 테스트 클래스
private final String[] locations; // 설정 파일 경로 (예: XML 파일)
private final Class<?>[] classes; // 설정 클래스 (@Configuration이 붙은 클래스)
// 기타 필드들...
public MergedContextConfiguration(
Class<?> testClass,
@Nullable String[] locations,
@Nullable Class<?>[] classes,
// 기타 매개변수들...
ContextLoader contextLoader,
@Nullable CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate,
@Nullable MergedContextConfiguration parent) {
// 필드 초기화
this.testClass = testClass;
this.locations = processStrings(locations);
this.classes = processClasses(classes);
// ...
}
// Getter 메서드들...
}
AnnotationConfigContextLoader는 AbstractContextLoader의 구현체 중 하나이다. AbstractContextLoader는 스프링의 테스트 컨텍스트 로더의 기본 구현체로, 애플리케이션 컨텍스트를 로드하는 역할을 한다.
public class AnnotationConfigContextLoader extends AbstractContextLoader {
@Override
protected ApplicationContext loadContext(MergedContextConfiguration mergedConfig) throws Exception {
// 설정 클래스를 사용하여 애플리케이션 컨텍스트를 로드
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
// 설정 클래스를 순서대로 등록
context.register(mergedConfig.getClasses());
// 컨텍스트를 초기화
context.refresh();
return context;
}
// ...
}
public abstract class AbstractContextLoader implements SmartContextLoader {
// ...
public void processContextConfiguration(ContextConfigurationAttributes configAttributes) {
// 설정 파일 경로를 처리
String[] processedLocations = this.processLocationsInternal(
configAttributes.getDeclaringClass(), configAttributes.getLocations());
configAttributes.setLocations(processedLocations);
}
protected void prepareContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) {
// 애플리케이션 컨텍스트를 준비
context.getEnvironment().setActiveProfiles(mergedConfig.getActiveProfiles());
// 프로퍼티 소스 추가 등
TestPropertySourceUtils.addPropertySourcesToEnvironment(
context, mergedConfig.getPropertySourceDescriptors());
TestPropertySourceUtils.addInlinedPropertiesToEnvironment(
context, mergedConfig.getPropertySourceProperties());
this.invokeApplicationContextInitializers(context, mergedConfig);
}
private void invokeApplicationContextInitializers(
ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) {
// 컨텍스트 초기화기(ApplicationContextInitializer)를 호출하여 추가적인 초기화 작업 수행
Set<Class<? extends ApplicationContextInitializer<?>>> initializerClasses =
mergedConfig.getContextInitializerClasses();
// 각 초기화기를 인스턴스화하고 초기화 메서드 호출
// ...
}
public final String[] processLocations(Class<?> clazz, String... locations) {
return this.processLocationsInternal(clazz, locations);
}
private String[] processLocationsInternal(Class<?> clazz, String... locations) {
// 설정 파일 경로가 없으면 기본 경로 생성
return ObjectUtils.isEmpty(locations) && this.isGenerateDefaultLocations()
? this.generateDefaultLocations(clazz)
: this.modifyLocations(clazz, locations);
}
protected String[] generateDefaultLocations(Class<?> clazz) {
// 클래스 이름을 기반으로 기본 설정 파일 경로를 생성
// 예: "com/example/MyTest" -> "classpath:/com/example/MyTest-context.xml"
// ...
}
protected String[] modifyLocations(Class<?> clazz, String... locations) {
// 설정 파일 경로를 적절한 형식으로 변환
return TestContextResourceUtils.convertToClasspathResourcePaths(clazz, locations);
}
// 기타 메서드들...
}
class ConfigurationClassParser {
// 기본적으로 제외할 클래스 이름에 대한 필터를 정의
private static final Predicate<String> DEFAULT_EXCLUSION_FILTER = (className) -> {
return className.startsWith("java.lang.annotation.") || className.startsWith("org.springframework.stereotype.");
};
// 지연된 ImportSelectorHolder를 정렬하기 위한 Comparator 정의
private static final Comparator<DeferredImportSelectorHolder> DEFERRED_IMPORT_COMPARATOR = (o1, o2) -> {
return AnnotationAwareOrderComparator.INSTANCE.compare(o1.getImportSelector(), o2.getImportSelector());
};
private final Log logger = LogFactory.getLog(this.getClass());
// 로깅을 위한 로거 객체 생성
private final MetadataReaderFactory metadataReaderFactory;
// 클래스 메타데이터를 읽기 위한 팩토리
private final ProblemReporter problemReporter;
// 문제 발생 시 리포팅하는 객체
private final Environment environment;
// 스프링의 환경 정보를 담고 있는 객체
private final ResourceLoader resourceLoader;
// 리소스를 로드하기 위한 객체
@Nullable
private final PropertySourceRegistry propertySourceRegistry;
// 프로퍼티 소스를 등록하기 위한 레지스트리 (환경이 ConfigurableEnvironment일 때만 사용)
private final BeanDefinitionRegistry registry;
// 빈 정의를 등록하기 위한 레지스트리
private final ComponentScanAnnotationParser componentScanParser;
// @ComponentScan 어노테이션을 파싱하는 파서
private final ConditionEvaluator conditionEvaluator;
// @Conditional 어노테이션을 평가하는 평가자
private final Map<ConfigurationClass, ConfigurationClass> configurationClasses = new LinkedHashMap();
// 이미 처리된 설정 클래스들을 저장하는 맵
private final Map<String, ConfigurationClass> knownSuperclasses = new HashMap();
// 이미 알려진 슈퍼클래스를 저장하는 맵
private final ImportStack importStack = new ImportStack();
// Import 어노테이션 처리 시 순환 참조를 방지하기 위한 스택
private final DeferredImportSelectorHandler deferredImportSelectorHandler = new DeferredImportSelectorHandler();
// 지연된 ImportSelector를 처리하는 핸들러
private final SourceClass objectSourceClass = new SourceClass(Object.class);
// java.lang.Object를 나타내는 SourceClass 객체
public ConfigurationClassParser(
MetadataReaderFactory metadataReaderFactory,
ProblemReporter problemReporter,
Environment environment,
ResourceLoader resourceLoader,
BeanNameGenerator componentScanBeanNameGenerator,
BeanDefinitionRegistry registry) {
// 생성자: 필요한 의존성을 주입받아 초기화
this.metadataReaderFactory = metadataReaderFactory;
this.problemReporter = problemReporter;
this.environment = environment;
this.resourceLoader = resourceLoader;
Environment var8 = this.environment;
PropertySourceRegistry var10001;
if (var8 instanceof ConfigurableEnvironment ce) {
var10001 = new PropertySourceRegistry(new PropertySourceProcessor(ce, this.resourceLoader));
// 환경이 ConfigurableEnvironment이면 PropertySourceRegistry를 초기화
} else {
var10001 = null;
// 아니면 null로 설정
}
this.propertySourceRegistry = var10001;
this.registry = registry;
this.componentScanParser = new ComponentScanAnnotationParser(
environment, resourceLoader, componentScanBeanNameGenerator, registry);
// @ComponentScan 어노테이션을 파싱하는 파서 초기화
this.conditionEvaluator = new ConditionEvaluator(registry, environment, resourceLoader);
// 조건 평가기 초기화
}
public void parse(Set<BeanDefinitionHolder> configCandidates) {
// 설정 클래스 후보들을 파싱하는 메서드
Iterator var2 = configCandidates.iterator();
while (var2.hasNext()) {
BeanDefinitionHolder holder = (BeanDefinitionHolder) var2.next();
BeanDefinition bd = holder.getBeanDefinition();
try {
if (bd instanceof AnnotatedBeanDefinition annotatedBeanDef) {
// 어노테이션이 있는 빈 정의인 경우
this.parse(annotatedBeanDef.getMetadata(), holder.getBeanName());
// 메타데이터와 빈 이름으로 파싱
} else {
if (bd instanceof AbstractBeanDefinition abstractBeanDef) {
if (abstractBeanDef.hasBeanClass()) {
// 빈 클래스가 이미 로드된 경우
this.parse(abstractBeanDef.getBeanClass(), holder.getBeanName());
continue;
}
}
this.parse(bd.getBeanClassName(), holder.getBeanName());
// 클래스 이름으로 파싱
}
} catch (BeanDefinitionStoreException var7) {
BeanDefinitionStoreException ex = var7;
throw ex;
// 예외 발생 시 그대로 던짐
} catch (Throwable var8) {
Throwable ex = var8;
throw new BeanDefinitionStoreException(
"Failed to parse configuration class [" + bd.getBeanClassName() + "]", ex);
// 다른 예외 발생 시 BeanDefinitionStoreException으로 래핑하여 던짐
}
}
this.deferredImportSelectorHandler.process();
// 지연된 ImportSelector들을 처리
}
protected final void parse(@Nullable String className, String beanName) throws IOException {
// 클래스 이름과 빈 이름으로 파싱하는 메서드
Assert.notNull(className, "No bean class name for configuration class bean definition");
// 클래스 이름이 null이면 예외 발생
MetadataReader reader = this.metadataReaderFactory.getMetadataReader(className);
// 메타데이터 리더를 통해 클래스의 메타데이터를 읽음
this.processConfigurationClass(new ConfigurationClass(reader, beanName), DEFAULT_EXCLUSION_FILTER);
// 읽어온 메타데이터로 ConfigurationClass를 생성하고 처리
}
protected final void parse(Class<?> clazz, String beanName) throws IOException {
// 클래스 객체와 빈 이름으로 파싱하는 메서드
this.processConfigurationClass(new ConfigurationClass(clazz, beanName), DEFAULT_EXCLUSION_FILTER);
// 클래스 객체로부터 ConfigurationClass를 생성하고 처리
}
protected final void parse(AnnotationMetadata metadata, String beanName) throws IOException {
// 어노테이션 메타데이터와 빈 이름으로 파싱하는 메서드
this.processConfigurationClass(new ConfigurationClass(metadata, beanName), DEFAULT_EXCLUSION_FILTER);
// 메타데이터로부터 ConfigurationClass를 생성하고 처리
}
protected void processConfigurationClass(ConfigurationClass configClass, Predicate<String> filter) throws IOException {
// 설정 클래스를 실제로 처리하는 메서드
if (!this.conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.PARSE_CONFIGURATION)) {
// @Conditional 어노테이션을 평가하여 스킵할지 결정
ConfigurationClass existingClass = this.configurationClasses.get(configClass);
if (existingClass != null) {
// 이미 처리된 설정 클래스인 경우
if (configClass.isImported()) {
if (existingClass.isImported()) {
existingClass.mergeImportedBy(configClass);
// 이미 import된 클래스라면 import된 정보를 병합
}
return;
// 더 이상 처리하지 않음
}
this.configurationClasses.remove(configClass);
// 기존 클래스를 제거하여 다시 처리할 수 있게 함
Collection var10000 = this.knownSuperclasses.values();
Objects.requireNonNull(configClass);
var10000.removeIf(configClass::equals);
// knownSuperclasses에서 해당 클래스를 제거
}
SourceClass sourceClass = null;
try {
sourceClass = this.asSourceClass(configClass, filter);
// 설정 클래스를 SourceClass로 변환
do {
sourceClass = this.doProcessConfigurationClass(configClass, sourceClass, filter);
// 설정 클래스 처리 메서드 호출
} while (sourceClass != null);
// 슈퍼클래스가 존재하면 반복하여 처리
} catch (IOException var6) {
IOException ex = var6;
throw new BeanDefinitionStoreException(
"I/O failure while processing configuration class [" + sourceClass + "]", ex);
// 예외 발생 시 BeanDefinitionStoreException으로 래핑하여 던짐
}
this.configurationClasses.put(configClass, configClass);
// 처리된 설정 클래스를 맵에 저장
}
}
@Nullable
protected final SourceClass doProcessConfigurationClass(
ConfigurationClass configClass, SourceClass sourceClass, Predicate<String> filter) throws IOException {
// 설정 클래스를 실제로 처리하는 메서드
if (configClass.getMetadata().isAnnotated(Component.class.getName())) {
// @Component 어노테이션이 붙어 있는 경우
this.processMemberClasses(configClass, sourceClass, filter);
// 멤버 클래스들을 처리
}
Iterator var4 = AnnotationConfigUtils.attributesForRepeatable(
sourceClass.getMetadata(), PropertySource.class, PropertySources.class, true).iterator();
// @PropertySource 어노테이션을 처리
AnnotationAttributes importResource;
while (var4.hasNext()) {
importResource = (AnnotationAttributes) var4.next();
if (this.propertySourceRegistry != null) {
this.propertySourceRegistry.processPropertySource(importResource);
// 프로퍼티 소스를 등록
} else {
this.logger.info("Ignoring @PropertySource annotation on [" + sourceClass.getMetadata().getClassName()
+ "]. Reason: Environment must implement ConfigurableEnvironment");
// 환경이 ConfigurableEnvironment가 아니면 무시하고 로그 출력
}
}
Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
sourceClass.getMetadata(), ComponentScan.class, ComponentScans.class, MergedAnnotation::isDirectlyPresent);
// 직접 선언된 @ComponentScan 어노테이션을 가져옴
if (componentScans.isEmpty()) {
componentScans = AnnotationConfigUtils.attributesForRepeatable(
sourceClass.getMetadata(), ComponentScan.class, ComponentScans.class, MergedAnnotation::isMetaPresent);
// 메타 어노테이션으로 선언된 @ComponentScan 어노테이션을 가져옴
}
if (!componentScans.isEmpty() && !this.conditionEvaluator.shouldSkip(
sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
// @ComponentScan이 있고, 조건에 맞는 경우
Iterator var14 = componentScans.iterator();
while (var14.hasNext()) {
AnnotationAttributes componentScan = (AnnotationAttributes) var14.next();
Set<BeanDefinitionHolder> scannedBeanDefinitions = this.componentScanParser.parse(
componentScan, sourceClass.getMetadata().getClassName());
// 컴포넌트 스캔을 수행하여 빈 정의들을 가져옴
Iterator var8 = scannedBeanDefinitions.iterator();
while (var8.hasNext()) {
BeanDefinitionHolder holder = (BeanDefinitionHolder) var8.next();
BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition();
if (bdCand == null) {
bdCand = holder.getBeanDefinition();
}
if (ConfigurationClassUtils.checkConfigurationClassCandidate(
bdCand, this.metadataReaderFactory)) {
this.parse(bdCand.getBeanClassName(), holder.getBeanName());
// 스캔된 빈 정의가 설정 클래스 후보이면 재귀적으로 파싱
}
}
}
}
this.processImports(configClass, sourceClass, this.getImports(sourceClass), filter, true);
// Import 어노테이션을 처리
importResource = AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), ImportResource.class);
if (importResource != null) {
// @ImportResource 어노테이션이 있으면
String[] resources = importResource.getStringArray("locations");
Class<? extends BeanDefinitionReader> readerClass = importResource.getClass("reader");
for (String resource : resources) {
String resolvedResource = this.environment.resolveRequiredPlaceholders(resource);
// 리소스 경로의 플레이스홀더를 해결
configClass.addImportedResource(resolvedResource, readerClass);
// 설정 클래스에 임포트된 리소스로 추가
}
}
Set<MethodMetadata> beanMethods = this.retrieveBeanMethodMetadata(sourceClass);
// @Bean 메서드를 가져옴
Iterator var18 = beanMethods.iterator();
while (var18.hasNext()) {
MethodMetadata methodMetadata = (MethodMetadata) var18.next();
configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass));
// 설정 클래스에 @Bean 메서드로 추가
}
this.processInterfaces(configClass, sourceClass);
// 인터페이스들을 처리
if (sourceClass.getMetadata().hasSuperClass()) {
// 슈퍼클래스가 있는 경우
String superclass = sourceClass.getMetadata().getSuperClassName();
if (superclass != null && !superclass.startsWith("java")
&& !this.knownSuperclasses.containsKey(superclass)) {
this.knownSuperclasses.put(superclass, configClass);
return sourceClass.getSuperClass();
// 슈퍼클래스를 다음에 처리할 SourceClass로 반환하여 반복 처리
}
}
return null;
// 더 이상 처리할 클래스가 없으면 null 반환
}
private void processImports(
ConfigurationClass configClass,
SourceClass currentSourceClass,
Collection<SourceClass> importCandidates,
Predicate<String> exclusionFilter,
boolean checkForCircularImports) {
// Import 어노테이션을 처리하는 메서드
if (!importCandidates.isEmpty()) {
if (checkForCircularImports && this.isChainedImportOnStack(configClass)) {
// 순환 임포트가 발생하는지 검사
this.problemReporter.error(new CircularImportProblem(configClass, this.importStack));
// 순환 임포트 문제를 리포팅
} else {
this.importStack.push(configClass);
// 현재 설정 클래스를 스택에 추가
try {
Iterator var22 = importCandidates.iterator();
while (var22.hasNext()) {
SourceClass candidate = (SourceClass) var22.next();
Class candidateClass;
if (candidate.isAssignable(ImportSelector.class)) {
// ImportSelector 인터페이스를 구현한 경우
candidateClass = candidate.loadClass();
ImportSelector selector = (ImportSelector) ParserStrategyUtils.instantiateClass(
candidateClass, ImportSelector.class, this.environment, this.resourceLoader, this.registry);
// ImportSelector 인스턴스 생성
Predicate<String> selectorFilter = selector.getExclusionFilter();
if (selectorFilter != null) {
exclusionFilter = exclusionFilter.or(selectorFilter);
// ImportSelector의 제외 필터를 추가
}
if (selector instanceof DeferredImportSelector) {
// DeferredImportSelector인 경우
DeferredImportSelector deferredImportSelector = (DeferredImportSelector) selector;
this.deferredImportSelectorHandler.handle(configClass, deferredImportSelector);
// 지연된 ImportSelector로 처리
} else {
String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());
// ImportSelector를 통해 임포트할 클래스 이름들을 가져옴
Collection<SourceClass> importSourceClasses = this.asSourceClasses(importClassNames, exclusionFilter);
// 클래스 이름들을 SourceClass로 변환
this.processImports(configClass, currentSourceClass, importSourceClasses, exclusionFilter, false);
// 재귀적으로 임포트 처리
}
} else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {
// ImportBeanDefinitionRegistrar 인터페이스를 구현한 경우
candidateClass = candidate.loadClass();
ImportBeanDefinitionRegistrar registrar = (ImportBeanDefinitionRegistrar) ParserStrategyUtils.instantiateClass(
candidateClass, ImportBeanDefinitionRegistrar.class, this.environment, this.resourceLoader, this.registry);
// ImportBeanDefinitionRegistrar 인스턴스 생성
configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());
// 설정 클래스에 ImportBeanDefinitionRegistrar 추가
} else {
// 일반적인 설정 클래스인 경우
this.importStack.registerImport(
currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());
// 임포트 관계를 스택에 등록
this.processConfigurationClass(candidate.asConfigClass(configClass), exclusionFilter);
// 임포트된 설정 클래스를 재귀적으로 처리
}
}
} catch (BeanDefinitionStoreException var18) {
BeanDefinitionStoreException ex = var18;
throw ex;
// 예외 발생 시 그대로 던짐
} catch (Throwable var19) {
Throwable ex = var19;
throw new BeanDefinitionStoreException(
"Failed to process import candidates for configuration class ["
+ configClass.getMetadata().getClassName() + "]: " + ex.getMessage(), ex);
// 다른 예외 발생 시 BeanDefinitionStoreException으로 래핑하여 던짐
} finally {
this.importStack.pop();
// 스택에서 설정 클래스를 제거
}
}
}
}
}
프레임워크 코드를 톺아보다보면 배우는 양이 압도적으로 많다는게 체감된다. 강의나 책에서 배운 추상적인 개념들이 실제로 구현된 코드를 하나 하나 읽어보다보면, 궁금했던 세세한 사항들까지도 의문점이 해결되고 내용이 와닿기 때문이다. 코드가 더 좋아진 인간 컴퓨터
Annotation을 불러와서 해석하는 코드들은 어떻게 찾아야하는지 정말 많이 어려웠다. OncePerRequestFilter 같은거야 Command + B로 바로바로 들어가면 되는데, Annotation은 해석하는 코드와 별도로 분리되어 있다보니 찾기가 참 난해했기 때문이다. 요즘은 GPT한테 궁금한 Annotation과 이를 해석해서 사용하는 프레임워크 코드를 달라 그러면 정말 잘 찾아준다. 패키지 명까지 찾아줘서 너무 편하다. 월 3만원치 일해라 GPT
요즘 개발하다보면 개념만으로는 이해가 안 될 때가 참 많다. 이것저것 해보면서 생긴 경험들로도 안 될 때가 종종 있다. 어찌저찌 해결했다 하더라도 이걸 남한테 설명해줘야 하는데 정작 원리는 모를 때도 많고... 이렇게 답답할 때면 결국 코드를 까보는게 정도인 것 같다. 이러다보니 "코드를 직접 까보면서 이해한 내용을 남한테 어떻게 쉽게 설명해줄 수 있을까?"라는 새로운 고민거리도 하나 생겼다. 모두가 이렇게 코드를 직접 까보는 것도 아니기도 하고... Mermaid 다이어그램을 적극적으로 활용하고는 있는데 한계가 있는 것 같아서 고민이다ㅜ