본문 바로가기
Framework & Lib/스프링

스프링 빈 생성 과정 분석 [2] - BeanDefinitionRegistry, SingletonBeanRegistry

by 코딩공장공장장 2024. 4. 20.

[스프링 빈 생성 과정 분석 시리즈]

 

지난 포스팅에서 스프링 빈을 생성하는 주체인 application context가 어떻게 이루어져 있는지 알아보았다.

이번 포스팅에서는 본격적으로 스프링 빈이 어떻게 생성되는지 그 과정을 알아보도록 하겠다.

 

1. 빈 생성 과정


스프링 빈 생성과정에 대해 간단히 소개 하겠다.

 

     1) 생성할 빈의 BeanDefition 정의

     2) BeanDefinition을 BeanDefinitionResitry에 저장

     3) BeanDefition을 바탕으로 빈을 인스턴스화

     4) 빈 인스턴스를 SingletonBeanRegistry에 저장
        (스프링 default 빈 생성 방식은 싱글톤)

 

스프링 빈은 BeanDefinitionResitry에 저장된 빈 정의 기반으로 인스턴스화하여 SingletonBeanRegistry에 저장된다.

두 저장소는 모두 map 타입이고 key값으로 빈 이름이 사용된다.
따라서 빈 정의와 빈 인스턴스를 별도로 관리하더라도 빈 이름이라는 키 값을 통해 식별할 수 있다.

 

구체적인 빈 등록과정을 알기 위해서는 어떤 구현체들이 사용되는지 알아야한다.

스프링 부트 AutoConfig 방식에서 사용되는 beanFactory의 구현체가 무엇인지 알아보자.

 

2. BeanFactory 구현체 찾기 - DefaultListableBeanFactory


(빈 팩토리 관련 상속만 표현함)

 

BeanFactory를 상속 받은 ApplicationContext 구현체의 상속관계를 보면 위와 같다.

허나, 위 구현체들 중 어느것도 BeanFactory의 추상 메서드에 대한 구현을 직접적으로 처리하고 있지 않다.

// 추상 메서드
public abstract ConfigurableListableBeanFactory getBeanFactory() throws IllegalStateException;

public Object getBean(String name) throws BeansException {
  this.assertBeanFactoryActive();
  // 추상 메서드인 getBeanFactory()에 처리를 돌림
  return this.getBeanFactory().getBean(name);
}

public boolean isSingleton(String name) throws NoSuchBeanDefinitionException {
  this.assertBeanFactoryActive();
  // 추상 메서드인 getBeanFactory()에 처리를 돌림
  return this.getBeanFactory().isSingleton(name);
}

public int getBeanDefinitionCount() {
  // 추상 메서드인 getBeanFactory()에 처리를 돌림
  return this.getBeanFactory().getBeanDefinitionCount();
}

public String[] getBeanDefinitionNames() {
  // 추상 메서드인 getBeanFactory()에 처리를 돌림
  return this.getBeanFactory().getBeanDefinitionNames();
}

 

BeanFactory를 오버라이딩한 메서드를 보니 구현로직이 존재하지 않고

getBeanFactory().getBean() 와 같이 메서드를 호출하여 해당 반환값을 그대로 반환하고 있다.

getBeanFactory()는 DefaultListableBeanFactory 타입의 인스턴스를 반환한다.

public abstract ConfigurableListableBeanFactory getBeanFactory() throws IllegalStateException;
public class GenericApplicationContext extends AbstractApplicationContext implements BeanDefinitionRegistry {

  private final DefaultListableBeanFactory beanFactory;

  public final ConfigurableListableBeanFactory getBeanFactory() {
      return this.beanFactory;
  }
  ...
}

 

즉, applicationContext가 beanFactory의 추상 메서드를 직접 구현하지 않고 참조속성으로 갖는 객체에 처리를 넘기고 있다.

상속을 합성 구조로 바꾸었다.


따라서 스프링 autoConfig 방식에서 사용되는 빈 생성 과정을 파악하기 위해서는 applicationContext의 참조속성인 DefaultListableBeanFactory의 구조를 파악해야한다.

 

[잠깐] ApplicationContext는 왜 상속을 합성으로 바꾸었을까?

1. 낮은 결합도
클래스 상속은 캡슐화가 되지 않기에 상위 타입의 책임이 하위 구현체에게 그대로 전가되는 구조를 갖는다.
하지만 이를 합성 구조로 바꾸게 된다면 상하위 모두 책임을 갖고 있는 구조가 아니라 합성된 구현체 하나에만 책임을 갖게할 수 있다.
결합도를 낮춰 변경사항에 영향을 미치는 범위를 줄일 수 있다.

