주의 : 틀린 내용이 있을 수 있습니다..! 내용을 보완 중에 있습니다.

 

SpringBootConfiguration?

개발자의 편의를 위해 EnableConfiguration과 SpringBootConfiguration, 그리고 Component Scan을 합쳐 하나로 제공한다.

// 기존에 작성하던 방식을 좀더 간편하게 하였다.
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
        @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public class ExampleApplication {

    public static void main(String[] args) {
        SpringApplication.run(DbunitApplication.class, args);
    }
}

// 동일하게 동작한다.
@SpringBootApplication
public class ExampleApplication {

    public static void main(String[] args) {
        SpringApplication.run(DbunitApplication.class, args);
    }
}

 

EnableAutoConfiguration?

Spring Boot Application에 존재하는 특정한 패키지에 포함된 jar 파일과 Bean Metadata들을 CLASSPATH를 통해 접근하여 Bean들을 자동 구성하는 애노테이션이다.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {

}

Spring-Boot-Stater Dependency들을 추가하게 되면 해당 정보들이

 

org.springframework.boot:spring-boot-autoconfigure > META-INF > spring.factories

 

에 포함되며 별도로 제외할 basePackage, class 정보를 적용하지 않는 이상 모두 자동으로 등록하게 된다.

 

 

spring.factories

# Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\
org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener

# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.autoconfigure.BackgroundPreinitializer

# Auto Configuration Import Listeners
org.springframework.boot.autoconfigure.AutoConfigurationImportListener=\
org.springframework.boot.autoconfigure.condition.ConditionEvaluationReportAutoConfigurationImportListener

# Auto Configuration Import Filters
org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\
org.springframework.boot.autoconfigure.condition.OnBeanCondition,\
org.springframework.boot.autoconfigure.condition.OnClassCondition,\
org.springframework.boot.autoconfigure.condition.OnWebApplicationCondition

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
.....

내부에 정의된 클래스 패스들을 따라가게 되면 @ConditionalXXX를 통해 각각의 조건을 검증하여 통과하는 경우에 등록하는 방식으로 선언되어 있다.

 

 

ex) DispatcherServletAutoConfiguration

@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass(DispatcherServlet.class)
@AutoConfigureAfter(ServletWebServerFactoryAutoConfiguration.class)
public class DispatcherServletAutoConfiguration {

    /**
     * The bean name for a DispatcherServlet that will be mapped to the root URL "/".
     */
    public static final String DEFAULT_DISPATCHER_SERVLET_BEAN_NAME = "dispatcherServlet";

    /**
     * The bean name for a ServletRegistrationBean for the DispatcherServlet "/".
     */
    public static final String DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME = "dispatcherServletRegistration";

  // DispatcherServletRegistrationConfiguration...

    // DefaultDispatcherServletCondition...

    // DispatcherServletRegistrationCondition...
}

 

EnableAutoConfiguration 흐름 요약

Context가 초기화되면 등록된 BeanPostProcessor들을 이용하여 BeanDefinition들을 읽어오게 됩니다. 이때 AutoConAutoConfigurationPackage와 연결된 AutoConfigurationPackage.class의 정적 클래스인 registrar가 Spring.factory파일의 클래스 패스 정보를 통해 Configuration Class들을 모두 가져와 저장하게 되고, 이것이 ConfigurationClassBeanDefinitionReader를 통해서 읽어져 온 뒤 ConfigurationClassParser에서 Condition 구현체를 통한 조건 검증을 진행하고 통과한 Bean Definition에 대해서만 ImportStack에 추가하여 BeanPostProcessor가 해당 정보를 싱글톤 형식의 Bean으로 등록하게 됩니다. 

 

 

@Configuration

해당 어노테이션이 작성된 클래스가 Java Config 기반의 Bean Definition 으로 사용되는 것임을 나타낸다.

내부적으로 @Component라는 Meta-Annotation이 정의되어 있으므로 ComponentScan을 통해 탐색되어 읽어 들일 수 있으며, 이는

  1. ConfigurationClassPostProcessor의 postProcessBeanDefinitionRegistry()메서드 호출
  2. 내부의 processConfigBeanDefinitions() 메서드를 호출한 뒤 ConfigurationClassParser 를 통해 읽어 들여진 Configuration Class들을 ConfigurationClass 타입으로 파싱 한다.
  3. 해당 정보를 ConfigurationClassBeanDefinitionReader의 loadBeanDefinitions() 메서드에 전달하여 순회한다.
  4. 순회되는 정보들을 loadBeanDefinitionsForConfigurationClass() 메서드를 통해 정보를 읽어 들어와 Bean Definition으로 등록한다.
  5. 이후 ConfigurationClass 객체들을 alreadyParsed에 저장하고 다음 로직을 진행한다.

