@SpringBootTest 는 어떻게 동작할까? (2)

정원식·2024년 12월 25일

Spring Test

목록 보기
2/4

개요

  • 본 시리즈에서는 @SpringBootTest 의 동작 원리를 확인합니다
  • 두번째 편에서는 테스트 환경에서 ApplicationContext 초기화 과정에 대해 살펴봅니다.
    • Spring Test 와 Spring Boot Test 의 다른 초기화 과정을 중점으로 봅니다.

사용한 버전

  • junit-jupiter: 5.10.3
  • spring-boot-test: 3.3.4
  • spring-test: 6.1.13

Spring Test

  • 플로우 자체는 첫번째 편과 동일하며 실제 호출되는 구현체를 기술하였습니다.
  • DelegatingSmartContextLoader 는 세부 AbstractGenericContextLoaderApplicationContext 생성을 위임합니다.
    • AnnotationConfigContextLoader : 설정 클래스에서 ApplicationContext 를 로딩합니다.
    • GenericXmlContextLoader : XML 에서 ApplicationContext 를 로딩합니다.
    • GenericGroovyXmlContextLoader : Groovy 와 XML 에서 ApplicationContext 를 로딩합니다.

AbstractGenericContextLoader

private GenericApplicationContext loadContext(
    MergedContextConfiguration mergedConfig, boolean forAotProcessing) throws Exception {
			
    validateMergedContextConfiguration(mergedConfig);  // do nothing
    // 1. GenericApplicationContext 를 생성합니다.
    GenericApplicationContext context = createContext();
    try {
        ApplicationContext parent = mergedConfig.getParentApplicationContext();
        if (parent != null) {
            context.setParent(parent);
        }
            
        prepareContext(context);  // do nothing
        // 2. ApplicationContext 를 설정합니다.
        prepareContext(context, mergedConfig);
        customizeBeanFactory(context.getDefaultListableBeanFactory());  // do nothing
        // 3. BeanDefinition 을 로드합니다.
        loadBeanDefinitions(context, mergedConfig);
        // 4. 기본적으로 필요한 빈을 등록합니다.
        AnnotationConfigUtils.registerAnnotationConfigProcessors(context);
        customizeContext(context);  // do nothing
        // 5. ContextCustomizer 를 실행합니다.
        customizeContext(context, mergedConfig);

        if (!forAotProcessing) {
            // 6. ApplicationContext 를 초기화 합니다.
            context.refresh();
            context.registerShutdownHook();
        }

        return context;
    } catch (Exception ex) {
        throw new ContextLoadException(context, ex);
    }
}

ApplicationContext 가 생성되는 순서는 다음과 같습니다.

  1. GenericApplicationContext 를 생성
  2. BeanDefinition 이 등록되기전에 ApplicationContext 를 설정합니다.
    1. ActiveProfile 을 등록합니다.
    2. PropertySource 를 등록합니다. (ex: @TestPropertySource(locations))
    3. 인라인 프로퍼티를 등록합니다. (ex: @TestPropertySource(properties))
    4. ApplicationContextInitializer 를 이용해 ApplicationContext 를 초기화 합니다.
  3. BeanDefinition 을 로드합니다.
    • 세부 구현체 별로 로딩하는 방법이 다릅니다.
  4. 기본적으로 필요한 빈을 등록합니다.
    • AnnotationAwareOrderComparator
    • ContextAnnotationAutowireCandidateResolver
    • ConfigurationClassPostProcessor
    • AutowiredAnnotationBeanPostProcessor
    • CommonAnnotationBeanPostProcessor
    • PersistenceAnnotationBeanPostProcessor
    • EventListenerMethodProcessor
    • DefaultEventListenerFactory
  5. ContextCustomizer 를 실행합니다.
  6. ApplicationContext 를 초기화 합니다.

Spring Boot Test

  • 플로우 자체는 첫번째 편과 동일하며 실제 호출되는 구현체를 기술하였습니다.
  • SpringBootContextLoaderSpringApplication 을 실행하고 생성된 ApplicationContext 를 반환합니다.

SpringBootContextLoader

