스프링 빈이 등록되는 과정(1)

BeanDefinition

Spring의 빈설정은 대표적으로 xml과 java config(annotation)으로 구성되어 있습니다.
하지만 이말은 사실은 BeanDefinition으로 추상화되어 있는 빈설정을 xml, java config로 표현하고 있다고 말할수 있습니다.

BeanDefinition은 다음과 같이 사용할 수 있습니다

BeanDefinition helloDef = new RootBeanDefinition(Sample.class);
helloDef.getPropertyValues().addPropertyValue("name","Spring");
applicationContext.registerBeanDefinition("hello2", helloDef);

저는 Spring Boot에서 Unit Test를 통해 코드를 돌려보려고 합니다
제가 작성한 코드는 다음과 같습니다

@RunWith(SpringRunner.class)
@SpringBootTest
public class DemoApplicationTest {

@Autowired
ApplicationContext applicationContext;

@Test
public void difinitionTest() {

BeanDefinition helloDef = new RootBeanDefinition(Sample.class);
helloDef.getPropertyValues().addPropertyValue("name","Spring");
applicationContext.registerBeanDefinition("hello2", helloDef);

System.out.println(applicationContext.getBean("hello2"));

}

}

이렇게 작성했을경우 컴파일 에러가 나게 됩니다

applicationContext.registerBeanDefinition("hello2", helloDef);

applicationContext는 registerBeanDefinition이라는 메소드가 없던 것입니다.

registerBeanDefinition메소드가 가능한 applicationContext는 제가 기본적으로 알고 있던 것이 StaticApplicationContext입니다.
StaticApplicationContext의 소스를 열어보면서 확인해보니 GenericApplicationContext라는 상위 클래스에 정의되어 있는 것을 알수 있습니다. 우리가 처음에 시도한 ApplicationContext의 경우 GenericApplicationContext의 상위 클래스입니다.
적어도 우리는 GenericApplicationContext를 상속받는 실제 구현체로 선언해야 하는거죠.. 물론 GenericApplicationContext로 타입을 주어도 상관 없지만 Spring Boot는 어떤 구현체를 사용하는 알고 싶어졌습니다.

@Test
public void applicationContexTypeTest() {

System.out.println("applicationContext " + applicationContext );
System.out.println("applicationContext " + applicationContext.getClass());

}
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v1.5.4.RELEASE)

2017-06-11 18:02:06.280 INFO 7204 --- [ main] com.ahea.spring.DemoApplicationTest : Starting DemoApplicationTest on Nohui-MacBook-Pro.local with PID 7204 (started by nohsunghyun in /Users/nohsunghyun/Downloads/BeanDefinition)
2017-06-11 18:02:06.281 INFO 7204 --- [ main] com.ahea.spring.DemoApplicationTest : No active profile set, falling back to default profiles: default
2017-06-11 18:02:06.301 INFO 7204 --- [ main] s.c.a.AnnotationConfigApplicationContext : Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@43bc63a3: startup date [Sun Jun 11 18:02:06 KST 2017]; root of context hierarchy
2017-06-11 18:02:06.621 INFO 7204 --- [ main] com.ahea.spring.DemoApplicationTest : Started DemoApplicationTest in 10.587 seconds (JVM running for 11.171)
applicationContext org.springframework.context.annotation.AnnotationConfigApplicationContext@43bc63a3: startup date [Sun Jun 11 18:02:06 KST 2017]; root of context hierarchy
applicationContext class org.springframework.context.annotation.AnnotationConfigApplicationContext
2017-06-11 18:02:06.655 INFO 7204 --- [ Thread-2] s.c.a.AnnotationConfigApplicationContext : Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@43bc63a3: startup date [Sun Jun 11 18:02:06 KST 2017]; root of context hierarchy

ApplicationContext객체의 클래스 타입을 찍어보니 AnnotationConfigApplicationContext인것을 알수 있습니다.

ApplicationContext타입을 AnnotationConfigApplicationContext로 변경후 테스트를 다음과 같이 진행하였습니다

@Autowired
AnnotationConfigApplicationContext applicationContext;

@Test
public void difinitionTest() {

BeanDefinition helloDef = new RootBeanDefinition(Sample.class);
helloDef.getPropertyValues().addPropertyValue("name","Spring");
applicationContext.registerBeanDefinition("hello2", helloDef);

System.out.println(applicationContext.getBean("hello2"));

}
결과
Sample{name='Spring'}

AnnotationConfigApplicationContext

@SpringBootApplication
public class DemoApplication {

public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}