의 흐름을 가지고 등록되게 된다.

 

 

@EnableAutoConfiguration에 포함된 AutoConfigurationImportSelector.class, @AutoConfigurationPackage의 흐름 따라가 보기

 

Import(AutoConfigurationImportSelector.class)

META-INF/spring.factories에 존재하는 정보들을 가져와 등록하는 클래스이다.

  1. Main 메서드가 호출되고 run 메서드가 실행된다.

  2. run 메서드 실행 중 refreshContext() 메서드가 ConfigurableApplicationContext 타입의 객체를 전달받으며 호출된다.

  3. 넘겨받은 객체 타입을 검증하는 refresh(ApplicationContext) 메서드에 전달하여 확인하고 해당 객체 타입의 인자가 전달되지 않는 refresh() 메서드를 호출한다.

  4. 메서드 내부적으로 invokeBeanFactoryPostProcessors(BeanFactory); 를 호출한다. 인자로 전달받은 beanFactory에서 PostProcessor 개수를 전달받은 뒤 순회하면서 해당 정보를 ArrayList인 currentRegistryProcessors에 저장한다.

  5. currentRegistryProcessors를 정렬하고 해당 리스트와 beanFactory를 BeanDefinitionRegistry로 Casting 한 다음 invokeBeanDefinitionRegistryPostProcessors() 메서드에 전달한다.

  6. 내부적으로 전달받은 ArrayList를 순회하며 각각의 PostProcessor 들을 등록하는 절차를 진행하는 중 postProcessBeanDefinitionRegistry → processConfigBeanDefinitions에 regisrar 정보를 넘긴다.

  7. processConfigBeanDefinitions 내부에서 BeanDefinitionHolder 정보를 담는 ArrayList인 configCandidates 객체를 생성한 뒤 해당 객체의 요소를 추가하는 작업을 진행한다.

  8. → 전달받은 registry가 가지고 있는 DefinitionNames 들을 가져와 순회하면서 BeanDefinition 정보와 Bean Name을 꺼내오고 그것을 저장하는 BeanDefinitionHolder를 생성하여 저장한다.

  9. configCandidates 객체를 정렬한 뒤 ConfigurationClassParser를 생성하고 정렬된 객체를 Set으로 변환한 다음 parse() 메서드의 인자로 넘긴다.

  10. parse() 메서드 내부에서는 전달받은 Set를 순회하며 가져온 BeanDefinition 정보를 instanceof를 통해 Sub Class Type을 검증한 뒤 해당 정보에 맞게 캐스팅하여 BeanName과 함께 parse()를 호출하며 인자로 넘긴다.

  11. deferredImportSelectorHandler의 process()가 호출된다.

  12. 해당 메서드는 deferredImportSelectors 정보를 가져와 정렬한 뒤 forEach를 통해 순회하며 DeferredImportSelectorGroupingHandler register()를 호출하여 해당 정보에 포함된 Annotation Meta 정보와 ConfigurationClass 정보, deferredImport 정보를 호출된 객체 내부에 존재하는 Map에 저장한다.

    List<ConfigurationClassParser.DeferredImportSelectorHolder> deferredImports 
            = this.deferredImportSelectors;
    
    ConfigurationClassParser.DeferredImportSelectorGroupingHandler handler 
            = ConfigurationClassParser.this.new DeferredImportSelectorGroupingHandler();
    
    deferredImports.sort(ConfigurationClassParser.DEFERRED_IMPORT_COMPARATOR);
    deferredImports.forEach(handler::register);
    handler.processGroupImports();
    
    private class DeferredImportSelectorGroupingHandler {
    
            private final Map<Object, DeferredImportSelectorGrouping> groupings = new LinkedHashMap<>();
    
            private final Map<AnnotationMetadata, ConfigurationClass> configurationClasses = new HashMap<>();
    
            public void register(DeferredImportSelectorHolder deferredImport) {
                Class<? extends Group> group = deferredImport.getImportSelector().getImportGroup();
                DeferredImportSelectorGrouping grouping = this.groupings.computeIfAbsent(
                        (group != null ? group : deferredImport),
                        key -> new DeferredImportSelectorGrouping(createGroup(group)));
                grouping.add(deferredImport);
                this.configurationClasses.put(deferredImport.getConfigurationClass().getMetadata(),
                        deferredImport.getConfigurationClass());
            }
  13. 저장된 정보를 가지고 processGroupImports() 메서드를 호출한다.

  14. 앞서 저장된 Map을 순회하면서 getImports() 메서드를 호출한다.

  15. getImports() 메서드는 전달받은 DeferredImportSelectorHolder 타입 객체 정보를 순회한다.

  16. AutoConfigurationImportSelector의 process()를 호출하면서 DeferredImportSelectorHolder의 ConfigurationClass의 Metadate 정보와 importSelector를 전달한다.

  17. 주어진 importSelector를 AutoConfigurationImportSelector로 캐스팅하여 getAutoConfigurationEntry() 메서드를 호출한 다음 AutoConfigurationEntry 타입 객체인 autoConfigurationEntry에 저장한다.

  18. 저장한 entry의 MetaData를 가져와 ConfigurationClass 타입의 객체를 선언하여 저장한다.

  19. processImports() 메서드를 호출하고 주어진 ConfigurationClass 객체를 importStack이 상속받은 ArrayDeque에 해당 정보를 저장한다.

  20. 전달받은 Collection importCandidates 향상된 For문으로 순회한다.

  21. 해당 정보는 ImportSelector 또는 ImportBeanDefinitionRegistrar가 아닌 Configuration 정보이기에 해당 정보를 importStack의 LinkedMultiValueMap에 저장하게 된다.

    → 이때 저장되는 정보는 위에서 주어진 ConfigurationClass를 SourceClass로 필터를 거쳐 Converting 된 객체의 Metadata와 enrty의 포함된 Import Class 컬랙션이다.

  22. importCandidates의 요소를 ConfigClass로 변환하고 grouping에 저장되어 있던 exclusionFilter를 전달한 뒤 SourceClass로 컨버팅하고 doProcessConfigurationClass()를 호출한다.

  23. Bean, ComponentScan 등의 어노테이션들 정보가 포함되어 있는지 확인하여 각각의 어노테이션에 맞게 해당 정보를 로딩하거나 파싱 한 뒤 Bean을 생성한다.

 

@AutoConfigurationPackage?

자동 구성 패키지 정보를 저장하는 AutoConfigurationPackages 클래스의 정보를 가져오는 어노테이션이다.

 

AutoConfigurationPackages 클래스는 자동 구성 패키지 내의 정의된 Meta-annotation들을 기반으로 정보들을 가져온 뒤 별도의 리스트를 통해 (이후에 참조될) 패키지 이름들을 저장하는 로직을 가지고 있다.

 

해당 로직을 통해 저장된 패키지 정보들은 이후 beanDefinition으로 저장되게 되는데 이는 Import 된 AutoConfigurationPackages.Registrar.class를 통해 이루어진다.

static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {

        @Override
        public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
            register(registry, new PackageImports(metadata).getPackageNames().toArray(new String[0]));
        }

        @Override
        public Set<Object> determineImports(AnnotationMetadata metadata) {
            return Collections.singleton(new PackageImports(metadata));
        }
}

