인프런 커뮤니티 질문&답변

식빵님의 프로필 이미지
식빵

작성한 질문수

스프링 핵심 원리 - 기본편

옵션 처리

@SpringBootApplication과 따로 만든 @ComponentScan 의 동작에 대해서 궁금한 게 있습니다.

해결된 질문

작성

·

983

5

안녕하세요, 정말 재밌게 수강중인 직장인입니다.
다름이 아니라 궁금한 게 있어서 질문드립니다.

강의 [옵션처리] - 2:57 에서 관련된 @Test 에러를 고치기 위해서 수정하는 과정을 보여주셨습니다.

그런데 여기서 궁금한 게 생겼습니다.

@SpringBootApplication 도 내부적으로  @ComponentScan이 있고, 

@ComponentScan 이 붙은 또 다른 클래스(AutoAppConfig)가 있습니다.

이렇게  @ComponentScan  를 갖는 2개의 클래스가 존재하는 상태입니다.

여기서 질문입니다.

첫 번째 질문:
스프링 부트를 실행하면 @SpringBootApplication 에서 한번 스캔하고, AutoAppConfig 에서 한 번 더 스캔을 시도 하는 건가요?

두 번째 질문:
만약 첫 번째 질문의 대답이 YES면, 
처음 스캔을 시도해서 먼저 등록된 빈이 있으면,
두 번째 스캔 때는 처음 스캔을 통해서 등록된 빈은 무시하고 빈 등록을 시도하지 않는 건가요?

답변 7

9

식빵님의 프로필 이미지
식빵
질문자

안녕하세요 선생님. 말씀하신 대로 제가 스스로 납득 될때까지 테스트를 돌려봤습니다.

조금 시간이 걸렸지만, 궁금했던 모든 걸 알아내니 정말 속이 시원하네요 😁

역시 직접해보는게 좋네요!

.

일단 ConfigurationClassParser 의 doProcessConfigurationClass 메소드에서 다음 부분에 디버깅 포인트를 찍습니다.

위 그림에서 디버깅 포인트를 찍은 지점에만 집중해서 설명하겠습니다.

1. this.componentScanParser.parse 메소드는 일단 CoreApplication 의 빈 스캔으로 자기 자신을 제외한

스캔 후보(타입은 BeanDefinition)들을 뽑습니다. 해당 정보들은 모두 scannedBeanDefinitions 변수에 들어갑니다.

이 과정이 "스캔"입니다. 참고로 이 과정에서 DefaultListableBeanFactory 클래스의 beanDefinitionMap 멤버에

스캔된 BeanDefinition이 저장됩니다.

.

2. 구해진 모든 beanDefinition으로 다시 한번 for문을 돌리면서 parse(~)를 호출합니다. (2번째 디버깅 지점)

.

3. 만약 parse의 파라미터로 넣은 beanDefinition이 @ComponentScan 어노테이션을 갖고 있다고 판단되면

재귀적으로 다시 1번과 같은 과정을 거칩니다 . 이번에는 스캔의 주체가 AutoAppConfig 입니다.

결론은 CoreApplication과 AutoAppConfig 클래스 모두 스캔을 하는 것입니다.

.

4.  추가적으로 "1번에서 스캔해온 것을 3번에서도 스캔해오지 않을까? 이러면 에러가 나지 않을까?" 의문이 들었습니다.

그래서 this.componentScanParser.parse 메소드를 계속 따라가 보니 다음과 같은 if문들이 있습니다.

빨간 네모의 if만 집중하겠습니다.

isCompatible은 앞서 beanDefinitionMap 에 등록한 BeanDefinition과 현재 스캔해서 얻어낸 BeanDefinition이 서로 

"호환"이 되는지 확인합니다. 호환 여부 중에서 "같은 클래스"이면 호환이 된다고 판단하는 조건문이 있습니다.

그러니 1번에서 스캔된 클래스와 3번에서 스캔된 클래스는 "같은 클래스"이므로 호환이 됩니다.