private ApplicationContext loadContext(MergedContextConfiguration mergedConfig, Mode mode,
    ApplicationContextInitializer<ConfigurableApplicationContext> initializer) throws Exception {
    
    // 1. @SpringBootTest 어노테이션 추출
    assertHasClassesOrLocations(mergedConfig);
    SpringBootTestAnnotation annotation = SpringBootTestAnnotation.get(mergedConfig);
    String[] args = annotation.getArgs();    // SpringBootTest
    
    // 2. SpringApplication 실행
    // 2-1. SpringApplication 의 main 메서드 실행을 통해 애플리케이션 실행
    UseMainMethod useMainMethod = annotation.getUseMainMethod();
    Method mainMethod = getMainMethod(mergedConfig, useMainMethod);
    if (mainMethod != null) {
        ContextLoaderHook hook = new ContextLoaderHook(mode, initializer,
	    (application) -> configure(mergedConfig, application));
        return hook.runMain(() -> ReflectionUtils.invokeMethod(mainMethod, null, new Object[] { args }));
    }
    
    // 2-2. SpringApplication#run 을 통해 애플리케이션 실행
    SpringApplication application = getSpringApplication();
    configure(mergedConfig, application);
    ContextLoaderHook hook = new ContextLoaderHook(mode, initializer, ALREADY_CONFIGURED);
    return hook.run(() -> application.run(args));
}

// 3. 설정
private void configure(MergedContextConfiguration mergedConfig, SpringApplication application) {
    // 3-1. Context 설정 등록
    application.setMainApplicationClass(mergedConfig.getTestClass());
    application.addPrimarySources(Arrays.asList(mergedConfig.getClasses()));
    application.getSources().addAll(Arrays.asList(mergedConfig.getLocations()));
    
    // 3-2. ApplicationContextInitializer 추출
    List<ApplicationContextInitializer<?>> initializers = getInitializers(mergedConfig, application);
    
    // 3-3. Web 환경별 설정
    if (mergedConfig instanceof WebMergedContextConfiguration) {
        application.setWebApplicationType(WebApplicationType.SERVLET);
        if (!isEmbeddedWebEnvironment(mergedConfig)) {
            new WebConfigurer().configure(mergedConfig, initializers);
        }
    } else if (mergedConfig instanceof ReactiveWebMergedContextConfiguration) {
        application.setWebApplicationType(WebApplicationType.REACTIVE);
    } else {
        application.setWebApplicationType(WebApplicationType.NONE);
    }
    
    // 3-4. ApplicationContextFactory 설정
    application.setApplicationContextFactory(getApplicationContextFactory(mergedConfig));
    if (mergedConfig.getParent() != null) {
        application.setBannerMode(Banner.Mode.OFF);
    }
    
    // 3-5. ApplicationContextInitializer 등록
    application.setInitializers(initializers);
    // 3-6. ConfigurableEnvironment 등록
    ConfigurableEnvironment environment = getEnvironment();
    if (environment != null) {
        prepareEnvironment(mergedConfig, application, environment, false);
        application.setEnvironment(environment);
    } else {
        application.addListeners(new PrepareEnvironmentListener(mergedConfig));
    }
}

ApplicationContext 가 생성되는 순서는 다음과 같습니다.

  1. @SpringBootTest 어노테이션 추출
  2. SpringApplication 실행
    1. Spring Application 의 main 메서드 실행을 통해 애플리케이션 실행 참조
      • @SpringBootTest(useMainMethod)ALWAYS 혹은 WHEN_AVAILABLE 일때 실행됩니다.
    2. SpringApplication#run 을 통해 애플리케이션 실행
      • @SpringBootTest(useMainMethod)NEVER 혹은 WHEN_AVAILABLE 일때 실행됩니다.
  3. 애플리케이션 실행전 SpringApplication 을 설정합니다.
    1. Context 설정 등록
    2. ApplicationContextInitializer 추출
      • ContextCustomizerContextCustomizerAdapter 를 통해 ApplicationContextInitializer 로 등록됩니다.
    3. Web 환경별 설정
    4. ApplicationContextFactory 설정
      • Web 환경별 ApplicationContext 를 생성하는 팩토리를 등록합니다.
    5. ApplicationContextInitializer 등록
    6. ConfigurableEnvironment 등록

결론

  • 테스트 환경에서 ApplicationContext 초기화 과정에 대해 살펴봅니다.
  • Spring Test 와 Spring Boot Test 의 초기화 과정은 다르지만
    Spring Test 의 스펙대로 테스트 컴포넌트가 정상 등록됨을 확인할수 있었습니다. (ex: ContextCustomizer)
  • 다음편에서는 보다 통합 테스트를 잘 작성하기 위한 사용법을 위주로 살펴보겠습니다~

Reference

profile
매일매일 성장하고 싶은 백엔드 개발자입니다.

0개의 댓글