2. 응집도 향상
수직적인 상속은 하나의 구현체에 여러 책임을 떠안게 할 수 있다.
이를 합성으로 바꾸면 여러 구현체에 책임을 분산시킬 수 있기에 응집도를 향상 시킬 수 있다.
응집도가 낮으면 서로 연관 없는 로직에 의해 영향을 받을 수 있고,
코드 가독성에도 좋지 못한다. 
applicationContext는 beanFactory 이외에도 많은 인터페이스를 수직적으로 구현하고 있기에
beanFactory마저 구현했다면 applicationContext는 수많은 책임을 떠안은 코드 모음이 되었을 것이다.

3. 코드 재사용성 향상
applicationContext는 beanFactory 이외에도 많은 인터페이스를 구현하고 있다.
만약 beanFactory 관련 기능 변경 및 확장시 새로운 beanFactory로 교체하고 기존 코드는 재사용할 수 있다.
상속 구조라면 새로운 클래스를 생성해서 사용해야하는 단점이 있다.(클래스 폭발)

 

DefaultListableBeanFactory의 상속 관계

 

상속관계를 보니 이전에 설명한 BeanDefinitionRegistry와 SingletonBeanRegistry가 존재한다.

실제 소스를 분석해보면 BeanDefinitionRegistry의 구현은 DefaultListableBeanFactory에 되어있고,

SingletonBeanRegistry의 구현은 DefaultSingletonBeanRegistry에 되어있는 것을 확인할 수 있다.

 

이제 본격적으로 DefaultListableBeanFactory에 구현된 로직을 분석하여 스프링 빈 등록 과정에 대해 알아보자.

 

(* applicationContext구현체는 의존성에 설정에 따라 다를 수 있다. spring-boot-web 의존 설정이 되어있다면 GenericWebApplicationContext가 구현체로 사용된다. 허나, DefaultListableBeanFactory가 빈팩토리 역할을 하는 것은 같다.)

 

3. BeanDefinitionRegistry


3.1 BeanDefinition

BeanDefinition에 대해 먼저 알고가자.

String SCOPE_SINGLETON = "singleton";
String SCOPE_PROTOTYPE = "prototype";

String getBeanClassName();

void setScope(@Nullable String scope);

@Nullable
String getScope();

void setLazyInit(boolean lazyInit);

boolean isLazyInit();

void setAutowireCandidate(boolean autowireCandidate);

boolean isAutowireCandidate();

void setPrimary(boolean primary);

 

위는 BeanDefinition 인터페이스에 명시된 메서드 몇개를 간추린 것이다.

 

각 속성은 다음과 같다.

  • beanClassName : 빈의 클래스명(패키지 포함)
  • scope : 스코프
                - singleton : 컨텍스트에 하나 생성
                - prototype : 조회시 마다 생성
  • 초기화 시점(true: 빈 조회 시점 초기화, false: 구동 시점에 모두 초기화, default는 false이다.)
  • AutowireCandidate : 다른 빈 객체에게 Autowired 되는지 여부
  • primary : 상위 타입이 같은 구현체들 사이에서 의존 주입시 우선순위를 갖는지 여부 

 

BeanDefinition을 간단히 말해서 빈의 속성이라고 하는데 이렇게 얘기하면 마치 객체의 인스턴스 속성처럼 들리기도 한다.
그래서 나는 이를 명확히 구분하고자 빈의 생성 및 관리 속성이라고 하는게 구분하기에 좋을 것 같다.

 

위와 같은 정의가 먼저 등록된 이후 이를 바탕으로 빈이 생성되고 관리되는 것이다.

 

빈의 이름이 없어서 의아할 수 있겠지만 앞에서 얘기햇듯이 빈의 이름이 키값으로 사용되고 value값이 beanDefinition으로 사용되기에 빈의 이름이 beanDefinition에 포함되지 않는 것이다.

 

 (* 빈 스코프에 request나 session이 있다고 하는 글을 많이 보았을 것이다.
이는 spring-web 관련 의존설정을 한 경우 WebApplicationContext에서 사용되는 스코프 설정이다.)

 

3.2 BeanDefinitionRegistry

BeanDefinitionRegistry 인터페이스에는 아래와 같은 두 메서드가 존재한다.

public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)

public BeanDefinition getBeanDefinition(String beanName)

 