결국 false를 리턴하게 됩니다.

false를 리턴하면 아래 그림(1번의 그림과 같음)의 scannedBeanDefinitions  변수에는 false 판정을 받은 BeanDefinition을 제외시켜 버립니다.

최종적으로 1번의 과정에서 이미 CoreApplication가 모두 스캔을 다하여 BeanDefinition을 생성했기 때문에,

AutoAppConfig에 의해 스캔된 대상들은 isCompatible에서 모두 false가 납니다.

요약하자면 AutoAppConfig의 스캔 자체가 무시되는 것입니다.

(좀 설명이 횡설수설합니다. 죄송합니다!)

안녕하세요 좋은 정리 해주셔서 정말 감사합니다. 혹시 위의 로직을 추적한 방법이있을까요?? 음 뭔가 질문이 이상한데 저같은경우 @SpringBootApplication 어노테이션에 들어가서 내부에 @ComponentScan 어노테이션이 있는것은 찾았습니다.  문제는 이런 Annotation 들은 전부 인터페이스라서 소스 내부에서 실제로 동작하는 부분을 어떻게 찾으셨는지가 궁금합니다.

SpringApplication.run(... 함수 내부를 따라가긴했는데 이 내부에서 찾아봐도 component scan 에 대한 직접적인 소스 히스토리는 없어서요.

혹시 디버깅 한 과정을 알수있을까요??

 

//실제 SpringApplication.run 안의 소스

	public ConfigurableApplicationContext run(String... args) {
		long startTime = System.nanoTime();
		DefaultBootstrapContext bootstrapContext = createBootstrapContext();
		ConfigurableApplicationContext context = null;
		configureHeadlessProperty();
		SpringApplicationRunListeners listeners = getRunListeners(args);
		listeners.starting(bootstrapContext, this.mainApplicationClass);
		try {
			ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
			ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
			configureIgnoreBeanInfo(environment);
			Banner printedBanner = printBanner(environment);
			context = createApplicationContext();
			context.setApplicationStartup(this.applicationStartup);
			prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
			refreshContext(context);
			afterRefresh(context, applicationArguments);
			Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime);
			if (this.logStartupInfo) {
				new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), timeTakenToStartup);
			}
			listeners.started(context, timeTakenToStartup);
			callRunners(context, applicationArguments);

....
식빵님의 프로필 이미지
식빵
질문자

너무 오랜 시간이 지나서 정확히 어떻게 했는지는 기억이 안나네요... 혹시라도 시간 생기면 찾아보겠습니다^^;

앗... 댓글다신게 1년이넘었군요 죄송합니다 ㅎㅎ..

3

김영한님의 프로필 이미지
김영한
지식공유자

devToroko님 잘 정리해주셔서 감사합니다.

다른분들께도 도움이 될듯요^^

2

김영한님의 프로필 이미지
김영한
지식공유자

devToroko님

자세히 남겨주셔서 좀 더 이해가 되었습니다^^

CoreApplicationTests 딱 1가지만 제외하고, 여기있는 테스트 들은 스프링 부트를 전혀 사용하지 않고, 순수하게 스프링 컨테이너 자체를 생생해서 실행하는 테스트 입니다.

CoreApplicationTests는 @SpringBootTest를 사용하기 때문에 스프링 부트를 내부에 띄워서 사용합니다.

그래서 @SpringBootApplication를 통한 컴포넌트 스캔도 수행됩니다.

그럼 설명해주신 내용을 가지고 답변을 드릴게요.

1. @SpringBootApplication  ( 컴포넌트 스캔 )  => @Component 어노테이션을 갖은 AutoAppConfig 발견, 등록

2. AutoAppConfig 가 빈으로 등록되고 내부에 작성되어 있는 @Bean 등록과 @ComponentScan을 수행

3. 이때 자동 등록과 수동 등록이 같은 빈을 등록하므로 스프링 부트의 에러가 나서 테스트 실패

1. @SpringBootApplication  ( 컴포넌트 스캔 )

