new A(new B(new C()))
위와 같이 A, B, C가 의존관계에 있고
셋 모두 스프링 빈에 등록되어야 할 대상일 때,
스프링 컨테이너에서는 어떻게 인스턴스를 만들까요?
실제 구현 내용을 모른다면,
직접 구현한다면 어떻게 구현하실지 말씀해주시겠어요?
위 질문은 얼마 전 진행됐던 레벨 인터뷰에서 브라운이 인터뷰이에게 했던 질문입니다.
옵저버로서 해당 인터뷰를 관찰하면서도, 뜻밖에 질문에 저 또한 속으로 대답을 떠올려보고 있었습니다.
제 속에 떠올랐던 대답은 "현재 생성할 수 있는 인스턴스를 생성하는 사이클을 반복한다" 였습니다.
아래처럼요.
1) A는 B가 아직 생성 안되었기에 넘어가고, B도 마찬가지입니다. 그러면 C를 생성하고 mark 합니다.
2) A는 여전히 B가 없으니 넘어가고, B는 이제 C가 있어서 생성 가능하니 B를 생성하고 mark 합니다.
3) A를 생성하고 mark하고 반복이 종료됩니다.
당시 인터뷰이였던 크루는 프록시와 지연 생성이라는 키워드를 떠올려냈습니다.
대답을 듣자마자 크게 놀랐습니다. 배웠던 개념인데도 저는 미처 연결할 생각을 하지 못했는데,
긴장되는 인터뷰 순간에 그러한 생각을 해냈다니.. 정말 반짝이는 순간이었습니다.
같이 면접봤다면 난 떨어졌구나 싶었고요..ㅋㅋㅋ
아무튼 그래서 살짝만 찍먹을 해본 결과를 기록해봅니다.
call stack on new A(new B(new C()))
A가 B를, B가 C를 의존하는 상황입니다.
그리고 셋 모두 @Component 애너테이션을 적용해서
Spring Bean으로 등록되도록 설정하였습니다.
이 상황에서 C의 생성자에 브레이크 포인트를 걸고, 스프링을 기동했습니다.
그리고 아래와 같은 call stack을 확인할 수 있었습니다.
이 아래로 작성될 포스팅 내용은 모두 이 call stack을 토대로 추정한 내용입니다
<init>:8, C (com.example.beantest)
newInstance0:-1, NativeConstructorAccessorImpl (jdk.internal.reflect)
newInstance:62, NativeConstructorAccessorImpl (jdk.internal.reflect)
newInstance:45, DelegatingConstructorAccessorImpl (jdk.internal.reflect)
newInstance:490, Constructor (java.lang.reflect)
instantiateClass:211, BeanUtils (org.springframework.beans)
instantiate:87, SimpleInstantiationStrategy (org.springframework.beans.factory.support)
instantiateBean:1326, AbstractAutowireCapableBeanFactory (org.springframework.beans.factory.support)
createBeanInstance:1232, AbstractAutowireCapableBeanFactory (org.springframework.beans.factory.support)
doCreateBean:582, AbstractAutowireCapableBeanFactory (org.springframework.beans.factory.support)
createBean:542, AbstractAutowireCapableBeanFactory (org.springframework.beans.factory.support)
lambda$doGetBean$0:335, AbstractBeanFactory (org.springframework.beans.factory.support)
getObject:-1, 436094532 (org.springframework.beans.factory.support.AbstractBeanFactory$$Lambda$320)
getSingleton:234, DefaultSingletonBeanRegistry (org.springframework.beans.factory.support)
doGetBean:333, AbstractBeanFactory (org.springframework.beans.factory.support)
getBean:208, AbstractBeanFactory (org.springframework.beans.factory.support)
resolveCandidate:276, DependencyDescriptor (org.springframework.beans.factory.config)
doResolveDependency:1391, DefaultListableBeanFactory (org.springframework.beans.factory.support)
resolveDependency:1311, DefaultListableBeanFactory (org.springframework.beans.factory.support)
resolveAutowiredArgument:887, ConstructorResolver (org.springframework.beans.factory.support)
createArgumentArray:791, ConstructorResolver (org.springframework.beans.factory.support)
autowireConstructor:229, ConstructorResolver (org.springframework.beans.factory.support)
autowireConstructor:1372, AbstractAutowireCapableBeanFactory (org.springframework.beans.factory.support)
createBeanInstance:1222, AbstractAutowireCapableBeanFactory (org.springframework.beans.factory.support)
doCreateBean:582, AbstractAutowireCapableBeanFactory (org.springframework.beans.factory.support)
createBean:542, AbstractAutowireCapableBeanFactory (org.springframework.beans.factory.support)
lambda$doGetBean$0:335, AbstractBeanFactory (org.springframework.beans.factory.support)
getObject:-1, 436094532 (org.springframework.beans.factory.support.AbstractBeanFactory$$Lambda$320)
getSingleton:234, DefaultSingletonBeanRegistry (org.springframework.beans.factory.support)
doGetBean:333, AbstractBeanFactory (org.springframework.beans.factory.support)
getBean:208, AbstractBeanFactory (org.springframework.beans.factory.support)
resolveCandidate:276, DependencyDescriptor (org.springframework.beans.factory.config)
doResolveDependency:1391, DefaultListableBeanFactory (org.springframework.beans.factory.support)
resolveDependency:1311, DefaultListableBeanFactory (org.springframework.beans.factory.support)
resolveAutowiredArgument:887, ConstructorResolver (org.springframework.beans.factory.support)
createArgumentArray:791, ConstructorResolver (org.springframework.beans.factory.support)
autowireConstructor:229, ConstructorResolver (org.springframework.beans.factory.support)
autowireConstructor:1372, AbstractAutowireCapableBeanFactory (org.springframework.beans.factory.support)
createBeanInstance:1222, AbstractAutowireCapableBeanFactory (org.springframework.beans.factory.support)
doCreateBean:582, AbstractAutowireCapableBeanFactory (org.springframework.beans.factory.support)
createBean:542, AbstractAutowireCapableBeanFactory (org.springframework.beans.factory.support)
lambda$doGetBean$0:335, AbstractBeanFactory (org.springframework.beans.factory.support)
getObject:-1, 436094532 (org.springframework.beans.factory.support.AbstractBeanFactory$$Lambda$320)
getSingleton:234, DefaultSingletonBeanRegistry (org.springframework.beans.factory.support)
doGetBean:333, AbstractBeanFactory (org.springframework.beans.factory.support)
getBean:208, AbstractBeanFactory (org.springframework.beans.factory.support)
preInstantiateSingletons:955, DefaultListableBeanFactory (org.springframework.beans.factory.support)
finishBeanFactoryInitialization:918, AbstractApplicationContext (org.springframework.context.support)
refresh:583, AbstractApplicationContext (org.springframework.context.support)
refresh:147, ServletWebServerApplicationContext (org.springframework.boot.web.servlet.context)
refresh:734, SpringApplication (org.springframework.boot)
refreshContext:408, SpringApplication (org.springframework.boot)
run:308, SpringApplication (org.springframework.boot)
run:1306, SpringApplication (org.springframework.boot)
run:1295, SpringApplication (org.springframework.boot)
main:10, BeantestApplication (com.example.beantest)
C -> B -> A 순으로 생성자가 호출된다
프록시 객체로 생성한다면, 생성되는 순서와 상관없이 생성할 수 있을 것 같았습니다.
그런데 결과를 보면 쉽게 상상 가능한 순서대로 생성자가 호출되고 있었습니다.
DefaultListableBeanFactory 의 preInstantiateSingletons 메서드를 살펴보면
생성해야할 bean 들의 이름을 리스트로 순회하며 bean들을 초기화하는데요,
분명 a가 먼저 생성 대상임에도 c 가 먼저 생성됨을 알 수 있습니다.
또한, A의 생성자에 매개변수로 전달된 B객체
B객체가 필드로 가지고 있는 C객체들이 프록시 객체가 아닌 진짜 해당 타입의 인스턴스이었기에
프록시를 사용하지 않고 생성했음을 알 수 있었습니다.
클래스에 final 을 선언해도 Spring Bean으로 등록이 된다?!
A, B, C의 클래스 정의에 final을 선언해도 문제 없이 생성되었습니다.
이전까지는 Spring Bean으로 등록할 class의 정의에는 final 키워드를 사용할 수 없는 것으로 알고 있었습니다.
@Transactional 이 선언된 클래스에는 final keyword 선언 시 컴파일 에러가 납니다.
그러나 해당 애너테이션이 없는 @Controller나, @Component 선언된 클래스에는 final 선언이 가능했습니다.
이는 프록시 객체를 생성하지 않고 해당 객체 타입으로 인스턴스를 바로 만들기 때문으로 유추합니다.
어떨 때 프록시 객체를 생성할까?
AOP 적용을 위해 바이트 코드를 조작해야 하는 경우 프록시 객체를 만든다. 라는 것이 현재 유추의 결론입니다.
위의 애니메이션을 보시면 Repository 인터페이스를 의존하는 RichService는
JdkDynamicAopProxy 타입의 h라는 필드를 가지고 있고, 이 필드에는 타겟소스가 있습니다.
이 내부가 실제 구현부에 해당합니다.
아래의 이미지를 보시면 RichService를 의존하는 RichController는
RichService 타입으로 캐스팅된 SpringCGLIB 타입을 받습니다.
RichController의 생성자 매개변수로 전달된 richService에는 memberRepository 필드가 null로 할당되어 있습니다.
대신 CGLIBCALLBACK_0 필드 내부에 타겟 소스가 있는데, 이 안에 있는 richService에는
memberRepository 필드가 존재합니다.
즉 AOP적용을 위해 바이트 코드 조작이 적용되는 경우,
메서드를 직접 수행하는 것이 아니라 타겟 소스를 감싼 프록시 객체의 메서드를 통해 수행하기 때문에
memberRepository 필드가 null로 할당된 거라 생각됩니다.
invoke 영역이 그래서 존재했군요!
간혹 서비스 레이어에서 디버깅을 수행하다가 invoke 라는 메서드를 보게되는 경우가 있었습니다.
뭔가 미궁에 잘못 빠진 것처럼 느껴져서 황급히 빠져나와 다음 라인으로 이동하곤 했는데요,
이렇게 프록시 객체가 생성되고 타겟 소스가 호출되는 과정이 눈에 들어오고 나니,
왜 invoke 라는 영역으로 가게 되었고, 그게 어디인지 이해가 되었습니다.
AOP 적용을 위해 프록시에서 타겟 소스를 호출한 것이
제가 디버깅하던 서비스 레이어였던 것이고,
서비스 레이어의 수행이 끝난 뒤엔 해당 타겟 소스를 호출했던 프록시 객체로 돌아가졌던 것입니다.
C -> B -> A 순서는 어떻게 처리된 걸까
call stack을 통해 분석한 내용은 다음과 같습니다.
1. 패키지 전체를 탐색해서 스프링 빈으로 등록할 클래스들을 탐색한다.
2. 이때, IDE에서 볼 때 위에서 아래로 알파뱃 순으로 정렬되듯 이 순서대로 List<String> 에 담긴다
3. 해당 리스트를 순회하며 스프링 빈을 생성한다.
4. 생성할 때, 필요한 오토와이어 대상이 있을 경우, 이를 BeanFactory로부터 얻어온다.
5. BeanFactory는 요청받은 스프링 빈이 있으면 반환, 없으면 생성한다.
6. 따라서 A 생성 시도시 B가 오토와이어 대상이어서 B를 생성하고자 시도하게 되고,
다시 B 생성 시도 시 C가 오토와이어 대상이어서 C를 생성시도하게 된다.
7. 그래서 C 생성자에서 브레이크 포인트가 가장 먼저 걸리긴 하지만,
이는 A를 생성하는 과정에서 필요하게 되어 생성한 것이고, 이는 call stack에서 살펴볼 수 있다.
프록시가 적용되건 아니건 상관 없이,
깊이 우선 탐색을 수행하는 것처럼 bean이 생성됨을 알 수 있었습니다.
가령 A가 먼저 만들 대상이라면, A를 만들기 위해 필요한 B와 C를 만들어서 A를 처리하게 되고,
그 뒤 B와 C는 이미 만들어져있기 때문에 즉시 다음 인덱스로 넘어가게 됩니다.
요약
- 바이트 코드 조작이 일어나는 클래스는 final 키워드 선언이 불가하다
- AOP 등의 적용으로 인해 바이트 코드 조작이 없는 컴포넌트는 final 키워드 선언이 가능하다
- 바이트 코드 조작이 일어나는 클래스는 프록시 타입으로 생성되어 스프링 빈으로 등록된다
- beanName 들을 순회하며 스프링 빈으로 등록하되, 생성 시에 필요한 오토와이어 대상을
BeanFactory로부터 getBean으로 얻어온다 - getBean 시점에 아직 해당 스프링 빈이 존재하지 않는다면 생성해서 반환한다.
- 따라서 A를 먼저 생성 시도하게 되면 매개변수인 B를 생성 시도하게 되고,
이때 다시 B를 만들기 위해 C를 생성하게 된다. - C가 만들어지면 B가 만들어지고, 이 B를 이용해 A를 만드는 작업이 완료된다.
- 이후 B와 C 생성 순서에는 이미 생성되었다고 마킹되어있기에 다음 순서로 넘어간다.
제대로 이해(?) 분석(?) 한 것인지 확신이 매우 떨어지나,
우선 시도해본 내용을 기록해봅니다.
추후 잘못된 내용 혹은 보충해야할 내용이 있을 경우 추가해두겠습니다.
잘못 소개된 내용이 있다면 공유부탁드립니다. 감사합니다.
'Java & Spring' 카테고리의 다른 글
jitpack, github를 이용한 라이브러리 배포하기 (0) | 2022.08.27 |
---|---|
📦 DTO는 택배상자 (Bean Validation 검증은 누가 하나?) (3) | 2022.07.25 |
🌱 Spring에 Handler가 등록되는 과정 (0) | 2022.06.21 |
🖋 Servlet부터 DispatcherServlet까지 (Front Controller 패턴) (0) | 2022.06.20 |
🤔 객체지향 생활체조 돌아보기 (우테코 레벨2를 마치며) (0) | 2022.06.17 |