Post

EnableAutoConfiguration

우리가 필요할 것 같은 빈들을 자동으로 설정해주는 자동 설정 기능을 활성화하는 애노테이션이다

자동 설정을 적용할 클래스 선별은 AutoConfigurationImportSelector가 처리해준다. 이때 특정 클래스를 설정으로 추가하는 @Import를 통해 해당 클래스를 추가해준다. 즉, @EnableAutoConfiguration을 붙이면 AutoConfigurationImportSelector까지 추가된다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {

  String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

  Class<?>[] exclude() default {};

  String[] excludeName() default {};
}

AutoConfigurationImportSelector는 자동 설정할 대상들을 결정하는데, 자동 설정이 가능한 목록은 spring-boot-autoconfigure.jar의 META-INF 디렉토리의 spring.factories에서 확인할 수 있다. 해달 파일을 보면 다양한 자동 설정을 스프링이 지원하고 있음을 알 수 있다.


AutoConfigurationImportSelector는 다음 메소드를 통해 자동 설정이 가능한 후보군들을 모두 불러온다

1
2
3
4
5
6
7
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata,AnnotationAttributes attributes){
  List<String> configurations=SpringFactoriesLoader.loadFactoryNames(
  getSpringFactoriesLoaderFactoryClass(),getBeanClassLoader());
  Assert.notEmpty(configurations,"No auto configuration classes found in META-INF/spring.factories. 
  If you are using a custom packaging,make sure that file is correct.");
  return configurations;
  }

SpringFactoriesLoader.loadFactoryNames로 가보면 클래스 로더를 사용해서 spring.factories에서 값을 불러옴을 확인할 수 있다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
public final class SpringFactoriesLoader {

  public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";


  private static final Log logger = LogFactory.getLog(SpringFactoriesLoader.class);

  static final Map<ClassLoader, Map<String, List<String>>> cache = new ConcurrentReferenceHashMap<>();


  private SpringFactoriesLoader() {
  }

  public static <T> List<T> loadFactories(Class<T> factoryType, @Nullable ClassLoader classLoader) {
    Assert.notNull(factoryType, "'factoryType' must not be null");
    ClassLoader classLoaderToUse = classLoader;
    if (classLoaderToUse == null) {
      classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
    }
    List<String> factoryImplementationNames = loadFactoryNames(factoryType, classLoaderToUse);
    if (logger.isTraceEnabled()) {
      logger.trace("Loaded [" + factoryType.getName() + "] names: " + factoryImplementationNames);
    }
    List<T> result = new ArrayList<>(factoryImplementationNames.size());
    for (String factoryImplementationName : factoryImplementationNames) {
      result.add(instantiateFactory(factoryImplementationName, factoryType, classLoaderToUse));
    }
    AnnotationAwareOrderComparator.sort(result);
    return result;
  }


  public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
    ClassLoader classLoaderToUse = classLoader; //클래스 로더를 사용해서 값을 가져온다
    if (classLoaderToUse == null) {
      classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
    }
    String factoryTypeName = factoryType.getName();
    return loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
  }

  private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
    Map<String, List<String>> result = cache.get(classLoader);
    if (result != null) {
      return result;
    }

    result = new HashMap<>();
    try {
      Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);
      while (urls.hasMoreElements()) {
        URL url = urls.nextElement();
        UrlResource resource = new UrlResource(url);
        Properties properties = PropertiesLoaderUtils.loadProperties(resource);
        for (Map.Entry<?, ?> entry : properties.entrySet()) {
          String factoryTypeName = ((String) entry.getKey()).trim();
          String[] factoryImplementationNames =
            StringUtils.commaDelimitedListToStringArray((String) entry.getValue());
          for (String factoryImplementationName : factoryImplementationNames) {
            result.computeIfAbsent(factoryTypeName, key -> new ArrayList<>())
              .add(factoryImplementationName.trim());
          }
        }
      }

      // Replace all lists with unmodifiable lists containing unique elements
      result.replaceAll((factoryType, implementations) -> implementations.stream().distinct()
        .collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList)));
      cache.put(classLoader, result);
    } catch (IOException ex) {
      throw new IllegalArgumentException("Unable to load factories from location [" +
        FACTORIES_RESOURCE_LOCATION + "]", ex);
    }
    return result;
  }

  @SuppressWarnings("unchecked")
  private static <T> T instantiateFactory(String factoryImplementationName, Class<T> factoryType, ClassLoader classLoader) {
    try {
      Class<?> factoryImplementationClass = ClassUtils.forName(factoryImplementationName, classLoader);
      if (!factoryType.isAssignableFrom(factoryImplementationClass)) {
        throw new IllegalArgumentException(
          "Class [" + factoryImplementationName + "] is not assignable to factory type [" + factoryType.getName() + "]");
      }
      return (T) ReflectionUtils.accessibleConstructor(factoryImplementationClass).newInstance();
    } catch (Throwable ex) {
      throw new IllegalArgumentException(
        "Unable to instantiate factory class [" + factoryImplementationName + "] for factory type [" + factoryType.getName() + "]",
        ex);
    }
  }

}

