woniper

Spring IoC Container를 까보자 #Bean 등록은 어떻게 될까? 본문

Spring

Spring IoC Container를 까보자 #Bean 등록은 어떻게 될까?

Woniper 2017.02.05 01:16

  Spring에서 IoC Container 역할을 하는 BeanFactory 와 ApplicationContext 동작 방법이 궁금했다. 작년 SLiPP에서 스터디를 했었는데, 다시 한번 블로그에 정리하려 한다.

스프링 구조

  일단 스프링에서 BeanFactoryApplicationContext의 다이어그램을 보자. 사실 스프링 구조를 잘 이해하지 못하고 다이어그램만 보고는 각각의 interface와 class가 어떤 역할을 하는지 도통 알 수 없다.

핵심은 BeanFactory와 ApplicationContext다.



ApplicationContext applicationContext = new AnnotationConfigApplicationContext(BeanConfig.class);

  AnnotationConfigApplicationContext라는 ApplicationContext의 구현체를 생성했다. 어떤 일이 일어 나는지 알아보자.
  ApplicationContext의 구현체는 여러가지가 있지만 이번 글에서는 AnnotationConfigApplicationContext를 기준으로 설명하겠다. 동작 방식은 거의 유사하다.

private final AnnotatedBeanDefinitionReader reader;
private final ClassPathBeanDefinitionScanner scanner;

public AnnotationConfigApplicationContext() {
    this.reader = new AnnotatedBeanDefinitionReader(this);
    this.scanner = new ClassPathBeanDefinitionScanner(this);
}

public AnnotationConfigApplicationContext(Class<?>... annotatedClasses) {
    this();
    register(annotatedClasses);
    refresh();
}

  AnnotationConfigApplicationContext 코드의 일부다. 우리가 사용한 AnnotationConfigApplicationContext(Class<?>... annotatedClasses); 는 기본 생성자를 호출하고 registerrefresh라는 메소드를 호출한다. 차근차근 하나씩 까보자.

AnnotatedBeanDefinitionReader

  AnnotationConfigApplicationContext(Class<?>... annotatedClasses) 생성자는 파라미터로 클래스 타입을 받는다. 이 타입은 Bean을 설정한 클래스인데 @Configuration이 붙은 클래스(이제 부터 BeanConfig라고 말하겠다.)를 말한다. (아래와 같은 클래스)

@Configuration
public class BeanConfig {

    @Bean
    public AccountService accountService() {
        AccountService accountService = new AccountService();
        accountService.setAccountRepository(accountRepository());
        return accountService;
    }

    @Bean
    public AccountRepository accountRepository() {
        return new AccountRepository();
    }
}

  JavaConfig라고도 하는데 몇 년 전만 해도 우린 Bean 설정을 xml을 통해 설정했다.(아직도 많이 사용하지만..) 그와 같은 설정이다. 이것을 설명하기 위해 메타데이터를 설명하고 싶다.

위키 백과에는 아래와 같이 설명한다.

메타데이터(metadata)는 데이터(data)에 대한 데이터이다. 이렇게 흔히들 간단히 정의하지만 엄격하게는, Karen Coyle에 의하면 어떤 목적을 가지고 만들어진 데이터 (Constructed data with a purpose)라고도 정의한다.

  데이터에 대한 데이터라는 표현이 딱 맞는 표현인 거 같다. AnnotationConfigApplicationContext(Class<?>... annotatedClasses) 파라미터로 넘긴 BeanConfig는 Bean을 생성하기 위한 메타데이터다.

그럼 메타데이터인 BeanConfig를 이용해 어떻게 Bean을 생성하는지 알아보자.

public void register(Class<?>... annotatedClasses) {
    Assert.notEmpty(annotatedClasses, "At least one annotated class must be specified");
    this.reader.register(annotatedClasses);
}

AnnotationConfigApplicationContext.register 메소드다. 여기서 바로 AnnotatedBeanDefinitionReader.register 메소드를 호출한다. 가보자!