public static void register(BeanDefinitionRegistry registry, String... packageNames) {
    if (registry.containsBeanDefinition(BEAN)) {
            BasePackagesBeanDefinition beanDefinition = (BasePackagesBeanDefinition) registry.getBeanDefinition(BEAN);
            beanDefinition.addBasePackages(packageNames);
    }
    else {
            registry.registerBeanDefinition(BEAN, new BasePackagesBeanDefinition(packageNames));
    }
}

PackageImports(AnnotationMetadata metadata) {
        AnnotationAttributes attributes = AnnotationAttributes
                .fromMap(metadata.getAnnotationAttributes(AutoConfigurationPackage.class.getName(), false));
        List<String> packageNames = new ArrayList<>(Arrays.asList(attributes.getStringArray("basePackages")));
        for (Class<?> basePackageClass : attributes.getClassArray("basePackageClasses")) {
                packageNames.add(basePackageClass.getPackage().getName());
        }
        if (packageNames.isEmpty()) {
                packageNames.add(ClassUtils.getPackageName(metadata.getClassName()));
        }
        this.packageNames = Collections.unmodifiableList(packageNames);
}

 

EnableAutoConfiguration와 관련된 어노테이션들

  • @AutoConfigureOrder

    ApplicationContext에 전달될 자동 구성 클래스 순서를 정의하는 어노테이션이다.

    각 Bean의 의존 관계와 DependsOn 어노테이션을 통해 내부적으로 등록 순서가 결정되며 Bean이 만들어지는 것과 주입하는 순서에 대한 속성과는 별개의 어노테이션이다.

  • @AutoConfigureAfter

    인자로 주어진 자동 구성 클래스가 적용된 뒤에 해당 어노테이션이 적용된 자동 구성 클래스를 적용하게끔 하는 어노테이션이다.

  • @Import

    다른 @Configuration 이 적용된 클래스의 Bean Definition을 명시적으로 컨테이너나 구성 클래스에 적용할 수 있도록 한다.

    ComponentScan 어노테이션과 유사한 동작 방식을 가지지만, 다수의 구성 클래스를 적용하기 위해서는 모두 명시적으로 작성하여야 된다.

    기본적으로 사용하는 이유는 여러 설정을 하나의 클래스에 작성하지 않고 유사성을 따라 분리하여 정의하고 하나의 구성 클래스에서 정보를 집계하여 사용하기 위함이다.

      @Configuration
      @Import({ AConfig.class, BConfig.class })
      public class GroupConfiguration {
      }

 

