
@ComponentScan 자체는 원래 알고 있던 내용이지만 basePackageClasses, Filter 등 몰랐던 내용이 있어 간단히 정리하고자 한다.
일단 @ComponentScan 자체는 아래처럼 구성되어 있다.
package org.springframework.context.annotation;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Repeatable(ComponentScans.class)
public @interface ComponentScan {
Class<?>[] basePackageClasses() default {};
Filter[] includeFilters() default {};
Filter[] excludeFilters() default {};
/* ... 생략 ... */
}
basePackageClasses 는 basePackage 랑 거의 동일한데, 어느 클래스가 속한 package 를 basePackage 로 지정해주는 속성이기 때문이다.
또한 includeFilters, excludeFilters 는 @ComponentScan 이 클래스를 포함, 제외할 filter 를 설정하는 속성이다.
참고로 @Filter 어노테이션은 @ComponentScan 속에 Nested interface 로 정의되어 있다.
public @interface ComponentScan {
@Retention(RetentionPolicy.RUNTIME)
@Target({})
@interface Filter {
FilterType type() default FilterType.ANNOTATION;
@AliasFor("classes")
Class<?>[] value() default {};
@AliasFor("value")
Class<?>[] classes() default {};
String[] pattern() default {};
}
}
(여담으로 Java SE 에 따르면 중첩 인터페이스는 클래스, 인터페이스 body 에 존재하는 인터페이스이다. 꼭 클래스 내부에 있어야 '중첩 인터페이스' 라 부르는 건 아니다.) [1]
FilterType 은 Enum 상수로 정의되어 아래처럼 분류된다.
FilterType
1.ANNOTATION:
특정 어노테이션을 기반으로 필터를 구성
ASSIGNABLE_TYPE:
클래스, 인터페이스 등의 타입을 기반으로 필터를 구성
ASPECTJ,REGEX:
AspectJ, 정규표현식 등 패턴을 기반으로 필터를 구성
CUSTOM:
사용자가 직접 만든FilterType을 통해 필터를 구성public enum FilterType { ANNOTATION, ASSIGNABLE_TYPE, ASPECTJ, REGEX, CUSTOM }
이를 이용해 아래처럼 "특정 어노테이션이 붙은 개체를 Bean 생성에서 제외" 하는 등 좀 더 유연한 동작이 가능하다.
(물론 정말로 사용할 일이 있는지는 몰?루)
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface MyExclusion {
}
@Configuration
@ComponentScan(
includeFilters = @Filter(type = FilterType.ASSIGNABLE_TYPE, classes = AbstractSupClass.class),
excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = {MyExclusion.class})
)
class Config {
}
조금 더 테스트하다 알게 된 사실인데, FilterType.ASPECTJ 를 이용하려면 실제 AspectJ 라이브러리가 있어야 작동하는 걸 발견했다.
처음엔 클래스 weaving time 차이로 인한 문제일 줄 알았는데 spring-core 라이브러리를 디버깅하다 정확한 원인을 발견했다.
(Spring 기본은 RTW, AspectJ 는 CTW 또는 LTW 로 기억한다.)
@ComponentScan 의 excludeFilters 혹은 includeFilters 를 사용하면 TypeFilterUtils 의 createTypeFiltersFor 메서드가 호출된다.
이 때 FilterType 에 따라 알맞게 filter 를 생성하는데, FilterType.ASPECTJ 이면 새로운AspectJTypeFilter 를 생성하려고 한다.
그런데 AspectJTypeFilter 를 보면 실제 org.assertj 에 사용되는 World, TypePattern 이 필요함을 볼 수 있고, 이 때문에 실제 AspectJ 라이브러리가 필요하다.
즉, 아래처럼 만들어야 FilterType.ASPECTJ 를 사용할 수 있다.
// build.gradle
dependencies {
/* ... */
implementation 'org.springframework.boot:spring-boot-starter-aop'
// starter-aop 에는 애초에 aspectj:aspectjweaver 를 depends on 한다.
// aspectj 라이브러리도 같이 딸려온다는 소리.
}
@Configuration
@ComponentScan(
excludeFilters = @Filter(type = FilterType.ASPECTJ, pattern = "*..SomeClass")
)
public class TestConfig {
}
하지만 풀리지 않는 의문점이 존재하는데, "왜 FilterType.ASPECTJ 를 안 사용했을 때는 잘 작동하는가?" 이다.
결국 문제가 되는 이유는 TypeFilterUtils 에서 AspectJTypeFilter 를 사용하려 하고, AspectJTypeFilter 에는 AspectJ 라이브러리를 필요로 하기 때문이었다.
따라서 애초에 build 시 AspectJTypeFilter 를 필요로 할 것이고, AspectJTypeFilter 를 컴파일 하는 과정에서 AspectJ 라이브러리가 없어 에러가 발생해야 한다.
하지만 확인해보니 정말 이상하게 FilterType.ASPECTJ 를 사용했을 때만 build 실패가 발생하고 사용하지 않았을땐 실패하지 않는다.
혹시 Spring 의 Dependency Management 가 Runtime 에 행해지는지 확인하려 했지만 정확한 공식 문서를 잘 찾지 못했다.
일단 내가 무언가 놓쳤을 지도 모르므로 한번 Spring-framework 깃헙에 해당 문제를 issue 로 물어본 상태이다.
뭔가 새롭게 알게 된 내용이 있다면 추가하겠다.
++
AspectJTypeFilter에러update직접
Spring-framework에 물어보니 이는 의도된 행동이라고 한다.나는
AspectJ가 유명하니까이 패턴대로 정의해 사용하세요!라는 의도로 만들어진 줄 알았는데,AspectJ 기능을 선택해 작동하기 위해 만들어졌다고 한다.또한
FilterType.ASPECTJ를 사용했을 때만 테스트가 실패해 생겼던 궁금증은Spring과 관련된 내용이 아니라Java의Class loading과 관련된 내용이었다.나는 이전에
JavaCompile시점에 바이트 코드 변환 뿐만 아니라, 필요한 클래스들의loading,linking까지 이루어진다고 생각했었다.하지만 알아보니
Class loading,Linking은 런타임 시점에 이루진다 한다.
Java를C처럼 생각해버린 것이다.어느 부분에서 내가 잘못 생각했는지는 파악했지만, 이를 정확하게 정리하려면
Java Class loading에 대한 공부가 필요할 것 같다.
더 깊게 들어가면JVM자체까지 들어가야 될 수도 있을 것 같다.지금 당장 공부할 계획은 없으므로 나중을 위해 관련 자료만 첨부하겠다.
Chapter 9. Interfaces - Java Specification[1] : A nested interface is any interface whose declaration occurs within the body of another class or interface declaration. A nested interface may be a member interface (§8.5, §9.5) or a local interface (§14.3).