loadSpringFactories 메소드는 전체 jar 파일로부터 spring.factories들을 불러오는 기능으로, 스프링 애플리케이션 시작 시에 매우 자주 호출된다. 문제는 해당 호출이 디스크에서 값을 읽어오므로 처리 속도가 상당히 느리다는 것인데 이 때문에 스프링은 캐싱 방식을 적용하고 있다. loadSpringFactories메소드의 처음 로직을 보면 캐시에서 값을 꺼내고 없으면 디스크에서 읽어오도록 처리하는 부분이 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public final class SpringFactoriesLoader{
  private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
    Map<String, List<String>> result = cache.get(classLoader);  //이 부분에서 캐시를 사용한다
    if (result != null) {   //읽어온 값이 있다면 그대로 리턴한다
      return result;
    }
    
    //읽어온 값이 없다면 값을 디스크에서 읽어오는 작업을 한다
    result = new HashMap<>();
    try {
      Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);    //META-INF/spring.factories 에서 값을 읽어올 것임
      while (urls.hasMoreElements()) {
        URL url = urls.nextElement();
        UrlResource resource = new UrlResource(url);
        Properties properties = PropertiesLoaderUtils.loadProperties(resource);
        for (Map.Entry<?, ?> entry : properties.entrySet()) {
          String factoryTypeName = ((String) entry.getKey()).trim();
          String[] factoryImplementationNames =
            StringUtils.commaDelimitedListToStringArray((String) entry.getValue());
          for (String factoryImplementationName : factoryImplementationNames) {
            result.computeIfAbsent(factoryTypeName, key -> new ArrayList<>())
              .add(factoryImplementationName.trim());
          }
        }
      }

      // Replace all lists with unmodifiable lists containing unique elements
      result.replaceAll((factoryType, implementations) -> implementations.stream().distinct()
        .collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList)));
      cache.put(classLoader, result);
    } catch (IOException ex) {
      throw new IllegalArgumentException("Unable to load factories from location [" +
        FACTORIES_RESOURCE_LOCATION + "]", ex);
    }
    return result;
  }
}

자동 설정을 위한 auto-configiration 클래스들은 @Configuration이 있는 설정 클래스들인데, 어떤 클래스의 존재 유무를 판단하기 위해 @Conditional 애노테이션이 사용된다. 많은 조건 애노테이션들 중에서 다음이 가장 많이 사용된다.

  • @ConditionalOnClass : 해당 클래스가 클래스 패스에 존재하는 경우
  • @ConditionalOnBean : 해당 클래스나 이름이 빈 팩토리에 포함되어 있는 경우
  • @ConditionalOnMissingBean : 해당 빈이 등록되어있지 않을 경우
  • 기타…

ConditionalOnMissingBean이 자주 사용되는 이유는 우리가 직접 설정한 빈과 자동 설정으로 등록되는 빈이 중복될 경우 우리가 직접 설정한 빈에 우선순위를 부여하기 위함이다.

@EnableAutoConfiguration이 붙어있는 클래스는 특별한 의미를 갖는다

특정 작업들을 위한 베이스 패키지가 되기 때문이다

대표적으로 JPA의 @Entity 클래스를 탑색하는 작업은 @EnableAutoConfiguration이 붙어있는 클래스의 패키지를 기준으로 진행된다. 일반적으로 @EnableAutoConfiguration@SpringBootApplication 안에 포함되어있으므로 자동 설정이 활성화 되지만, 해당 애노테이션을 직접 붙여주는 경우에는 위와 같은 이유(빈에 우선순위 부여)로 루트 패키지에 있는 클래스에 붙여주는 것이 좋다.

This post is licensed under CC BY 4.0 by the author.