Docs/Spring

Spring Doc - Core -IOC Container

이쟁쟁 2021. 5. 3. 17:55

Spring framework Document 읽기

The IoC Container

스프링에서 IOC container를 어떻게 구현했는지 알아봅니다.

Bean과 IoC Container

  • org.springframework.beansorg.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

beanScope

  • 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);
          }
        }
      }

빈 초기화 과정

beanInitiation

  • annotation 기반의 설정을 사용한 spring boot 1.5.1 버전의 예시로 xml 방식 또는 다른 버전에서는 구현체가 다를 수 있습니다.
  • DefaultListableBeanFactory의 상속 구조
    DefaultListableBeanFactoryHierachy2

Container

Dependency Injection

  • Spring에서 DI는 컨테이너가 bean 인스턴스를 생성한 후에 의존관계를 주입해주는 방식으로 동작합니다.
  • DI를 사용함으로써 얻는 장점으로는 객체간 결합도를 낮출 수 있고, 코드가 깔끔해지며, mock으로 대체하기 쉽기때문에 test에도 유리합니다.
  • Constructor-based 방식과 Setter-based 방식이 존재합니다.
    • Spring 팀에서는 생성자 주입방식을 추천합니다.
      1. argument를 통한 programmatic validation 가능
      2. 컴포넌트를 불변객체로 구현가능
      3. Client에게 완벽히 초기화된 상태로 리턴가능
      4. 의존관계가 너무 늘어난 경우 발생하는 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 방식로 주입하는 코드도 찾아보면 좋을것 같습니다
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 차이에서 오는 문제를 해결 할 수 있습니다

    https://spring.io/blog/2004/08/06/method-injection/

    • 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

application-context-init-process-at-spring-boot

  • 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을 수행합니다.

    • BeanPostProcessorApplicationContext의 생성에 직접 관여하게 됨으로 잘못 커스터마이징 하는경우 동작에 치명적일 수 있습니다.

    • 구현 예

          @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되었는지 확인이 필요합니다.
  • Spring annotation을 대신해서 Java annotation을 사용하는 방법도 존재합니다.
    annotations

  • 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을 사용할 수 없습니다