이 코드는 SpringBoot Application을 시작하기 위한 코드입니다.
저는 spring 2.5부터 시작을 했었는데 기존에는 web.xml에 리스너를 통해 스프링 컨텍스트를 올렸죠. 스프링 부트를 모르시는 분들은 그 설정없이 위에 코드가 그 부분이라고 생각하셔도 좋겠네요.
SpringApplication을 들어가보면 쉽게 AnnotationConfigApplicationContext를 찾을수 있습니다

protected ConfigurableApplicationContext createApplicationContext() {
Class<?> contextClass = this.applicationContextClass;
if(contextClass == null) {
try {
contextClass = Class.forName(this.webEnvironment?"org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext":"org.springframework.context.annotation.AnnotationConfigApplicationContext");
} catch (ClassNotFoundException var3) {
throw new IllegalStateException("Unable create a default ApplicationContext, please specify an ApplicationContextClass", var3);
}
}

return (ConfigurableApplicationContext)BeanUtils.instantiate(contextClass);
}

중간 코드를 보면 contextClass 가 AnnotationConfigEmbeddedWebApplicationContext 또는 AnnotationConfigApplicationContext가 webEnviroment의 값에 따라 나뉘어지게 되는군요..
둘다 뭐가 어쨌든 어노테이션 기반 어플리케이션 컨텍스트인거 같습니다.

AnnotationConfigApplicationContext에 대해 조금 더 알아보겠습니다. AnnotationConfigApplicationContext의 코드를 열어보면 다음과 같은 프로퍼티가 있습니다

private final AnnotatedBeanDefinitionReader reader;
private final ClassPathBeanDefinitionScanner scanner;

해당 클래스에서 reader가 하는 것들을 모아봤습니다

this.reader.setEnvironment(environment);
this.reader.setBeanNameGenerator(beanNameGenerator);
this.reader.setScopeMetadataResolver(scopeMetadataResolver);
this.reader.register(annotatedClasses);
  • 환경설정인걸까요?
  • 빈 네임 제너레이터를 set해주는 것도 있습니다
  • 메타데이터 리졸버라는 것이 있네요?
  • 어노테이션 클래스를 register하고 있습니다

reader의 메소드 명은 AnnotatedBeanDefinitionReader인데요. 어노테이션으로 된 beanDefinition 리더 라고 이름이 지어져 있습니다. BeanDefinition은 이번 포스팅에 가장 처음 나온 이야기였는데요. 이름으로 추측하건데 어노테이션으로 된 BeanDefinition을 읽어오는 녀석인거 같습니다.

scanner는 어떤지 보겠습니다

this.scanner.setEnvironment(environment);
this.scanner.setBeanNameGenerator(beanNameGenerator);
this.scanner.setScopeMetadataResolver(scopeMetadataResolver);
this.scanner.clearCache();
this.scanner.scan(basePackages);

위에 3개는 reader와 같은 일을 하나 봅니다
새로운 것이 밑에 두개인데요

scanner.clearCache는 무엇인지 쫒아 들어가보면

private final Map<Resource, MetadataReader> metadataReaderCache = new LinkedHashMap<Resource, MetadataReader>(256, 0.75F, true) {
protected boolean removeEldestEntry(Entry<Resource, MetadataReader> eldest) {
return this.size() > CachingMetadataReaderFactory.this.getCacheLimit();
}
};

이런 코드가 있습니다. 때려 맞춰보건데 scanner는 메타데이터를 캐싱처리하며 들고 있고 이것을 클리어 할수 있도록 메소드를 제공해주고 있습니다.

scan은 파라미터로 넘기려는 변수명부터가 아주 친숙합니다. ComponentScan할때 basePackage를 주죠. ComponentScan이 이때 이녀석을 통해 일어나는지 확인해보고 싶겠죠???

public int scan(String... basePackages) {
int beanCountAtScanStart = this.registry.getBeanDefinitionCount();
this.doScan(basePackages);
if(this.includeAnnotationConfig) {
AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);
}

return this.registry.getBeanDefinitionCount() - beanCountAtScanStart;
}

우선 scan의 리턴 타입은 int인데요, 이 값으로 어떤 처리를 하려는지는 더 분석해봐야 겠습니다. 스캔을 호출하는 AnnotationConfigApplicationContext에서는 리턴값을 받아놓지 않아서요…

우리는 ComponentScan과의 관계, 즉 basePackages값에 있는 어노테이션을 빈으로 등록해주는가를 찾아가보도록 하겠습니다

세번째 줄에 doScan(basePackages)를 호출하는데 굉장히 의심스럽습니다.

타고 들어가보면 다음 소스인데요