Spring Boot가 설정해주는 것들 몇 가지 짚어보기

 

DataSource 자동 초기화

  1. Main 메서드가 호출되고 run 메서드가 실행된다.

  2. run 메서드 실행 중 refreshContext() 메서드가 ConfigurableApplicationContext 타입의 객체를 전달받으며 호출된다.

  3. 넘겨받은 객체 타입을 검증하는 refresh(ApplicationContext) 메서드에 전달하여 확인하고 해당 객체 타입의 인자가 전달되지 않는 refresh() 메서드를 호출한다.

  4. 메서드 내부적으로 invokeBeanFactoryPostProcessors(BeanFactory); 를 호출한다. 인자로 전달받은 beanFactory에서 PostProcessor 개수를 전달받은 뒤 순회하면서 해당 정보를 ArrayList인 currentRegistryProcessors에 저장한다.

  5. currentRegistryProcessors를 정렬하고 해당 리스트와 beanFactory를 BeanDefinitionRegistry로 Casting 한 다음 invokeBeanDefinitionRegistryPostProcessors() 메서드에 전달한다

  6. 내부적으로 전달받은 ArrayList를 순회하며 각각의 PostProcessor 들을 등록하는 절차를 진행한다. postProcessBeanDefinitionRegistry → processConfigBeanDefinitions → ConfigurationClassBeanDifinitionReader에 정보를 넘겨 생성한 뒤 loadBeanDefinitions 메서드를 호출한다.

  7. 넘겨받은 Set를 순회하며 loadBeanDefinitionsForConfigurationClass를 호출한다. 해당 메서드는 loadBeanDefinitionsFromRegistrars()를 호출하여 registrar 들의 registerBeanDefinitions() 메서드를 호출하는 절차를 진행한다.

  8. DataSourceInitializationConfiguration 내부에 Regisrar의 registerBeanDefinitions()를 호출하고 해당 로직에서 dataSourceInitializerPostProcessor이라는 이름을 가진 PostProcessor 가 존재하지 않는 경우 DataSourceInitializerPostProcessor.class을 등록하는 로직을 실행한다.

  9. 이후 Bean 이 등록되어 초기화 절차를 진행하는데 이때 afterPropertiesSet() 메서드를 호출하여 createSchema()를 통해 DB의 Schema를 등록하는 절차를 진행한다.

  10. 위의 로직이 성공하면 initialize()를 호출하여 구성된 Schema에 data.sql 정보를 읽어 등록한다.

  11. sql 파일들을 통해 정상적으로 DB가 구성되고 사용할 수 있게 된다.

 

내용을 계속 추가하고 있습니다.

 

 

 

Spring vs Spring Boot

[의존성 관리와 설정을 자동으로 해준다! - Starter]

 

스프링 부트는 스프링 프레임워크의 경량화를 위해 분리된 수많은 Jar 파일을 필요에 맞게 등록하고, 관련 설정을 선언하는 절차없이 기본 설정값으로 제공되는 @Configuration Class들을 통해 빠른 실행을 가능케 하고, Bean에 대한 Builder를 통해 사용자 정의도 쉽게 할 수 있게 하여 빠른 개발 환경 구성을 지원하기 위해 만들어진 것입니다.

 

내장 톰켓에 대해서도 요청을 관리하기 위해 필요했던 매핑 작업, 서블릿 등록, 컨텍스트 설정들을 구성해주고 있기에 바로 실행하여 결과를 확인할 수 있게 되는 것을 알 수 있습니다.

 