public void registerBean(Class<?> annotatedClass, String name, Class<? extends Annotation>... qualifiers) {
    AnnotatedGenericBeanDefinition abd = new AnnotatedGenericBeanDefinition(annotatedClass);
    if (this.conditionEvaluator.shouldSkip(abd.getMetadata())) {
        return;
    }

    ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(abd);
    abd.setScope(scopeMetadata.getScopeName());
    String beanName = (name != null ? name : this.beanNameGenerator.generateBeanName(abd, this.registry));
    AnnotationConfigUtils.processCommonDefinitionAnnotations(abd);
    if (qualifiers != null) {
        for (Class<? extends Annotation> qualifier : qualifiers) {
            if (Primary.class == qualifier) {
                abd.setPrimary(true);
            }
            else if (Lazy.class == qualifier) {
                abd.setLazyInit(true);
            }
            else {
                abd.addQualifier(new AutowireCandidateQualifier(qualifier));
            }
        }
    }

    BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(abd, beanName);
    definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
    BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, this.registry);
}

  register 메소드를 타고 쭉 타고 들어가면 overloading된 registerBean 메소드를 찾을 수 있다. 여기서 몇가지 중요한 클래스가 등장한다.

  • AnnotatedGenericBeanDefinition : BeanDefinition 구현체중 하나, BeanConfig를 통해 등록될 Bean에 메타데이터를 정의한 클래스라고 할 수 있다.
  • ScopeMetadata : 이름에서 알 수 있듯이 Bean Scope에 메타데이터를 정의한 클래스다.
  • BeanDefinitionHolder : 메타데이터가 정의된 BeanDefinition을 정의한다. 이 클래스는 BeanDefinition과 name, alias를 포함한다.
  • BeanDefinitionReaderUtils : Bean을 IoC Container 즉, ApplicationContext에 저장하는 역할을 하는 utils 클래스 이다.

BeanDefinitionReaderUtils

public static void registerBeanDefinition(
        BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry)
        throws BeanDefinitionStoreException {

    // Register bean definition under primary name.
    String beanName = definitionHolder.getBeanName();
    registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());

    // Register aliases for bean name, if any.
    String[] aliases = definitionHolder.getAliases();
    if (aliases != null) {
        for (String alias : aliases) {
            registry.registerAlias(beanName, alias);
        }
    }
}

  BeanDefinitionRegistry.registerBeanDefinition 메소드와 registerAlias 메소드를 통해 Bean을 등록하고 있다. 여기서 파라미터로 넘어오는 BeanDefinitionRegistry에 정체는 무엇일까?

다시 처음으로 돌아가서 AnnotationConfigApplicationContext의 기본생성자를 보자.

public AnnotationConfigApplicationContext() {
    this.reader = new AnnotatedBeanDefinitionReader(this);
    this.scanner = new ClassPathBeanDefinitionScanner(this);
}

  AnnotatedBeanDefinitionReader를 생성하며 자기 자신(AnnotationConfigApplicationContext)을 넘긴다. BeanDefinitionRegistry에 정체가 바로 이것이다. AnnotationConfigApplicationContextBeanDefinitionRegistry을 implements했다. 정확히는 GenericApplicationContext(AnnotationConfigApplicationContext은 GenericApplicationContext의 자식)가 implements했다. (구조가 복잡하다.)

조금만 더 가보자.

BeanDefinitionRegistry.registerBeanDefinition

private final DefaultListableBeanFactory beanFactory;

@Override
public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
        throws BeanDefinitionStoreException {

    this.beanFactory.registerBeanDefinition(beanName, beanDefinition);
}

  위 코드는 BeanDefinitionRegistry에 구현체인 GenericApplicationContext이다. 그런데!! 여기서 또 BeanFactory.registerBeanDefinition 메소드를 호출한다.
  GenericApplicationContext의 초기화된 BeanFactory는 DefaultListableBeanFactory다. 다시 처음으로 돌아가 다이어그램을 보면 BeanDefinitionRegistry 를 implements 한 구현체는 GenericApplicationContextDefaultListableBeanFactory다.