DefaultListableBeanFactory에서 구현한 두 메서드의 구현부를 보면 각각 아래와 같은 부분이 존재한다.

beanDefinitionMap.put(beanName, beanDefinition)

beanDefinitionMap.get(beanName);

 

beanDefinitionMap은 DefaultListableBeanFactory의 참조속성이다.

private final Map<String, BeanDefinition> beanDefinitionMap;

 

이 beanDefinitionMap이 beanDefintionRegistry가 되는 것이다.

 

그런데, registerBeanDefinition 메서드를 보니 beanDefinition을 인자값으로 받는다.

registerBeanDefinition 메서드는 인자로 받은 beanDefinition을 등록만 할 뿐 beanDefinition을 생성하지 않는다.

 

3.3 BeanDefinition 생성 및 등록 과정

beanDefinition이 어떻게 생성되는지 알아보자.


[ClassPathBeanDefinitionScanner의 doScan메서드]

public class ClassPathBeanDefinitionScanner extends ClassPathScanningCandidateComponentProvider {
. . .
protected Set<BeanDefinitionHolder> doScan(String... basePackages) {

  for(int var5 = 0; var5 < var4; ++var5) {
      String basePackage = var3[var5];
       /**
        * 1. basePackage에 @Component가 붙은 후보군 스캔
        * 스캔 과정에서 beanDefinition을 생성하여 가져온다.
        * 이때, 가져오는 beanDefinition에는 클래스타입, 어노테이션 정보들이 포함되어있다.
        */
      Set<BeanDefinition> candidates = this.findCandidateComponents(basePackage);

      while(var8.hasNext()) {
           /**
            * 2 빈 스코프 정의
            */
          candidate.setScope(scopeMetadata.getScopeName());

          if (candidate instanceof AnnotatedBeanDefinition annotatedBeanDefinition) {
           /**
            * 3. lazy, primary 빈 정보 등록
            */
      AnnotationConfigUtils.processCommonDefinitionAnnotations(annotatedBeanDefinition);
          }

          if (this.checkCandidate(beanName, candidate)) {
          
               // 4. beanDefinitionRegistry에 beanDefinition 저장
               this.registerBeanDefinition(definitionHolder, this.registry);
          }
      }
  }

  return beanDefinitions;
}

 

ClassPathBeanDefinitionScanner의 doScan메서드가 beanDefinition을 생성하고 속성값을 지정한다.

basePackage 하위 경로를 스캔하여 @Configuration, @Component 어노테이션이 부착된 클래스의 beanDefinition을 생성한다.

이후 Primary나 Lazy와 같은 어노테이션이 부착됬는지 체크하고 beanDefinition 속성을 정의하게 된다.

모든 속성을 추출하여 설정했다면 최종적으로 beanDefinitionRegistry에 등록하게 된다.

 

ClassPathBeanDefinitionScanner의 doScan메서드는 BeanFactoryPostProcessor에 의해 호출된다.

(다음 글에서 BeanFactoryPostProcessor에 대해 설명하며 BeanDefintion 생성 과정에 대해 보다 자세히 설명하겠다.)

 

4. SingletonBeanRegistry


스프링 빈 인스턴스화는 BeanFactory의 getBean 메서드를 통해 이루어진다.

우리는 이전 포스팅에서 ListableBeanFactory에 getBeanDefinitionNames() 메서드가 존재하는 것을 보았다.

beanDefinition이 모두 등록 되었다면 getBeanDefinitionNames()을 통해 등록된 beanDefinition 이름을 모두 가져오고

getBean(beanName) 메서드를 통해 빈의 인스턴스화와 등록이 이루어진다.

beanName은 식별자이기에  beanDefinitionRegistry에서 beanDefinition을 가져와 이를 기반으로 인스턴스화가 가능한 것이다.

그리고 이러한 플로우의 구체적인 처리가 singletonBeanRegistry에 정의되어있다.

 

singletonBeanRegistry는 빈의 인스턴스화 및 등록이라는 중요한 책임도 있지만,

싱글톤 방식으로 관리하기 위해 동기화 처리를 제공해준다.

singletonBeanRegistry가 동기화 처리를 대신해주기에 우리는 싱글톤으로 관리할 스프링 빈 클래스에 직접 싱글톤 설계를 적용하지 않더라도 싱글톤으로 사용할 수 있다.

다소, 주제에서 벗어날 수 있는 내용일 수도 있으나 singletonBeanRegistry의 핵심이라고 생각되어 함께 설명하도록 하겠다.

 

4.1 싱글톤 소개

[예제 코드]

 

위 코드는 싱글톤 객체를 생성하는 대표적인 예제 코드이다.

 

구현 방식을 보면 

  1. 생생자를 막는다.
  2. get함수를 통해 인스턴스 존재 유무 파악
    2-1) 존재시 인스턴스 반환
    2-2) 미존재시 클래스에 synchronized 적용하고 인스턴스화

 이제 스프링의 싱글톤 빈 생성 방식을 보자.

 

4.2 SingletonBeanRegistry

void registerSingleton(String beanName, Object singletonObject);

Object getSingleton(String beanName);

 

SingletonBeanRegistry 인터페이스의 싱글톤 빈 인스턴스화 관련 메서드만 보겠다.

 

i) registerSingleton 메서드

public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {
   // 싱글톤 레지스트리(스프링 싱글톤 빈 인스턴스를 관리하는 맵 객체)
   private final Map<String, Object> singletonObjects = new ConcurrentHashMap(256);

   // 생략
   public void registerSingleton(String beanName, Object singletonObject) throws IllegalStateException {
      // 싱글톤 레지스트리에 락을 걸어 접근 제한
      synchronized(this.singletonObjects) {
          Object oldObject = this.singletonObjects.get(beanName);
          if (oldObject != null) { // null이 아니면 에러
              throw new IllegalStateException("Could not register object [" + singletonObject + "] under bean name '" + beanName + "': there is already object [" + oldObject + "] bound");
          } else { // 존재하지 않으면 등록
              this.addSingleton(beanName, singletonObject);
          }
      }
   }
   // 생략
}

 

registerSingleton 메서드를 보라.

인자값으로 singletonObject를 받는다.

인스턴스화를 진행하는게 아니라, 이미 인스턴스화된 객체를 받고 있다.

registerSingleton이라는 메서드명으로 인해 마치 빈을 인스턴스화하고 등록 해줄 것 같지만
이미 인스턴화된 빈을 등록만할 뿐 빈 생성을 하지 않는다.

registerSingleton 메서드를 통해 직접 등록되는 빈들은 주로 컨텍스트 초기화 단계(컨텍스트가 의존성을 갖춰나가는 단계)에서 필요로하는 빈들이다. (ex. Environment, MessageSource, 스프링 부트 배너)

 

ii) getSingleton 메서드

개발자가 직접 정의한 빈을 포함한 대부분의 빈은 getSingleton(String beanName, ObjectFactory<?> singletonFactory) 메서드를 통해 인스턴스화 되고 싱글톤 레지스트리에 저장된다.

   public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
      // 싱글톤 레지스트리에 락을 걸어 접근 제한
      synchronized(this.singletonObjects) {
           Object singletonObject = this.singletonObjects.get(beanName);

           // 빈 미존재시
           if (singletonObject == null) {

              // 생략
              try {
                  // 빈 인스턴스화
                  singletonObject = singletonFactory.getObject();
                  newSingleton = true;
              } catch (IllegalStateException var16) {
              // 생략
              } catch (BeanCreationException var17) {
              // 생략
              } finally {
              // 생략
              if (newSingleton) {
                  // 싱글톤 레지스트리에 빈 이름과 인스턴스 저장
                  this.addSingleton(beanName, singletonObject);
              }
          }

          return singletonObject;
      }
   }

 

두번째 인자값인 ObjectFactory<?> singletonFactory는 객체 생성 함수를 갖는 팩토리 객체이다.

 

AbstractBeanFactory의 getBean 메서드를 추적하면 아래와 같이

이전에 등록한 beanDefinition을 가져와 인스턴스화 목적의 팩토리 메서드에 전달해주고 있음을 확인할 수 있다.

// beanName에 해당하는 beanDefinition 조회
RootBeanDefinition mbd = this.getMergedLocalBeanDefinition(beanName);

if (mbd.isSingleton()) {
   // getSingleton 메서드에 빈 인스턴스화 메서드를 포함한 팩토리 객체 전달
   sharedInstance = this.getSingleton(beanName, () -> {
       try {
           // 빈 인스턴스화 메서드
           return this.createBean(beanName, mbd, args);
       } catch (BeansException var5) {
           this.destroySingleton(beanName);
           throw var5;
       }
   });

   beanInstance = this.getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
}

 

 

전체 과정을 단순하게 표현하면 아래와 같다.