이것이 Spring과 Spring Boot 제일 큰 차이이며, Spring Boot를 사용해야하는 근본적인 이유임을 알 수 있습니다. 

 

 

참고 자료

 

Post Entity, DTO 들을 만들고 생성 테스트를 진행한 뒤에 Post Controller와 Service를 생성하고 PostMapper에 Mapper 어노테이션 작성과 Mapper.xml 작성을 완료한 후 Teliend API를 통한 테스트를 진행하였다.

 

그런데.. POST 요청 보냈더니 이러한 에러가 발생하였다.

org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): com.somaeja.post.mapper.PostMapper.save
at org.apache.ibatis.binding.MapperMethod$SqlCommand.<init>(MapperMethod.java:235) ~[mybatis-3.5.5.jar:3.5.5]
at org.apache.ibatis.binding.MapperMethod.<init>(MapperMethod.java:53) ~[mybatis-3.5.5.jar:3.5.5]
at org.apache.ibatis.binding.MapperProxy.lambda$cachedInvoker$0(MapperProxy.java:115) ~[mybatis-3.5.5.jar:3.5.5]
at java.base/java.util.concurrent.ConcurrentHashMap.computeIfAbsent(ConcurrentHashMap.java:1705) ~[na:na]
at org.apache.ibatis.binding.MapperProxy.cachedInvoker(MapperProxy.java:102) ~[mybatis-3.5.5.jar:3.5.5]
at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:85) ~[mybatis-3.5.5.jar:3.5.5]
at com.sun.proxy.$Proxy54.save(Unknown Source) ~[na:na]
at com.somaeja.post.service.PostService.savePost(PostService.java:20) ~[classes/:na]
...
...
...

2시간 정도 고생을 하다보니 설정된 코드에서 문제점을 발견하였다...

 

 

문제점

@EnalbeAutoConfiguration <- 문제점 
@MapperScan(basePackages = "com.somaeja")
public class PersistenceConfig {
        <- 문제점 
}

 

문제점 파악.

 

@EnableAutoConfiguration 은 Spring Boot 관련 자동 구성 어노테이션이었는데,

Dependencies > spring-boot-autoconfigure > META-INF > spring.factories

디렉터리 내부의 Definition 들을 Bean으로 등록해 준다고 하여서 configuration 대신에 사용하였었다.

 

ContextLoad Test

@SpringBootTest
class MainApplicationTests {
    @Test
    void contextLoads() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PersistenceConfig .class);
        String[] beanDefinitionNames = ac.getBeanDefinitionNames();
        for (String beanDefinitionName : beanDefinitionNames) {
            System.out.println("beanDefinitionName = " + beanDefinitionName);
        }
    }
}

ContextLoad 테스트를 진행하였을 때에도 Datasource, sqlSessionFactory, postMapper 빈이 등록되어 있어 문제가 없다고 판단하였지만, 사실은 Mybatis 설정이 적용되지 않은 sqlSessionFacroty와 Root Context (MainApplication.class)에 등록되지 않은 postMapper 이였다는 것을 알지 못하였고, 헤맬 수밖에 없었다.

 

 

문제 해결 방법.

@Configuration // @EnableAutoConfiguration 에서 Configuration 으로 변경
@MapperScan(basePackages = "com.somaeja")
public class PersistenceConfig {

    // Custom Initializer -> Mybatis MapperLocations 설정이 적용된 SqlSessionFactory 생성 
    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        final SqlSessionFactoryBean sqlSessionFactory = new SqlSessionFactoryBean();
        sqlSessionFactory.setDataSource(dataSource);

        // 스프링에서 Xml 파싱을 통한 Bean 생성시 사용하는 PathMatchingResourcePatternResolver 를 사용
        // 하여 classpath: 패턴의 경로를 작성하고 Mapper Location에 설정하였다.
        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        sqlSessionFactory.setMapperLocations(resolver.getResources("classpath:mybatis/mapper/*.xml"));
        return sqlSessionFactory.getObject();
    }

}

 

참고 문서

MyBatis - 마이바티스 3 | 소개

MyBatis - 마이바티스 3 | 매퍼 XML 파일

Mybatis-Spring-Boot-Starter 소개 문서 번역

'프로젝트' 카테고리의 다른 글

Mybatis의 IndexOutOfBoundsException?  (2) 2021.01.12
Spring Boot Gradle Test 실패?  (0) 2020.12.31
[Somaeja : 소매자] 01. 프로젝트 개요  (0) 2020.11.15

+ Recent posts