- @Component 어노테이션을 갖는 MemoryMemberRepository 스프링 빈 등록(컴포넌트 스캔 등록)

- AutoAppConfig 발견 등록

- AutoAppConfig 내부에 있는 @Bean MemoryMemberRepository 스프링 빈 추가 등록(@Bean 수동 등록)

-> 이 지점에서 중복 등록으로 오류 발생

입니다. AutoAppConfig에 있는 @ComponentScan을 주석처리해도 같은 결과가 나오는 것을 확인하실 수 있을거에요.

감사합니다.

1

김영한님의 프로필 이미지
김영한
지식공유자

안녕하세요. devToroko님^^

Q: 이 상태에서 스프링 부트를 실행하면 실제 컴포넌트 스캔을 수행하는 클래스는

CoreApplication, AutoAppConfig 둘 다 인가요? 아니면 CoreApplication 만 스캔을 수행하나요? 

A: -> 제가 바로 답을 드릴 수도 있지만, 직접 코드로 한번 테스트 해보시면 더 많이 이해하실 수 있을거에요^^ 코드로 한번 테스트 해보시고 답변 남겨주세요^^

1

식빵님의 프로필 이미지
식빵
질문자

감사합니다 선생님, 제가 에러가 나는 원인을 조금 이상한 곳에서 찾고 있었던 거 같습니다 :)

.

선생님,  죄송하지만 조금만 더 질문 드리고 싶습니다.

.

( CoreApplicationTests 테스트는 해결이 된 상태라고 가정하겠습니다 )

.

현재 저희 코드에는 @ComponentScan이 붙은 클래스는 두 개가 있습니다.

이 상태에서 스프링 부트를 실행하면 실제 컴포넌트 스캔을 수행하는 클래스는

CoreApplication, AutoAppConfig 둘 다 인가요? 아니면 CoreApplication 만 스캔을 수행하나요? 

여러 @ComponentScan 클래스들 중에서 스프링 부트는 대체 어떤 기준으로

스캔을 수행하는 클래스를 결정하는 건가요?

0

식빵님의 프로필 이미지
식빵
질문자

선생님, 안녕하세요.
조금 제가 착각하는 부분이 있는듯 하여 질문을 조금 다르게 하겠습니다.

@SpringBootTest 어노테이션이 붙은 테스트는 @SpringBootApplication 을 통한 컴포넌트 스캔은 수행이 되지 않는 건가요?
(참고로 제가 질문 드리는 시점인  [옵션처리] - 2:57 에서는 @SpringBootTest 테스트가 실패합니다)

저는 @SpringBootTest 가 컨테이너 생성 후, @SpringBootApplication의 컴포넌트 스캔을 통해 찾은 빈들을 컨테이너에 등록하는 것으로 알고 있습니다. 
이로 인해서  [옵션처리] - 2:57 에서 에러가 났다고 생각했습니다.
그리고 그런 에러가 난 절차를 아래와 같이 추측했습니다.

1. @SpringBootApplication  ( 컴포넌트 스캔 )  => @Component 어노테이션을 갖은
AutoAppConfig 발견, 등록
2. AutoAppConfig 가 빈으로 등록되고 내부에 작성되어 있는 @Bean 등록과 @ComponentScan을 수행
3. 이때 자동 등록과 수동 등록이 같은 빈을 등록하므로 스프링 부트의 에러가 나서 테스트 실패

혹시 제가 위에서 착각한 부분 지적해주실 수 있을까요?

0

김영한님의 프로필 이미지
김영한
지식공유자

devToroko님! 정말 중요한 사실이 있습니다.

지금 스프링 부트를 전혀 사용하지 않는다는 점입니다. 따라서 @SpringBootApplication도 전혀 사용하지 않습니다!

스프링 컨테이너를 직접 띄워서 사용하고 있습니다! 그게 바로 ApplicationContext 입니다.

감사합니다.

식빵님의 프로필 이미지
식빵

작성한 질문수

질문하기