protected Set doScan(String... basePackages) {
Assert.notEmpty(basePackages, "At least one base package must be specified");
Set beanDefinitions = new LinkedHashSet();
String[] var3 = basePackages;
int var4 = basePackages.length;

for(int var5 = 0; var5 < var4; ++var5) {
String basePackage = var3[var5];
Set candidates = this.findCandidateComponents(basePackage);
Iterator var8 = candidates.iterator();

while(var8.hasNext()) {
BeanDefinition candidate = (BeanDefinition)var8.next();
ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
candidate.setScope(scopeMetadata.getScopeName());
String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);
if(candidate instanceof AbstractBeanDefinition) {
this.postProcessBeanDefinition((AbstractBeanDefinition)candidate, beanName);
}

if(candidate instanceof AnnotatedBeanDefinition) {
AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition)candidate);
}

if(this.checkCandidate(beanName, candidate)) {
BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
beanDefinitions.add(definitionHolder);
this.registerBeanDefinition(definitionHolder, this.registry);
}
}
}

return beanDefinitions;
}

코드를 쭉 살펴보면 Set candidates = this.findCandidateComponents(basePackage)가 있는데요 리턴 타입이 BeanDefinition을 Set으로 주는걸 보니 이부분을 잘 봐야 겠습니다. findCandidateComponents(basePackage)를 호출하면 basePackage에 있는 빈을 주는건 아닐까요?

안으로 들어가보면

LinkedHashSet candidates = new LinkedHashSet();

.
.
MetadataReader metadataReader = this.metadataReaderFactory.getMetadataReader(resource);
if(resource.isReadable()) {
try {
MetadataReader metadataReader = this.metadataReaderFactory.getMetadataReader(resource);
if(this.isCandidateComponent(metadataReader)) {
ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
sbd.setResource(resource);
sbd.setSource(resource);
if(this.isCandidateComponent((AnnotatedBeanDefinition)sbd)) {
if(debugEnabled) {
this.logger.debug("Identified candidate component class: " + resource);
}

candidates.add(sbd);
} else if(debugEnabled) {
this.logger.debug("Ignored because not a concrete top-level class: " + resource);
}
} else if(traceEnabled) {
this.logger.trace("Ignored because not matching any filter: " + resource);
}
} catch (Throwable var13) {
throw new BeanDefinitionStoreException("Failed to read candidate component class: " + resource, var13);
}
} else if(traceEnabled) {
this.logger.trace("Ignored because not readable: " + resource);
}

해당 코드에서는 this.isCandidateComponent(metadataReader) 조건에 따라 ScannedGenericBeanDefinition는 생성하여 Set에 담아주는것을 볼수 있습니다

코드를 쫒아와서 이부분인가? 싶은곳을 이렇게 발견했지만 실제 여기 맞는지 검증을 해보려고 합니다

우선 Application에 ComponentScan을 추가하고 패키지를 줬습니다

@SpringBootApplication
@ComponentScan(basePackages = "com.ahea.spring")
public class DemoApplication {

public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}

그리고 패키지 안에 HomeController라고 하나 만들어놓고 @Controller어노테이션을 줘봤습니다

@Controller
public class HomeContoller {
}

그리고 마지막으로 findCandidateComponents에 중단점을 찍은 후 디버그 모드로 돌려봤습니다.
결과는 basePackage라는 파라미터에는 ComponentScan에 넣어준 basePackages값이 있었으며
img2
리턴값인 candidates 에는 HomeController가 정의된 BeanDefinition이 들어 있는것을 확인할수 있었습니다.
img1
다시 돌아와서 doScan에서는 위에 내용과 같이 ComponentScan하여 나온 빈정보들을 가지고 registerBeanDefinition를 통해 ApplicationContext에 등록하게 됩니다.

if(this.checkCandidate(beanName, candidate)) {
BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
beanDefinitions.add(definitionHolder);
this.registerBeanDefinition(definitionHolder, this.registry);
}

하나 흥미로운 점은 Bean 객체나 BeanDefinition을 등록하는것이 아니라 BeanDefinitionHolder에 한번 감싸서 등록하는데요. AnnotationConfigUtils.applyScopedProxyMode를 통해 프록시모드 처리를 진행하는 코드를 확인할 수 있었습니다. 프록시 설정에 따라 스프링에서 프록시 객체를 만드는 법을 확인해 볼수 있는데요. 추후에 이부분도 진행하겠습니다

답글 남기기

아래 항목을 채우거나 오른쪽 아이콘 중 하나를 클릭하여 로그 인 하세요:

WordPress.com 로고

WordPress.com의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Google photo

Google의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Twitter 사진

Twitter의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Facebook 사진

Facebook의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

%s에 연결하는 중

WordPress.com 제공.

위로 ↑

%d 블로거가 이것을 좋아합니다: