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

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

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

BeanDefinitionRegistry, SingletonBeanRegistry


1. 빈 생성 과정

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

 

     1) 생성할 빈의 BeanDefition 정의

     2) BeanDefition을 BeanDefinitionResitry에 저장

     3) BeanDefition을 바탕으로 빈을 인스턴스화 (스프링 default 빈 생성 방식은 싱글톤)

     4) 빈 인스턴스를 SingletonBeanRegistry에 저장

 

BeanDefition과 빈 인스턴스는 1대1 관계이다.

두 저장소는 모두 Map타입이고 key값으로 빈의 이름이 사용된다.
BeanDefinitionRegistry와 SingletonBeanResgistry가 별도의 공간임에도
식별할 수 있는 이유는 빈 이름이 식별값으로 사용되기 때문이다.

따라서 빈 이름은 컨텍스트 내에서 고유해야하는 빈의 식별값이다.

 

2. BeanDefinitionRegistry, SingletonBeanRegistry의 구현체

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

스프링 부트 AutoConfig 방식에서 사용되는 ApplicationContext의 구현체는  AnnotationConfigApplicationContext이다.
AnnotationConfigApplicationContext의 상속관계를 알아보자.

 

2.1 AnnotationConfigApplicationContext 상속 관계

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

AbstractApplicationContext 클래스는 추상 클래스이고,
GenericApplicationContext, AnnotationConfigApplicationContext가 구현체이다.

 

허나, GenericApplicationContext, AnnotationConfigApplicationContex는 BeanDefinition 및 인스턴스 등록을 하지 않는다.

즉, 빈 생성 및 관리를 하지 않는다.

 

AbstractApplicationContext를 보면 그 비밀을 알 수 있다.

// 추상 메서드
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();
}

AbstractApplicationContext가 구현한 BeanFactory의 메서드들을 보면 모두 getBeanFactory()라는 메서드를 호출하여 구현하였다.
getBean() 메서드를 보면 getBeanFactory().getBean()와 같이 구현되어있다.

 

그런데  getBeanFactory()는 추상 메서드이다.

 

일반적으로 자바에서 getObject 형식으로 선언된 추상 메서드는 하위 구현체에서 해당 Object를 참조속성으로 갖고 제공하라는 형식으로 많이 사용된다.

 

AbstractApplicationContext는 상위 인터페이스의 구현을 추상메서드가 반환하는 객체에게 돌렸다.
상속을 합성으로 바꾸는 구조이다.

 

AbstractApplicationContext의 하위 구현체인 GenericApplicationContext를 보면 getBeanFactory를 통해 자신의 참조속성인 DefaultListableBeanFactory 타입의 객체를 반환한다.

public class GenericApplicationContext extends AbstractApplicationContext implements BeanDefinitionRegistry {

  private final DefaultListableBeanFactory beanFactory;

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

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

 

GenericApplicationContext 또한 상위타입 인터페이스인 BeanDefinitionRegistry에 정의된 메서드를 자신의 참조속성인 DefaultListableBeanFactory에 아래와 같이 처리를 넘긴다.

public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition) throws BeanDefinitionStoreException {
  this.beanFactory.registerBeanDefinition(beanName, beanDefinition);
}

 

(잠깐)
AbstractApplicationContext는 상위 타입 인터페이스 메서드 구현 책임을 하위 구현체에게 떠넘겼다.
이는 상속관계를 합성관계로 바꾼 것을 의미한다.


  • AbstractApplicationContext는 왜 상속을 합성으로 바꾸었을까?

    AbstractApplicationContext의 구조를 보면 다른 인터페이스는 모두 구현을 하였지만 BeanFactory, HirerachicalBeanFactory, ListableBeanFactory의 메서드 구현에 대한 책임을 하위 구현체에게 전가하였다.

    여기서 책임전가라는 표현이 부정적으로 들릴지 모르겠지만 사실 이는 결합도를 낮추는 좋은 구조이다.
    클래스 상속은 캡슐화가 되지 않기에 상위 타입의 책임이 하위 구현체에게 그대로 전가되는 구조를 갖는다.
    하지만 이를 합성 구조로 바꾸게 된다면 상위 하위 모두 책임을 갖고 있는 구조가 아니라
    하위 구현체 하나만 책임을 갖게할 수 있다.

    AbstractApplicationContext를 보면 하위에 구현체가 두개 더 존재한다.
    만약 빈팩토리 관련 메서드를 모두 구현하였다면 이 하위 두 구현체 모두에 책임이 전가되고
    각 applicationContext는 수많은 책임 떠안은 클래스가 되는 구조가 되었을 것이다.

    이뿐만 아니라 합성 구조를 사용하면 BeanFactory 쉽게 교체하여 사용할 수 있다.
    getBeanFactory를 통해 반환되는 객체만  바꿔주면 새로운 beanFactory를 사용할 수 있다.
    상속 구조였다면 하나하나 다 오버라이딩을 해야하고 재상용성이 굉장히 떨어졌을 것이다.



2.2 DefaultListableBeanFactory의 상속 관계

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

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

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

이제 본격적으로 각 구현체에서 beanDefinition 등록과정과 빈 인스턴스 등록 과정을 알아보자.

 

(* applicationContext구현체는 gradle 설정에 따라 다를 수 있다. 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을 등록한다.
BeanDefinition 인터페이스에 정의되어있는 스코프, lazy, primary와 같은 정보가 저장됨을 알 수 있다.

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

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

 

 

4. SingletonBeanRegistry

4.1 싱글톤 소개

[예제 코드]

class Singleton {
private static Singleton singleton;
private Singleton() {} //생성자에 접근 x

public static Singleton getInstance(){
    if(singleton == null){
        synchronized (Singleton.class) {
            if(singleton == null) singleton = new Singleton();
        }
    }
    return singleton;
}
}

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

 

구현 방식을 보면 

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

따라서 이 메서드는 생성자처럼 빈을 생성하는게 아니다.

 

또한 등록시에 get을 통해 빈의 존재 유무를 판단하고 존재하지 않을시 싱글톤 레지스트리에 빈을 등록한다. 

 

이 코드를 설명한 이유는 메서드명을 보고 마치 생성자와 같은 역할을 한다고 착각하지 말자는 의미에서 설명하였다.

 

참고로, 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 메서드를 추적하면 아래와 같이 getSingleton을 호출하고
메서드의 인자로 createBean 메서드를 포함한 객체를 전달하고 있다.


// 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);

}

이 팩토리 객체를 getSingleton 메서드의 인자값으로 전달하면 getSingleton 내에서 인스턴스의 존재여부를 판단하고 존재하지 않을시 팩토리 객체의 createBean 메서드를 실행한다.

 

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

getBean() -> getSingleton() -> 인스턴스 존재시 반환, 미존재시 createBean()

 

registerSingleton 메서드는 빈을 인스턴스화 하지 않았고 getSingleton은 get을 통해 인스턴스가 존재하면 반환하고, 존재하지 않으면 인스턴스화 하여 등록하였다.

 

두 메서드를 통해 알 수 있는 것은 싱글톤 레지스트리는 생성자와 같이 생성만 처리하는 메서드가 존재하지 않고,
반드시 get메서드를 통해 인스턴스 존재유무를 파악하고 생성한다는 것이다.

 

이는 위에서 예제로 설명한 싱글톤 패턴 예제 코드의 구현 방식과 거의 같다.

 

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

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

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


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

 

다시 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하고 이때도 존재하지 않으면 인스턴스를 생성하는 것이다.



반응형