Spring Doc - Core -IOC Container
Spring framework Document 읽기
- 현재 담당하는 서비스가 Spring5를 사용하진 않지만 5의 문서를 읽으면서 추가된 내용이 아니라 Spring framework 자체의 내용에 집중해볼 예정입니다.
Docs : https://docs.spring.io/spring/docs/5.2.3.RELEASE/spring-framework-reference/core.html#spring-core
Repo : https://github.com/spring-projects/spring-framework
Github repository에서 "T"를 누르면 파일 검색이 가능합니다.
The IoC Container
스프링에서 IOC container를 어떻게 구현했는지 알아봅니다.
Bean과 IoC Container
org.springframework.beans
와org.springframework.context
패키지가 Spring IoC container의 근간이 되는 패키지 입니다.Spring IOC Container에서 관리되는 모든 객체를 Bean이라고 합니다.
모든 타입의 객체를 관리하는 최상위 인터페이스로
BeanFactory
가 존재하고,
BeanFactory의 sub-interface인ApplicationContext
가 존재합니다.대부분의 책에서 ApplicationContext == BeanFactory로 설명하는데, ApplicationContext가 BeanFactory를 가지고 있는 형태에 가깝습니다.
The ApplicationContext is a complete superset of the BeanFactory
ApplicationContext
는 Spring IoC Container 그자체를 의미하고 annotation, xml, java code형식의 metadata를 읽어서 bean의 인스턴스화를 담당합니다.AspectJ를 사용하여 IoC Container 외부에서 객체를 설정하는 방식이나
ApplicationContext를 통해 BeanFactory를 꺼내서 직접 등록해주는 방식도 존재 합니다.
Bean
Overview
bean은
BeanDefinition
인터페이스로 표현되고 "패키지를 포함한 클래스 이름", "의존관계" "scope" 와 같은 bean 생성에 필요한 metadata를 포함합니다.- 일반적인
BeanFactory
구현체에서BeanDefinition
과 생성된 singleton bean은 Map의 형태로 저장되어 있습니다.
- 일반적인
bean은 유니크한 identifier 값을 가지게 되는데 id나 name값을 직접 지정해주지 않는 경우 class명을 기반으로 이름을 생성해줍니다.
java.beans.Introspector.decapitalize
를 사용- e.g. "FooBah" becomes "fooBah" and "X" becomes "x", but "URL" stays
- inner class의 경우엔
$
통해서 구분하게 됩니다- e.g. com.example.SomeThing$OtherThing
컨테이너가 bean을 생성할때는
BeanDefinition
을 바탕으로 생성하게 됩니다.일반적으로 reflection을 통해서 생성자나 factory메서드를 호출하여 빈을 생성합니다.
Factory 메서드로 bean을 생성하는 방법
XML<!-- static factory method --> <bean id="clientService" class="examples.ClientService" factory-method="createInstance"/> <!-- factory method from "factory bean" --> <bean id="serviceLocator" class="examples.DefaultServiceLocator"/> <bean id="accountService" factory-bean="serviceLocator" factory-method="createAccountServiceInstance"/>
Annotation
@Configuration public class SomeThingConfig { //SomeThingHelper에 대한 factory메서드 @Bean public SomeThingHelper someThingHelper() { return new SomeThingHelper(); } }
Bean scope
- web-aware Spring ApplicationContext란
GenericWebApplicationContext
와 같은 웹 환경의ApplicationContext
구현체를 의미합니다. Scope
인터페이스를 사용하여 Custom 스코프를 정의할 수 있습니다SimpleThreadScope
란 스코프도 존재하지만 default로 포함되지 않음으로 사용하기 위해선 custom 스코프와 같이 별도의 등록이 필요합니다.Scope threadScope = new SimpleThreadScope(); beanFactory.registerScope("thread", threadScope);
- Singletone scope의 경우 컨테이너 단위의 singletone임으로 classLoader 단위의 singletone을 의미하는 GoF의 singletone 패턴과는 컨셉이 다릅니다.
applicationContext
가 여러개라면 각 컨테이너 마다 하나의 인스턴스를 갖게 됩니다.- Bean scope singletone은 하나의 bean에 대해 하나의 인스턴스임으로 bean 단위의 싱글톤이라고 할 수도 있습니다.
Bean customizing
InitializingBean
,DisposableBean
과 같은 인터페이스를 사용하여 bean의 lifecyle을 커스터마이징 할 수 있지만 스프링과의 decoupling을 위해서@PostConstruct
,@PreDestory
와 같은 java spec의 annotation사용이 권장됩니다.Aware
인터페이스Programmatic하게 스프링 컨테이너에 포함된 정보(
ApplicationContext
나 bean 인스턴스등)에 접근할때 사용 가능한 인터페이스 입니다.
해당 인터페이스를 구현하고 포함된 함수를 override 하면 컨테이너에서 적합한 값을 주입해줍니다.Autowiring으로도 동일한 효과를 얻을 수 있습니다.
BeanNameAware
예시public class BeanNameAwareTestClass implements BeanNameAware{ private String beanName; @Override public void setBeanName(String beanName) { this.beanName = beanName; } }
Aware 인터페이스 종류 : https://docs.spring.io/spring/docs/5.2.3.RELEASE/spring-framework-reference/core.html#aware-list
AbstractAutowireCapableBeanFactory.invokeAwareMethods
를 보시면 어떤식으로 구현되었는지 알 수 있습니다.private void invokeAwareMethods(final String beanName, final Object bean) { if (bean instanceof Aware) { if (bean instanceof BeanNameAware) { ((BeanNameAware) bean).setBeanName(beanName); } if (bean instanceof BeanClassLoaderAware) { ((BeanClassLoaderAware) bean).setBeanClassLoader(getBeanClassLoader()); } if (bean instanceof BeanFactoryAware) { ((BeanFactoryAware) bean).setBeanFactory(AbstractAutowireCapableBeanFactory.this); } } }
빈 초기화 과정
- annotation 기반의 설정을 사용한 spring boot 1.5.1 버전의 예시로 xml 방식 또는 다른 버전에서는 구현체가 다를 수 있습니다.
DefaultListableBeanFactory
의 상속 구조
Container
Dependency Injection
- Spring에서 DI는 컨테이너가 bean 인스턴스를 생성한 후에 의존관계를 주입해주는 방식으로 동작합니다.
- DI를 사용함으로써 얻는 장점으로는 객체간 결합도를 낮출 수 있고, 코드가 깔끔해지며, mock으로 대체하기 쉽기때문에 test에도 유리합니다.
- Constructor-based 방식과 Setter-based 방식이 존재합니다.
- Spring 팀에서는 생성자 주입방식을 추천합니다.
- argument를 통한 programmatic validation 가능
- 컴포넌트를 불변객체로 구현가능
- Client에게 완벽히 초기화된 상태로 리턴가능
- 의존관계가 너무 늘어난 경우 발생하는 bad code smell로 인해 적절한 리팩토링이 필요함을 느낄 수 있음
- Setter-based 방식은 할당할 default 값이 있는 경우에만 고려될 수 있으며, 사용하는곳에서 반드시 null check가 필요합니다.
- 장점이라면 re-injection을 할 수있다는 점이 있고, Circular dependency 문제가 발생했을때의 해결책으로 사용할 수 있습니다.
- Autowired를 통해 injection 하는 부분 코드 (
InjectionMetadata.java
)protected void inject(Object target, String requestingBeanName, PropertyValues pvs) throws Throwable { if (this.isField) { Field field = (Field) this.member; ReflectionUtils.makeAccessible(field); field.set(target, getResourceToInject(target, requestingBeanName)); } else { if (checkPropertySkipping(pvs)) { return; } try { Method method = (Method) this.member; ReflectionUtils.makeAccessible(method); method.invoke(target, getResourceToInject(target, requestingBeanName)); } catch (InvocationTargetException ex) { throw ex.getTargetException(); } } }
- Constructure-based 방식로 주입하는 코드도 찾아보면 좋을것 같습니다
- Spring 팀에서는 생성자 주입방식을 추천합니다.
Dependency Resolution Process
ApplicationContext
는 모든 bean의 metadata를 가지고 초기화 됩니다.Bean의 의존성은 생성자 argument, properties 등으로 표현되고, bean이 실제로 생성되는 시점에 세팅됩니다.
<bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <!-- results in a setDriverClassName(String) call --> <property name="driverClassName" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/mydb"/> <property name="username" value="root"/> <property name="password" value="masterkaoli"/> </bean>
Default인 singletone-scoped & pre-instaniated로 설정된 bean은 컨테이너가 생성될때 함께 생성되고(eager-loading), 그렇지 않은 bean은 요청이 들어온 시점에 생성됩니다.
Default 설정이 아닌 bean은 사용 시점에 생성됨으로 runtime exception을 유발할 수 있습니다.
Bean이 생성되면 생성된 bean간의 의존관계를 나타내는 그래프(Dependency tree)가 작성되게 됩니다.
Autowiring은 컨테이너가 가지고있는 bean을 확인해서 적합한 타입의 의존성을 자동으로 주입해주는 방식입니다.
Method injection방식은 bean간의 lifecyle 차이에서 오는 문제를 해결 할 수 있습니다
Singletone bean A가 prototype bean B를 가지고 있는경우
applicationContext가 생성되는 시점에 A가 생성되고, 이 singletone bean을 만들기 위해 prototype bean인 B를 생성하게 됩니다.
이때 의도 대로라면 A에서 B를 사용하기위해 호출할때마다 B가 새로 생성되어야 하지만, 실제로는 이미 생성된 B를 가지고 계속 사용하게 됩니다.ApplicationContext에서 직접 새로운 prototype bean을 꺼내주는 방식으로 해결 가능합니다.
import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; public class A implements ApplicationContextAware { private ApplicationContext applicationContext; public Object process(Map state) { // get New B B b = createB(); b.setState(state); return b.execute(); } protected B createB() { return this.applicationContext.getBean("b", B.class); } public void setApplicationContext( ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } }
Container Initiation Process
Annotation 기반의 설정을 사용한 spring boot 1.5.1 버전의 예시로 xml 방식 또는 다른 버전에서는 구현체가 다를 수 있습니다.
refreshContext()
는((AbstractApplicationContext) applicationContext).refresh();
를 호출하게 되는데 사실상 이 함수가applicationContext
를 완성합니다.@Override public void refresh() throws BeansException, IllegalStateException { synchronized (this.startupShutdownMonitor) { // Prepare this context for refreshing. prepareRefresh(); // Tell the subclass to refresh the internal bean factory. ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory(); // Prepare the bean factory for use in this context. prepareBeanFactory(beanFactory); try { postProcessBeanFactory(beanFactory); // 사용자의 BeanDefinition이 모두 등록되었지만 아직 bean이 생성되진 않음 invokeBeanFactoryPostProcessors(beanFactory); // BeanPostProcessor를 등록, BeanPostProcessor들의 bean이 생성 registerBeanPostProcessors(beanFactory); initMessageSource(); initApplicationEventMulticaster(); // Initialize other special beans in specific context subclasses. // BeanDefinition을 가지고 bean 생성 onRefresh(); registerListeners(); // onRefresh단계에서 생성하지 않은 나머지 non-lazy-init한 singleton bean들을 생성 finishBeanFactoryInitialization(beanFactory); // Last step: publish corresponding event. finishRefresh(); } catch (BeansException ex) { if (logger.isWarnEnabled()) { logger.warn("Exception encountered during context initialization - cancelling refresh attempt: " + ex); } // Destroy already created singletons to avoid dangling resources. destroyBeans(); // Reset 'active' flag. cancelRefresh(ex); // Propagate exception to caller. throw ex; } finally { // Reset common introspection caches in Spring's core, since we // might not ever need metadata for singleton beans anymore... resetCommonCaches(); } } }
Container Extension Points
BeanPostProcessor
컨테이너/빈 생성후에 실행할 커스텀한 로직을 구현할 수 있습니다.
Bean 생성에 대한 로직을 수정하고싶다면
BeanFactoryPostProcessor
를 사용해야 합니다.BeanPostProcessor
는 Bean 인스턴스에 대한 call-back을 완전히 무시하도록 구현할 수도 있지만 이런식으로 구현하면 proxy로 동작하는 Spring의 feature들이 동작하지 않습니다.Spring-AOP는 proxy 동작을 위해 post-processor에서 weaving을 수행합니다.
BeanPostProcessor
는ApplicationContext
의 생성에 직접 관여하게 됨으로 잘못 커스터마이징 하는경우 동작에 치명적일 수 있습니다.구현 예
@Component public class TestBeanPostProcessor implements BeanPostProcessor { @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { System.out.println(beanName); return bean; } @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { System.out.println(beanName); return bean; } }
BeanFactoryPostProcessor
BeanPostProcessor
와 비슷하지만 생성된 bean을 조작할지, bean이 생성을 조작할지의 차이가 있습니다.
FactoryBean
FactoryBean
인터페이스를 구현해서 bean 생성 팩토리를 추가할 수 있습니다.Srping의 근간이 되는 Proxy Object를
ProxyFactoryBean
을 통해서 생성합니다.FactoryBean 인스턴스를 호출하고 싶다면
&
과 함께 요청해야 합니다, 그렇지 않다면 factoryBean의getObject()
의 결과값을 리턴하게 됩니다.ApplicationContext.get("abcFactoryBean") //abcFactoryBean.getObject() ApplicationContext.get("&abcFactoryBean") //abcFactoryBean
Classpath Scanning and Managed Components
@Component
@Component
어노테이션은 대상을 bean으로 만들어주는 어노테이션 입니다.@Component
는 기본적으로@Controller
,@Service
,@Repository
어노테이션과 동일하지만,
위치에 맞는 어노테이션을 사용하는것이 권장됩니다.Pointcut의 대상으로 잡을 수 있고, Spring이 발전해 나가며 추가적인 기능에 대한 마커역할이 생길 수 있습니다.
@Repository
의 경우 이미 persistence layer에 대한 자동 예외 번역(Exception translation)기능의 마커로써 사용됩니다.
@ComponentScan
- Configuration 클래스 (
@Configuration
)에@ComponentScan
어노테이션을 사용함으로써 bean으로 등록할 대상을 찾아낼 수 있습니다.- attribute로 basePackages를 지정함으로써 스캔의 시작 위치를 지정할 수 있습니다.
- attribute로 includeFilters / excludeFilters를 지정함으로써 스캔에 포함/제외시킬 대상을 커스터마이징이 가능합니.
- 찾아낸 클래스들은 그에 맞는 형태의
BeanDefinition
인스턴스로ApplicationContext
에 등록 됩니다.@ComponentScan이 Bean을 등록하는게 아니라 BeanDefinition 인스턴스를 등록하고 ApplicationContext가 로딩되는 과정에서 Bean이 등록됩니다.
- JDK9부터 사용되는 module에서도 정상적으로 적용되지만
module-info
에 Component 클래스들이 정상적으로 export되었는지 확인이 필요합니다.
- Configuration 클래스 (
Spring annotation을 대신해서 Java annotation을 사용하는 방법도 존재합니다.
Lite모드 bean
@Bean
메서드가@Configuration
이 붙지 않은 클래스(POJO나@Component
클래스 등)에 존재할때는 lite 모드로 동작하게 됩니다.Lite 모드인 bean은 proxying 되지 못함으로 bean간의 dependency가 적용되지 않습니다. 반대로 full 모드인 경우엔 컨테이너로 리다이렉션되어 호출됨으로 bean 간의 dependency가 적용 가능합니다.
Lite 모드에서 bean간의 호출시 IDE에서 경고를 띄우며, 호출이 proxy 되지 못하기 때문에 매번 새로운 응답을 받아오게 됩니다.
@Configuration public class FullModeTestConfig { @Bean public String test(){ test2(); System.out.println("test"); test2(); return "test"; } @Bean public String test2(){ System.out.println("test2"); return "test2"; } } public class liteModeTestConfig { @Bean public String test3(){ test4(); System.out.println("test3"); test4(); return "test3"; } @Bean public String test4(){ System.out.println("test4"); return "test4"; } }
- 결과 값
test2 //test 팩토리 메서드에서 test2가 호출됨에 따라 test2 팩토리 우선 호출 test //test 팩토리 메서드의 결과를 bean에 등록하기 위해 실행하며 test2를 2번 호출하지만 proxy되기 때문에 생성된 인스턴스만 반환 test4 //test3 메서드에서 test4를 호출하여 실행 test3 //test3 팩토리 메서드의 결과를 bean에 등록하기 위해 실행 test4 //test3 메서드에서 test4를 호출하여 실행 test4 //test4 팩토리 메서드의 결과를 bean에 등록하기 위해 실행
@Configuration
클래스는 모두CGLIB
(spring-core에 포함org.springframework.cglib
)를 사용하여 생성되기때문에 proxy가 가능합니다.
- 결과 값
BeanFactory or ApplicationContext
ApplicationContext
는 spring의 bootstrap 과정의 entry point가 되기때문에BeanFactory
를 직접 사용하지 말고 superset인ApplicationContext
를 사용해야합니다.- Bean의 처리 과정 전체를 직접 컨트롤해야 하는 경우에는
BeanFactory
를 직접 사용해야할 수도 있지만 이런 경우 신경써야할 부분이 많아집니다.ApplicationContext
에서는 bean들이 이름이나 타입같은 컨벤션에 의해 감지되지만,BeanFactory
의 일반적인 구현체인DefaultListableBeanFactory
에서는 몇몇 bean이 감지되지 못합니다.ApplicationContext
가 제공하는BeanPostProcessor
가 없음으로 AOP proxy나 annotation processing을 사용할 수 없습니다