getBean() -> getSingleton() -> 인스턴스 존재시 반환, 미존재시 createBean()을 통해 인스턴스화 후 등록

 

getSingleton() 메서드의 처리가 

위에서 예제로 설명한 싱글톤 패턴 예제 코드와 비슷하다는 생각이 들 것이다.

인스턴스가 존재하면 존재하는 것을 반환하고 존재하지 않으면 인스턴스화하여 반환하는 것

 

다른 점은 동기화 적용 대상이다.

싱글톤 패턴 예제 코드에서는 get 메서드가 static 메서드로 클래스 레벨에 존재하기에 클래스에 동기화를 적용했고,

스프링의 경우 get 메서드가 싱글톤 레지스트리 객체에 동기화를 적용하였다.

스프링이 싱글톤 레지스트리라는 맵 인스턴스를 통해 동기화 방식을 구현하였기에
우리는 클래스 레벨에 싱글톤 인스턴스화 메서드를 구현하지 않아도 싱글톤 빈을 사용할 수 있는 것이다.

(잠깐) 싱글톤 패턴과 스프링 싱글톤의 차이

일반적으로 구현하는 싱글톤은 생성자를 private으로 선언하기에 상속이 불가하고, 
static 메서드를 통해 싱글톤 인스턴스에 접근하기에 클래스를 통해 결합해야하는 높은 의존도를 갖추게되는 단점이 있다.
허나 스프링의 빈은 클래스 레벨에 구현하지 않고도 싱글톤 레지스트리를 통해 싱글톤 인스턴스를 사용할 수 있으므로
상속 또한 가능하고, 결합도 또한 낮추는 구조를 가질 수 있다.

 

다시 getSingleton 메서드로 돌아와 코드를 보며 설명을 더 진행하겠다.

첫줄부터 synchronized키워드가 적용되어 있다.

보통 처음 인스턴스의 존재 유무를 파악하는 코드는 synchronized 키워드를 적용하지 않는 경우가 대부분이다.
이 메서드가 언제 동작되는지 디버깅을 찍어가며 테스트 해본 결과 구동시점에 빈을 등록하는데 사용된다.
(스프링 부트 3.2.3 기준)

이 메서드는 생성 목적으로 사용할 get메서드였기에 첫줄부터 synchronized를 적용한 것으로 판단된다.

 

iii) getSingleton - 조회 및 생성 목적

 

아래에서 설명할 다른 getSingleton 메서드는 빈이 모두 생성된 이후에 빈을 조회하는데 사용되는 메서드이다.

물론 빈 인스턴스화 또한 진행한다.

허나, 디버깅하며 테스트 해본 결과 해당 로직이 실행되는 경우는 한번도 보지 못했다.

일반적인 케이스에서 실행되는 경우는 아닌 것 같다.

(혹시 해당 케이스를 알고 있다면 댓글로 공유 해주시면 감사하겠습니다.)

@Nullable
public Object getSingleton(String beanName) {
   return this.getSingleton(beanName, true);
}

@Nullable
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
  // get을 통해 싱글톤 레지스트리에서 빈 가져옴
   Object singletonObject = this.singletonObjects.get(beanName); 

       // 빈 존재하지 않는다면
       if (singletonObject == null && allowEarlyReference) {
       
           // 동기화 블럭 시작
           synchronized(this.singletonObjects) { 
               // 다시 한번 get
               singletonObject = this.singletonObjects.get(beanName);  

               // 존재하지 않으면
               if (singletonObject == null) {
                        // 빈 생성
                       ObjectFactory<?> singletonFactory = (ObjectFactory)this.singletonFactories.get(beanName); 
                        // 생략 ...
                   }
               }
           }
       }
   }
   return singletonObject;

}

 

이 내용을 소개하는 이유는 스프링의 동기화 코드를 소개하기 위해서이다.

 

동기화 블럭 이전에 이미 한번 get을 함에도 동기화 블럭에서 또 get을 한다. 이 두 get메서드의 역할은 다르다.

 

첫번째 get은 조회 목적이다.

동기화 블럭 안에서 두번째 get은 인스턴스화 목적이다.
첫번째 get에 synchronized를 걸면 단순 빈 조회 목적인데도 동기화 처리를 하기 때문에 성능에서 떨어진다.
동기화 블럭진입 이후 한번 더 get하는 이유는 동시성 문제는 read하고 modify하는 짧은 시간차로 발생할 수 있기에 

다시 한번 get하고 이때도 존재하지 않으면 인스턴스를 생성하는 것이다.



반응형