즉, ApplicationContext(GenericApplicationContext)는 BeanFactory(DefaultListableBeanFactory)를 상속과 동시에 조립해서 사용하고 있다는 뜻이다.

구조가 너무 복잡하다고 여기서 포기할 수 없다. Bean을 어떻게 Container에 담아서 보관하는지 살펴보자.

DefaultListableBeanFactory.registerBeanDefinition

@Override
public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
        throws BeanDefinitionStoreException {

    // 생략...

    BeanDefinition oldBeanDefinition;

    oldBeanDefinition = this.beanDefinitionMap.get(beanName);
    if (oldBeanDefinition != null) {
        // 생략...        
        this.beanDefinitionMap.put(beanName, beanDefinition);
    } else {
        if (hasBeanCreationStarted()) {
            // 생략...
        } else {
            // Still in startup registration phase
            this.beanDefinitionMap.put(beanName, beanDefinition);
            this.beanDefinitionNames.add(beanName);
            this.manualSingletonNames.remove(beanName);
        }
        this.frozenBeanDefinitionNames = null;
    }

    // 생략...
}

  드디어 Bean이 어디에 저장되는지 정체가 나타났다. 바로 Map이다. 그런데 Map Generic Type이 Map<String, BeanDefinition>이다. 저장되는 type이 BeanDefinition이다. Bean instance가 아니다. 왜 그럴까? 다음글에 설명하겠지만, 바로 Scope 때문이다.
  IoC Container는 일단 BeanDefinition을 저장한다. 그 후 getBean 메소드가 호출 될때 Scope를 판단해 instance를 생성 > 저장(singleton인 경우) > 반환 한다.
  Scope를 생각해보면 바로 instance를 생성해서 저장하지 않는 이유를 알 것이다. singleton이면 항상 같은 객체를 반환하고, prototype이면 항상 새로운 객체를 생성해서 반환하기 때문이다. 바로 BeanDefinition을 기반으로.

요약

  1. Bean을 등록하기 위해 정의하는 클래스 또는 xml을 메타데이터라고 한다.
  2. BeanFactory와 ApplicationContext는 부모와 자식관계이며, 실제 구현체에서는 상속 뿐 아니라 조립을 통해 코드를 구현했다.
  3. 메타데이터를 통해 Bean을 정의하는 클래스는 BeanDefinition이며, BeanDefinition과 name, alias를 정의하는 클래스는 BeanDefinitionHolder 이다.
  4. IoC Container에 최초에 저장되는 Bean은 instance가 아닌 BeanDefinition이 Map에 저장된다.

  Spring 소스를 보면 구조가 굉장히 복잡한 것을 알 수 있다. 그런데 차근차근 살펴보면 그럴만한 이유가 있다. 바로 책임을 잘 분리하고 상속과 조립을 적절하게 사용하는 것을 배울 수 있다.

메소드 책임 분리

  BeanDefinitionRegistry를 보면 책임 분리를 interface를 통해 클래스 뿐 아니라 메소드 또한 하나의 책임을 갖고 있는 것을 볼 수 있다.

상속과 조립

  GenericApplicationContext 클래스를 보면 상속과 조합을 적절히 사용하는 것을 배울 수 있다. ApplicationContext는 BeanFactory를 상속하고 GenericApplicationContext는 ApplicationContext를 implements 하며 BeanFactory도 당연히 implements 된다. GenericApplicationContext 멤버 변수로 BeanFactory 구현체인 DefaultListableBeanFactory를 선언(조립)해 사용한다. BeanFactory를 implements해 구현해야될 메소드를 이미 모든 기능이 구현된 BeanFactory 구현체인 DefaultListableBeanFactory를 상속하지 않고 조립하여 대신하는 것을 보면 알 수 있다.

2 Comments
댓글쓰기 폼